chromium/remoting/host/keyboard_layout_monitor_chromeos.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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "remoting/host/keyboard_layout_monitor.h"

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/raw_span.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/sequence_bound.h"
#include "base/time/time.h"
#include "remoting/proto/control.pb.h"
#include "ui/base/ime/ash/ime_keyboard.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.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 remoting {

namespace {

using protocol::LayoutKeyFunction;

constexpr int kShiftLevelFlags[] = {0, ui::EF_SHIFT_DOWN, ui::EF_ALTGR_DOWN,
                                    ui::EF_SHIFT_DOWN | ui::EF_ALTGR_DOWN};

class Core : private ash::input_method::ImeKeyboard::Observer {
 public:
  explicit Core(
      base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback,
      const base::span<const ui::DomCode>& supported_keys);
  Core(const Core&) = delete;
  Core& operator=(const Core&) = delete;
  ~Core() override = default;

  void Start();

 private:
  // `ash::input_method::ImeKeyboard::Observer` implementation.
  void OnCapsLockChanged(bool enabled) override;
  void OnLayoutChanging(const std::string& layout_name) override;

  void QueryLayout();
  LayoutKeyFunction GetFunctionFromKeyboardCode(
      ui::KeyboardCode key_code) const;

  const base::raw_span<const ui::DomCode> supported_keys_;
  base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback_;
  base::ScopedObservation<ash::input_method::ImeKeyboard,
                          ash::input_method::ImeKeyboard::Observer>
      observation_{this};
  base::WeakPtrFactory<Core> weak_ptr_factory_{this};
};

class KeyboardLayoutMonitorChromeOs : public KeyboardLayoutMonitor {
 public:
  explicit KeyboardLayoutMonitorChromeOs(
      base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback,
      scoped_refptr<base::SingleThreadTaskRunner> input_task_runner);
  ~KeyboardLayoutMonitorChromeOs() override;

  // `KeyboardLayoutMonitor` implementation.
  void Start() override;

 private:
  base::SequenceBound<Core> core_;
};

Core::Core(
    base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback,
    const base::span<const ui::DomCode>& supported_keys)
    : supported_keys_(supported_keys), callback_(callback) {}

void Core::Start() {
  QueryLayout();

  observation_.Observe(
      ash::input_method::InputMethodManager::Get()->GetImeKeyboard());
}

void Core::OnCapsLockChanged(bool enabled) {}

void Core::OnLayoutChanging(const std::string& layout_name) {
  // OnLayoutChanging() is triggered when the layout is changing but hasn't
  // changed yet. We can post an async task to allow the OS to 'finish' the
  // layout change however on very slow machines (e.g. a development VM), the
  // layout change takes a hundred milliseconds or so. To ensure the layout
  // change has occurred on a normal machine, we delay the async task by a half
  // second which should be enough. Note that we can't rely on checking the
  // current layout as, from observation, the current keyboard layout name and
  // the actual layout map aren't updated atomically meaning the name will
  // change before the layout is actually loaded.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&Core::QueryLayout, weak_ptr_factory_.GetWeakPtr()),
      base::Milliseconds(500));
}

void Core::QueryLayout() {
  protocol::KeyboardLayout layout_message;
  ui::KeyboardLayoutEngine* keyboard_layout_engine =
      ui::KeyboardLayoutEngineManager::GetKeyboardLayoutEngine();

  bool has_alt_gr = ash::input_method::InputMethodManager::Get()
                        ->GetImeKeyboard()
                        ->IsAltGrAvailable();
  int shift_levels = has_alt_gr ? 4 : 2;
  for (auto code : supported_keys_) {
    std::uint32_t usb_code = ui::KeycodeConverter::DomCodeToUsbKeycode(code);
    auto& key_actions =
        *(*layout_message.mutable_keys())[usb_code].mutable_actions();
    for (int shift_level = 0; shift_level < shift_levels; shift_level++) {
      ui::DomKey key;
      ui::KeyboardCode key_code;
      int event_flags = kShiftLevelFlags[shift_level];
      if (!keyboard_layout_engine->Lookup(code, event_flags, &key, &key_code)) {
        continue;
      }

      LayoutKeyFunction function = GetFunctionFromKeyboardCode(key_code);
      if (function != LayoutKeyFunction::UNKNOWN) {
        key_actions[shift_level].set_function(function);
        continue;
      }

      std::string utf8;
      if (key.IsCharacter()) {
        base::WriteUnicodeCharacter(key.ToCharacter(), &utf8);
      } else if (key.IsDeadKey()) {
        base::WriteUnicodeCharacter(key.ToDeadKeyCombiningCharacter(), &utf8);
      }

      if (!utf8.empty()) {
        key_actions[shift_level].set_character(utf8);
      }
    }

    if (key_actions.empty()) {
      layout_message.mutable_keys()->erase(usb_code);
    }
  }

  callback_.Run(layout_message);
}

