910e62b5创建于 1月15日历史提交
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "remoting/host/linux/ei_keymap.h"

#include <string_view>
#include <xkbcommon/xkbcommon.h>

#include "base/files/file.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "remoting/host/linux/keyboard_layout_monitor_utils.h"
#include "remoting/proto/control.pb.h"
#include "ui/events/keycodes/dom/keycode_converter.h"

namespace remoting {

EiKeymap::EiKeymap(EiDevicePtr keyboard) : keyboard_(keyboard) {}

EiKeymap::~EiKeymap() = default;

base::WeakPtr<EiKeymap> EiKeymap::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void EiKeymap::Load(base::OnceClosure callback) {
  base::ScopedClosureRunner closure_runner(std::move(callback));
  // Get the keymap file descriptor and verify that it's (at time of writing)
  // the only supported type.
  struct ei_keymap* keymap = ei_device_keyboard_get_keymap(keyboard_.get());
  if (!keymap) {
    LOG(ERROR) << "No keymap found for the current keyboard";
    return;
  }
  auto type = ei_keymap_get_type(keymap);
  if (type != EI_KEYMAP_TYPE_XKB) {
    LOG(ERROR) << "Unsupported keymap type: " << type;
    return;
  }
  // The file descriptor is owned by libei so needs to be dup'd before using
  // ScopedFD.
  auto original_fd = ei_keymap_get_fd(keymap);
  base::ScopedFD fd(HANDLE_EINTR(dup(original_fd)));
  if (!fd.is_valid()) {
    LOG(ERROR) << "Failed to duplicate keymap file descriptor";
    return;
  }
  // Since the reader as a member variable, base::Unretained is safe.
  reader_ = FdStringReader::ReadFromFile(
      std::move(fd),
      base::BindOnce(&EiKeymap::OnKeymapLoaded, base::Unretained(this),
                     closure_runner.Release()));
}

bool EiKeymap::IsValid() const {
  return keymap_ != nullptr;
}

xkb_keymap* EiKeymap::Get() {
  return keymap_.get();
}

const protocol::KeyboardLayout& EiKeymap::GetLayoutProto() const {
  return layout_proto_;
}

EiKeymap::Recipe::Recipe(uint32_t usb_code) : usb_code(usb_code) {}
EiKeymap::Recipe::Recipe(const EiKeymap::Recipe& other) = default;
EiKeymap::Recipe::~Recipe() = default;

EiKeymap::Recipe EiKeymap::GetRecipeForCodepoint(
    uint32_t codepoint) const {
  auto it = codepoint_to_usb_code_and_shift_level_.find(codepoint);
  if (it == codepoint_to_usb_code_and_shift_level_.end()) {
    return Recipe(0);
  }
  const auto& [usb_code, shift_level] = it->second;
  Recipe result(usb_code);
  if (shift_level & 1) {
    result.modifiers.insert(shift_key_usb_code_);
  }
  if (shift_level & 2) {
    result.modifiers.insert(altgr_key_usb_code_);
  }
  return result;
}

void EiKeymap::OnKeymapLoaded(base::OnceClosure callback,
                              base::expected<std::string, Loggable> result) {
  base::ScopedClosureRunner closure_runner(std::move(callback));
  if (!result.has_value()) {
    LOG(ERROR) << "Reading keymap failed: " << result.error();
    return;
  }
  // Create an xkb_keymap object from the mapped string.
  std::unique_ptr<xkb_context, ui::XkbContextDeleter> ctx{
      xkb_context_new(XKB_CONTEXT_NO_FLAGS)};
  if (!ctx) {
    LOG(ERROR) << "Failed to create XKB context";
    return;
  }
  keymap_.reset(xkb_keymap_new_from_string(ctx.get(), result.value().c_str(),
                                           XKB_KEYMAP_FORMAT_TEXT_V1,
                                           XKB_KEYMAP_COMPILE_NO_FLAGS));
  auto numlock_index = xkb_keymap_mod_get_index(keymap_.get(), "NumLock");
  auto numlock_mask = (numlock_index == XKB_MOD_INVALID) ? 0 : 1 << numlock_index;
  auto shift_index = xkb_keymap_mod_get_index(keymap_.get(), "Shift");
  auto shift_mask = (shift_index == XKB_MOD_INVALID) ? 0 : 1 << shift_index;
  auto altgr_index = xkb_keymap_mod_get_index(keymap_.get(), "Mod5");
  auto altgr_mask = (altgr_index == XKB_MOD_INVALID) ? 0 : 1 << altgr_index;
  shift_level_to_mask_[0] = numlock_mask;
  shift_level_to_mask_[1] = shift_mask | numlock_mask;
  if (altgr_mask) {
    shift_level_to_mask_[2] = altgr_mask | numlock_mask;
    shift_level_to_mask_[3] = shift_mask | altgr_mask | numlock_mask;
  }
  xkb_state_.reset(xkb_state_new(keymap_.get()));
  layout_proto_ = protocol::KeyboardLayout();
  xkb_keymap_key_for_each(keymap_.get(), &EiKeymap::ProcessKey, this);
}

bool EiKeymap::CanAutoRepeatUsbCode(uint32_t usb_code) const {
  return idempotent_usb_codes_.contains(usb_code);
}

void EiKeymap::ProcessKey(xkb_keymap* keymap,
                          xkb_keycode_t keycode,
                          void* data) {
  auto* self = static_cast<EiKeymap*>(data);
  auto usb_keycode = ui::KeycodeConverter::NativeKeycodeToUsbKeycode(keycode);
  auto& actions =
      *(*self->layout_proto_.mutable_keys())[usb_keycode].mutable_actions();
  bool is_idempotent = false;
  for (auto [level, mask] : self->shift_level_to_mask_) {
    xkb_state_update_mask(self->xkb_state_.get(), mask, 0, 0, 0, 0, 0);
    auto keysym = xkb_state_key_get_one_sym(self->xkb_state_.get(), keycode);
    if (keysym == XKB_KEY_NoSymbol) {
      // Keys with no symbols are fairly common, and should be ignored. In
      // theory, keys with multiple symbols might exist, and could perhaps
      // be supported (for example, if all symbols correspond to characters
      // then they could be concatenated), but it seems to be very uncommon
      // so they are ignored for now.
      continue;
    }
    if (keysym == XKB_KEY_Num_Lock || keysym == XKB_KEY_Caps_Lock) {
      // Don't include Num Lock or Caps Lock because the client keyboard
      // doesn't support them.
      continue;
    }
    // Convert the key to a function and a codepoint. Keys may have either,
    // both, or neither of these.
    auto function = KeyvalToFunction(keysym);
    auto codepoint = xkb_keysym_to_utf32(keysym);
    // Use the function to update the modifier map. Note that we assume that
    // modifier keys do the same thing regardless of shift level, so we don't
    // record the shift level.
    switch (function) {
      case protocol::LayoutKeyFunction::SHIFT:
        self->shift_key_usb_code_ = usb_keycode;
        break;
      case protocol::LayoutKeyFunction::ALT_GR:
        self->altgr_key_usb_code_ = usb_keycode;
        break;
      default:
        break;
    }
    // Use the character to update the character map if it doesn't already
    // exist with a lower shift level. Because of the way shift levels are
    // numbered and the fact that we only support Shift and AltGr, a lower
    // shift level indicates fewer modifiers, which is better for synthesizing
    // input using key presses.
    //
    // TODO(crbug.com/440652982): We've previously assumed that NumLock is on
    // when computing key actions. This means that injecting text events will
    // not work correctly if NumLock is off.
    if (codepoint) {
      auto& existing = self->codepoint_to_usb_code_and_shift_level_[codepoint];
      if (existing.usb_code == 0 || existing.shift_level > level) {
        existing.usb_code = usb_keycode;
        existing.shift_level = level;
      }
    }
    // Decide whether or not the key is idempotent, based on its function and
    // whether or not it has a printable codepoint at shift level 0. Note that
    // this doesn't handle complex scenarios where a key is idempotent at one
    // shift level but not another, but the code that suppresses auto-repeat
    // doesn't handle that case either.
    if (level == 0) {
      switch (function) {
        // Keys with codepoints that are nevertheless idempotent.
        case protocol::LayoutKeyFunction::ESCAPE:
          is_idempotent = true;
          break;

        // Keys without codepoints that are nevertheless not idempotent.
        case protocol::LayoutKeyFunction::ARROW_DOWN:
        case protocol::LayoutKeyFunction::ARROW_LEFT:
        case protocol::LayoutKeyFunction::ARROW_RIGHT:
        case protocol::LayoutKeyFunction::ARROW_UP:
        case protocol::LayoutKeyFunction::SCROLL_LOCK:
        case protocol::LayoutKeyFunction::PRINT_SCREEN:
        case protocol::LayoutKeyFunction::PAGE_UP:
        case protocol::LayoutKeyFunction::PAGE_DOWN:
        case protocol::LayoutKeyFunction::INSERT:
          is_idempotent = false;
          break;

        // For all other keys, idempotent-ness is equated with having a
        // codepoint.
        default:
          is_idempotent = (codepoint == 0);
          break;
      }
    }
    // The function takes precedence over the character so that the correct
    // symbol is displayed by the client for things like the Enter key.
    if (function != protocol::LayoutKeyFunction::UNKNOWN) {
      actions[level].set_function(function);
      continue;
    }
    // Handle regular characters next.
    if (codepoint) {
      std::string utf8;
      base::WriteUnicodeCharacter(codepoint, &utf8);
      actions[level].set_character(utf8);
      continue;
    }
    // Handle dead keys last.
    const char* dead_key_utf8 = DeadKeyToUtf8String(keysym);
    if (dead_key_utf8) {
      actions[level].set_character(dead_key_utf8);
      continue;
    }
  }
  // Delete the entry that was implicitly added by the []-operator if
  // there are no actions.
  if (actions.empty()) {
    self->layout_proto_.mutable_keys()->erase(usb_keycode);
  }
  // Update the set of idempotent keys.
  if (usb_keycode && is_idempotent) {
    self->idempotent_usb_codes_.insert(usb_keycode);
  }
}

}  // namespace remoting