chromium/chrome/browser/ui/webui/ash/settings/pages/a11y/switch_access_handler.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/webui/ash/settings/pages/a11y/switch_access_handler.h"

#include <memory>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_constants.h"
#include "ash/constants/ash_pref_names.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/values.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_contents.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_codes_array.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/ozone/layout/keyboard_layout_engine.h"
#include "ui/events/ozone/layout/keyboard_layout_engine_manager.h"

namespace ash::settings {
namespace {

struct AssignmentInfo {
  std::string action_name_for_js;
  std::string pref_name;
};

std::string GetStringForKeyboardCode(ui::KeyboardCode key_code) {
  ui::DomKey dom_key;
  ui::KeyboardCode key_code_to_compare = ui::VKEY_UNKNOWN;
  for (const auto& dom_code : ui::kDomCodesArray) {
    if (!ui::KeyboardLayoutEngineManager::GetKeyboardLayoutEngine()->Lookup(
            dom_code, /*flags=*/ui::EF_NONE, &dom_key, &key_code_to_compare)) {
      continue;
    }
    if (key_code_to_compare != key_code || !dom_key.IsValid() ||
        dom_key.IsDeadKey()) {
      continue;
    }
    // Make sure the space key is rendered as "Space" instead of " ".
    if (key_code == ui::VKEY_SPACE) {
      return l10n_util::GetStringUTF8(IDS_SETTINGS_SWITCH_ASSIGN_OPTION_SPACE);
    }
    return ui::KeycodeConverter::DomKeyToKeyString(dom_key);
  }
  return std::string();
}

std::string GetSwitchAccessDevice(ui::InputDeviceType source_device_type) {
  switch (source_device_type) {
    case ui::INPUT_DEVICE_INTERNAL:
      return kSwitchAccessInternalDevice;
    case ui::INPUT_DEVICE_USB:
      return kSwitchAccessUsbDevice;
    case ui::INPUT_DEVICE_BLUETOOTH:
      return kSwitchAccessBluetoothDevice;
    case ui::INPUT_DEVICE_UNKNOWN:
      return kSwitchAccessUnknownDevice;
  }
}

}  // namespace

SwitchAccessHandler::SwitchAccessHandler(PrefService* prefs) : prefs_(prefs) {}

SwitchAccessHandler::~SwitchAccessHandler() {
  // Ensure we always leave Switch Access in a good state no matter what.
  if (web_ui() && web_ui()->GetWebContents() &&
      web_ui()->GetWebContents()->GetNativeView()) {
    web_ui()->GetWebContents()->GetNativeView()->RemovePreTargetHandler(this);
  }

  if (AccessibilityController::Get()) {
    AccessibilityController::Get()->SuspendSwitchAccessKeyHandling(false);
  }
}

void SwitchAccessHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      "refreshAssignmentsFromPrefs",
      base::BindRepeating(
          &SwitchAccessHandler::HandleRefreshAssignmentsFromPrefs,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "notifySwitchAccessActionAssignmentPaneActive",
      base::BindRepeating(
          &SwitchAccessHandler::
              HandleNotifySwitchAccessActionAssignmentPaneActive,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "notifySwitchAccessActionAssignmentPaneInactive",
      base::BindRepeating(
          &SwitchAccessHandler::
              HandleNotifySwitchAccessActionAssignmentPaneInactive,
          base::Unretained(this)));
}

void SwitchAccessHandler::AddPreTargetHandler() {
  CHECK(IsJavascriptAllowed());

  if (on_pre_target_handler_added_for_testing_) {
    std::move(on_pre_target_handler_added_for_testing_).Run();
  }
  if (skip_pre_target_handler_for_testing_) {
    return;
  }

  gfx::NativeView native_view = web_ui()->GetWebContents()->GetNativeView();

  // Ensure we don't add the handler twice.
  native_view->RemovePreTargetHandler(this);
  native_view->AddPreTargetHandler(this);
}

void SwitchAccessHandler::OnJavascriptAllowed() {
  if (action_assignment_pane_active_) {
    AddPreTargetHandler();
  }

  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(prefs_);
  pref_change_registrar_->Add(
      ash::prefs::kAccessibilitySwitchAccessSelectDeviceKeyCodes,
      base::BindRepeating(
          &SwitchAccessHandler::OnSwitchAccessAssignmentsUpdated,
          base::Unretained(this)));
  pref_change_registrar_->Add(
      ash::prefs::kAccessibilitySwitchAccessNextDeviceKeyCodes,
      base::BindRepeating(
          &SwitchAccessHandler::OnSwitchAccessAssignmentsUpdated,
          base::Unretained(this)));
  pref_change_registrar_->Add(
      ash::prefs::kAccessibilitySwitchAccessPreviousDeviceKeyCodes,
      base::BindRepeating(
          &SwitchAccessHandler::OnSwitchAccessAssignmentsUpdated,
          base::Unretained(this)));
}

void SwitchAccessHandler::OnJavascriptDisallowed() {
  web_ui()->GetWebContents()->GetNativeView()->RemovePreTargetHandler(this);
  pref_change_registrar_.reset();
}

void SwitchAccessHandler::OnKeyEvent(ui::KeyEvent* event) {
  CHECK(action_assignment_pane_active_);
  event->StopPropagation();
  event->SetHandled();

  if (event->type() == ui::EventType::kKeyReleased) {
    return;
  }

  base::Value::Dict response;
  response.Set("keyCode", static_cast<int>(event->key_code()));
  response.Set("key", GetStringForKeyboardCode(event->key_code()));
  ui::InputDeviceType deviceType = ui::INPUT_DEVICE_UNKNOWN;

  int source_device_id = event->source_device_id();
  for (const auto& keyboard :
       ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
    if (source_device_id == keyboard.id) {
      deviceType = keyboard.type;
      break;
    }
  }
  response.Set("device", GetSwitchAccessDevice(deviceType));

  FireWebUIListener("switch-access-got-key-press-for-assignment", response);
}

void SwitchAccessHandler::HandleRefreshAssignmentsFromPrefs(
    const base::Value::List& args) {
  AllowJavascript();
  OnSwitchAccessAssignmentsUpdated();
}

void SwitchAccessHandler::HandleNotifySwitchAccessActionAssignmentPaneActive(
    const base::Value::List& args) {
  action_assignment_pane_active_ = true;
  if (!IsJavascriptAllowed()) {
    AllowJavascript();
  } else {
    AddPreTargetHandler();
  }
  OnSwitchAccessAssignmentsUpdated();
  AccessibilityController::Get()->SuspendSwitchAccessKeyHandling(true);
}

void SwitchAccessHandler::HandleNotifySwitchAccessActionAssignmentPaneInactive(
    const base::Value::List& args) {
  action_assignment_pane_active_ = false;
  if (!skip_pre_target_handler_for_testing_) {
    web_ui()->GetWebContents()->GetNativeView()->RemovePreTargetHandler(this);
  }
  AccessibilityController::Get()->SuspendSwitchAccessKeyHandling(false);
}

void SwitchAccessHandler::OnSwitchAccessAssignmentsUpdated() {
  base::Value::Dict response;

  static base::NoDestructor<std::vector<AssignmentInfo>> kAssignmentInfo({
      {"select", ash::prefs::kAccessibilitySwitchAccessSelectDeviceKeyCodes},
      {"next", ash::prefs::kAccessibilitySwitchAccessNextDeviceKeyCodes},
      {"previous",
       ash::prefs::kAccessibilitySwitchAccessPreviousDeviceKeyCodes},
  });

  for (const AssignmentInfo& info : *kAssignmentInfo) {
    const auto& keycodes = prefs_->GetDict(info.pref_name);
    base::Value::List keys;
    for (const auto item : keycodes) {
      int key_code;
      if (!base::StringToInt(item.first, &key_code)) {
        NOTREACHED_IN_MIGRATION();
        return;
      }
      for (const base::Value& device_type : item.second.GetList()) {
        base::Value::Dict key;
        key.Set("key", GetStringForKeyboardCode(
                           static_cast<ui::KeyboardCode>(key_code)));
        key.Set("device", device_type.GetString());
        keys.Append(std::move(key));
      }
    }
    response.SetByDottedPath(info.action_name_for_js, std::move(keys));
  }

  FireWebUIListener("switch-access-assignments-changed", response);
}

}  // namespace ash::settings