LayoutKeyFunction Core::GetFunctionFromKeyboardCode(
    ui::KeyboardCode key_code) const {
  // CAPS_LOCK and NUM_LOCK are left unmapped as they aren't handled by the
  // protocol or client (meaning lock states are not synchronized across so the
  // soft keyboard might not reflect the local settings accurately).
  switch (key_code) {
    case ui::VKEY_SCROLL:
      return LayoutKeyFunction::SCROLL_LOCK;
    case ui::VKEY_BACK:
      return LayoutKeyFunction::BACKSPACE;
    case ui::VKEY_RETURN:
      return LayoutKeyFunction::ENTER;
    case ui::VKEY_TAB:
      return LayoutKeyFunction::TAB;
    case ui::VKEY_INSERT:
      return LayoutKeyFunction::INSERT;
    case ui::VKEY_DELETE:
      return LayoutKeyFunction::DELETE_;
    case ui::VKEY_HOME:
      return LayoutKeyFunction::HOME;
    case ui::VKEY_END:
      return LayoutKeyFunction::END;
    case ui::VKEY_PRIOR:
      return LayoutKeyFunction::PAGE_UP;
    case ui::VKEY_NEXT:
      return LayoutKeyFunction::PAGE_DOWN;
    case ui::VKEY_CLEAR:
      return LayoutKeyFunction::CLEAR;
    case ui::VKEY_UP:
      return LayoutKeyFunction::ARROW_UP;
    case ui::VKEY_DOWN:
      return LayoutKeyFunction::ARROW_DOWN;
    case ui::VKEY_LEFT:
      return LayoutKeyFunction::ARROW_LEFT;
    case ui::VKEY_RIGHT:
      return LayoutKeyFunction::ARROW_RIGHT;
    case ui::VKEY_F1:
      return LayoutKeyFunction::F1;
    case ui::VKEY_F2:
      return LayoutKeyFunction::F2;
    case ui::VKEY_F3:
      return LayoutKeyFunction::F3;
    case ui::VKEY_F4:
      return LayoutKeyFunction::F4;
    case ui::VKEY_F5:
      return LayoutKeyFunction::F5;
    case ui::VKEY_F6:
      return LayoutKeyFunction::F6;
    case ui::VKEY_F7:
      return LayoutKeyFunction::F7;
    case ui::VKEY_F8:
      return LayoutKeyFunction::F8;
    case ui::VKEY_F9:
      return LayoutKeyFunction::F9;
    case ui::VKEY_F10:
      return LayoutKeyFunction::F10;
    case ui::VKEY_F11:
      return LayoutKeyFunction::F11;
    case ui::VKEY_F12:
      return LayoutKeyFunction::F12;
    case ui::VKEY_ESCAPE:
      return LayoutKeyFunction::ESCAPE;
    case ui::VKEY_APPS:
      return LayoutKeyFunction::CONTEXT_MENU;
    case ui::VKEY_PAUSE:
      return LayoutKeyFunction::PAUSE;
    case ui::VKEY_SNAPSHOT:
      return LayoutKeyFunction::PRINT_SCREEN;
    case ui::VKEY_CONTROL:
    case ui::VKEY_LCONTROL:
    case ui::VKEY_RCONTROL:
      return protocol::LayoutKeyFunction::CONTROL;
    case ui::VKEY_SHIFT:
    case ui::VKEY_LSHIFT:
    case ui::VKEY_RSHIFT:
      return protocol::LayoutKeyFunction::SHIFT;
    case ui::VKEY_ALTGR:
      return protocol::LayoutKeyFunction::ALT_GR;
    case ui::VKEY_MENU:
    case ui::VKEY_RMENU:
    case ui::VKEY_LMENU:
      return protocol::LayoutKeyFunction::ALT;
    case ui::VKEY_LWIN:
    case ui::VKEY_RWIN:
      return protocol::LayoutKeyFunction::SEARCH;
    default:
      return LayoutKeyFunction::UNKNOWN;
  }
}

KeyboardLayoutMonitorChromeOs::KeyboardLayoutMonitorChromeOs(
    base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback,
    scoped_refptr<base::SingleThreadTaskRunner> input_task_runner)
    : core_(input_task_runner,
            // Ensure callback is always invoked on the current sequence and not
            // on `input_task_runner`.
            base::BindPostTaskToCurrentDefault(callback),
            kSupportedKeys) {}

KeyboardLayoutMonitorChromeOs::~KeyboardLayoutMonitorChromeOs() = default;

void KeyboardLayoutMonitorChromeOs::Start() {
  core_.AsyncCall(&Core::Start);
}

}  // namespace

std::unique_ptr<KeyboardLayoutMonitor> KeyboardLayoutMonitor::Create(
    base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback,
    scoped_refptr<base::SingleThreadTaskRunner> input_task_runner) {
  return std::make_unique<KeyboardLayoutMonitorChromeOs>(std::move(callback),
                                                         input_task_runner);
}

}  // namespace remoting