chromium/ash/webui/diagnostics_ui/backend/input/input_data_provider_keyboard.cc

// Copyright 2021 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 "ash/webui/diagnostics_ui/backend/input/input_data_provider_keyboard.h"

#include <fcntl.h>
#include <linux/input.h>

#include <string_view>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "ash/display/privacy_screen_controller.h"
#include "ash/ime/ime_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/diagnostics/mojom/input.mojom-shared.h"
#include "ash/webui/diagnostics_ui/backend/input/input_data_provider.h"
#include "ash/webui/diagnostics_ui/mojom/input_data_provider.mojom-shared.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/fixed_flat_map.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "ui/events/ash/keyboard_capability.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/ozone/evdev/event_device_info.h"

namespace ash {
namespace diagnostics {

namespace {

enum {
  kFKey1 = 0,
  kFKey2,
  kFKey3,
  kFKey4,
  kFKey5,
  kFKey6,
  kFKey7,
  kFKey8,
  kFKey9,
  kFKey10,
  kFKey11,
  kFKey12,
  kFKey13,
  kFKey14,
  kFKey15
};

// Numeric values of evdev KEY_F# are non-contiguous, making this mapping
// non-trivial.
constexpr auto kFKeyOrder =
    base::MakeFixedFlatMap<uint32_t, unsigned int>({{KEY_F1, kFKey1},
                                                    {KEY_F2, kFKey2},
                                                    {KEY_F3, kFKey3},
                                                    {KEY_F4, kFKey4},
                                                    {KEY_F5, kFKey5},
                                                    {KEY_F6, kFKey6},
                                                    {KEY_F7, kFKey7},
                                                    {KEY_F8, kFKey8},
                                                    {KEY_F9, kFKey9},
                                                    {KEY_F10, kFKey10},
                                                    {KEY_F11, kFKey11},
                                                    {KEY_F12, kFKey12},
                                                    {KEY_F13, kFKey13},
                                                    {KEY_F14, kFKey14},
                                                    {KEY_F15, kFKey15}});

// Hard-coded top-row key mappings. These are intended to match the behaviour of
// EventRewriterAsh::RewriteFunctionKeys for historical keyboards. No
// updates should be needed, as all new keyboards are expected to be using
// customizable top row keys (vivaldi).

constexpr mojom::TopRowKey kSystemKeys1[] = {
    mojom::TopRowKey::kBack,
    mojom::TopRowKey::kForward,
    mojom::TopRowKey::kRefresh,
    mojom::TopRowKey::kFullscreen,
    mojom::TopRowKey::kOverview,
    mojom::TopRowKey::kScreenBrightnessDown,
    mojom::TopRowKey::kScreenBrightnessUp,
    mojom::TopRowKey::kVolumeMute,
    mojom::TopRowKey::kVolumeDown,
    mojom::TopRowKey::kVolumeUp};

constexpr mojom::TopRowKey kSystemKeys2[] = {
    mojom::TopRowKey::kBack,
    mojom::TopRowKey::kRefresh,
    mojom::TopRowKey::kFullscreen,
    mojom::TopRowKey::kOverview,
    mojom::TopRowKey::kScreenBrightnessDown,
    mojom::TopRowKey::kScreenBrightnessUp,
    mojom::TopRowKey::kPlayPause,
    mojom::TopRowKey::kVolumeMute,
    mojom::TopRowKey::kVolumeDown,
    mojom::TopRowKey::kVolumeUp};

constexpr mojom::TopRowKey kSystemKeysWilco[] = {
    mojom::TopRowKey::kBack,
    mojom::TopRowKey::kRefresh,
    mojom::TopRowKey::kFullscreen,
    mojom::TopRowKey::kOverview,
    mojom::TopRowKey::kScreenBrightnessDown,
    mojom::TopRowKey::kScreenBrightnessUp,
    mojom::TopRowKey::kVolumeMute,
    mojom::TopRowKey::kVolumeDown,
    mojom::TopRowKey::kVolumeUp,
    mojom::TopRowKey::kNone,          // F10
    mojom::TopRowKey::kNone,          // F11
    mojom::TopRowKey::kScreenMirror,  // F12
    mojom::TopRowKey::kDelete  // Just a normal Delete key, but in the top row.
};

constexpr mojom::TopRowKey kSystemKeysDrallion[] = {
    mojom::TopRowKey::kBack,
    mojom::TopRowKey::kRefresh,
    mojom::TopRowKey::kFullscreen,
    mojom::TopRowKey::kOverview,
    mojom::TopRowKey::kScreenBrightnessDown,
    mojom::TopRowKey::kScreenBrightnessUp,
    mojom::TopRowKey::kVolumeMute,
    mojom::TopRowKey::kVolumeDown,
    mojom::TopRowKey::kVolumeUp,
    mojom::TopRowKey::kNone,  // F10
    mojom::TopRowKey::kNone,  // F11
    mojom::TopRowKey::kNone,  // F12 - May be Privacy Screen on some models.
    mojom::TopRowKey::kScreenMirror,
    mojom::TopRowKey::kDelete  // Just a normal Delete key, but in the top row.
};

// Wilco and Drallion have unique 'action' scancodes for their top rows,
// that are different from the vivaldi mappings. These scancodes are generated
// when a top-tow key is pressed without the /Fn/ modifier.
constexpr uint32_t kScancodesWilco[] = {
    0xEA, 0xE7, 0xD5, 0xD6, 0x95, 0x91, 0xA0,
    0xAE, 0xB0, 0x44, 0x57, 0x8B, 0xD3,
};

constexpr uint32_t kScancodesDrallion[] = {
    0xEA, 0xE7, 0xD5, 0xD6, 0x95, 0x91, 0xA0,
    0xAE, 0xB0, 0x44, 0x57, 0xd7, 0x8B, 0xD3,
};

// Turkish F-Type xkb keyboard layout id which is used to differentiate between
// a device from 'tr' region with Q-Type vs F-Type.
constexpr std::string_view kTurkishFLayoutId = "xkb:tr:f:tur";

// |kTurkeyRegionCode| is the real turkey region code.
// |kTurkeyFLayoutRegionCode| is used purely in the diagnostics app to
// accurately display F-Type keyboard layouts.
constexpr std::string_view kTurkeyRegionCode = "tr";
constexpr std::string_view kTurkeyFLayoutRegionCode = "tr.f";

mojom::MechanicalLayout GetSystemMechanicalLayout() {
  system::StatisticsProvider* stats_provider =
      system::StatisticsProvider::GetInstance();
  const std::optional<std::string_view> layout_string =
      stats_provider->GetMachineStatistic(system::kKeyboardMechanicalLayoutKey);
  if (!layout_string) {
    LOG(ERROR) << "Couldn't determine mechanical layout";
    return mojom::MechanicalLayout::kUnknown;
  }
  if (layout_string == "ANSI") {
    return mojom::MechanicalLayout::kAnsi;
  } else if (layout_string == "ISO") {
    return mojom::MechanicalLayout::kIso;
  } else if (layout_string == "JIS") {
    return mojom::MechanicalLayout::kJis;
  } else {
    LOG(ERROR) << "Unknown mechanical layout " << layout_string.value();
    return mojom::MechanicalLayout::kUnknown;
  }
}

std::optional<std::string> GetRegionCode() {
  system::StatisticsProvider* stats_provider =
      system::StatisticsProvider::GetInstance();
  const std::optional<std::string_view> layout_string =
      stats_provider->GetMachineStatistic(system::kRegionKey);
  if (!layout_string) {
    LOG(ERROR) << "Couldn't determine region";
    return std::nullopt;
  }

  // In Turkey, two different layouts are shipped (Q-Type and F-Type) under the
  // same region code |kTurkeyRegionCode|. To do a best effort differentiation
  // between the two, query the current IME. If it is |kTurkishFLayoutId|,
  // return our made up |kTurnishFLayoutRegionCode|.
  if (layout_string.value() == kTurkeyRegionCode) {
    ImeControllerImpl* controller = Shell::Get()->ime_controller();
    DCHECK(controller);
    if (base::EndsWith(controller->current_ime().id, kTurkishFLayoutId)) {
      return std::string(kTurkeyFLayoutRegionCode);
    }
  }

  return std::string(layout_string.value());
}

constexpr mojom::TopRowKey ConvertTopRowActionKeyToDiagnosticsTopRowKey(
    ui::TopRowActionKey action_key) {
  switch (action_key) {
    case ui::TopRowActionKey::kBack:
      return mojom::TopRowKey::kBack;
    case ui::TopRowActionKey::kForward:
      return mojom::TopRowKey::kForward;
    case ui::TopRowActionKey::kRefresh:
      return mojom::TopRowKey::kRefresh;
    case ui::TopRowActionKey::kFullscreen:
      return mojom::TopRowKey::kFullscreen;
    case ui::TopRowActionKey::kOverview:
      return mojom::TopRowKey::kOverview;
    case ui::TopRowActionKey::kScreenshot:
      return mojom::TopRowKey::kScreenshot;
    case ui::TopRowActionKey::kScreenBrightnessDown:
      return mojom::TopRowKey::kScreenBrightnessDown;
    case ui::TopRowActionKey::kScreenBrightnessUp:
      return mojom::TopRowKey::kScreenBrightnessUp;
    case ui::TopRowActionKey::kMicrophoneMute:
      return mojom::TopRowKey::kMicrophoneMute;
    case ui::TopRowActionKey::kVolumeMute:
      return mojom::TopRowKey::kVolumeMute;
    case ui::TopRowActionKey::kVolumeDown:
      return mojom::TopRowKey::kVolumeDown;
    case ui::TopRowActionKey::kVolumeUp:
      return mojom::TopRowKey::kVolumeUp;
    case ui::TopRowActionKey::kKeyboardBacklightToggle:
      return mojom::TopRowKey::kKeyboardBacklightToggle;
    case ui::TopRowActionKey::kKeyboardBacklightDown:
      return mojom::TopRowKey::kKeyboardBacklightDown;
    case ui::TopRowActionKey::kKeyboardBacklightUp:
      return mojom::TopRowKey::kKeyboardBacklightUp;
    case ui::TopRowActionKey::kNextTrack:
      return mojom::TopRowKey::kNextTrack;
    case ui::TopRowActionKey::kPreviousTrack:
      return mojom::TopRowKey::kPreviousTrack;
    case ui::TopRowActionKey::kPlayPause:
      return mojom::TopRowKey::kPlayPause;
    case ui::TopRowActionKey::kPrivacyScreenToggle:
      return mojom::TopRowKey::kPrivacyScreenToggle;
    case ui::TopRowActionKey::kAllApplications:
    case ui::TopRowActionKey::kEmojiPicker:
    case ui::TopRowActionKey::kDictation:
    case ui::TopRowActionKey::kAccessibility:
    case ui::TopRowActionKey::kUnknown:
      return mojom::TopRowKey::kUnknown;
    case ui::TopRowActionKey::kNone:
      return mojom::TopRowKey::kNone;
  }
}

}  // namespace

InputDataProviderKeyboard::InputDataProviderKeyboard() {}
InputDataProviderKeyboard::~InputDataProviderKeyboard() {}

InputDataProviderKeyboard::AuxData::AuxData() = default;
InputDataProviderKeyboard::AuxData::~AuxData() = default;

void InputDataProviderKeyboard::ProcessKeyboardTopRowLayout(
    const InputDeviceInformation* device_info,
    ui::KeyboardCapability::KeyboardTopRowLayout top_row_layout,
    const std::vector<uint32_t>& top_row_scan_codes,
    std::vector<mojom::TopRowKey>* out_top_row_keys,
    AuxData* out_aux_data) {
  // Simple array in physical order from left to right
  std::vector<mojom::TopRowKey> top_row_keys;

  // Map of scan-code -> index within tow_row_keys: 0 is first key to the
  // right of Escape, 1 is next key to the right of it, etc.
  base::flat_map<uint32_t, uint32_t> top_row_key_scancode_indexes;

  switch (top_row_layout) {
    case ui::KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayoutWilco:
      top_row_keys.assign(std::begin(kSystemKeysWilco),
                          std::end(kSystemKeysWilco));

      for (size_t i = 0; i < top_row_keys.size(); i++)
        top_row_key_scancode_indexes[kScancodesWilco[i]] = i;
      break;

    case ui::KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayoutDrallion:
      top_row_keys.assign(std::begin(kSystemKeysDrallion),
                          std::end(kSystemKeysDrallion));

      for (size_t i = 0; i < top_row_keys.size(); i++)
        top_row_key_scancode_indexes[kScancodesDrallion[i]] = i;

      // On some Drallion devices, the F12 key is used for the Privacy Screen.

      // The scancode for F12 does not need to be modified, it is the same on
      // all Drallion devices, only the interpretation of the key is different.

      // This should be the same logic as in
      // EventRewriterControllerImpl::Initialize. This is a historic device, and
      // this logic should not need to be updated, as newer devices will use
      // custom top row layouts (vivaldi).
      if (Shell::Get()->privacy_screen_controller() &&
          Shell::Get()->privacy_screen_controller()->IsSupported()) {
        top_row_keys[kFKey12] = mojom::TopRowKey::kPrivacyScreenToggle;
      }

      break;

    case ui::KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayoutCustom: {
      top_row_keys.reserve(top_row_scan_codes.size());

      // If action keys cannot be found or if it has a different size than the
      // top row scan codes, do not fill out the arrays.
      // This will only happen if there is an error in `KeyboardCapability`.
      const auto* action_keys =
          Shell::Get()->keyboard_capability()->GetTopRowActionKeys(
              ui::KeyboardDevice(device_info->input_device));
      if (!action_keys || action_keys->size() != top_row_scan_codes.size()) {
        break;
      }

      // Exclude all top row keys which are considered `kNone`.
      size_t index = 0;
      for (size_t i = 0; i < top_row_scan_codes.size(); i++) {
        auto top_row_key =
            ConvertTopRowActionKeyToDiagnosticsTopRowKey((*action_keys)[i]);
        if (top_row_key == mojom::TopRowKey::kNone) {
          continue;
        }
        top_row_keys.push_back(top_row_key);
        top_row_key_scancode_indexes[top_row_scan_codes[i]] = index++;
      }
      break;
    }

    case ui::KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayout2:
      top_row_keys.assign(std::begin(kSystemKeys2), std::end(kSystemKeys2));
      // No specific top_row_key_scancode_indexes are needed
      // for classic ChromeOS keyboards, as they do not have an /Fn/ key and
      // only emit /F[0-9]+/ keys.
      break;

    case ui::KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayout1:
    default:
      top_row_keys.assign(std::begin(kSystemKeys1), std::end(kSystemKeys1));
      // No specific top_row_key_scancode_indexes are needed for classic
      // ChromeOS keyboards, as they do not have an /Fn/ key and only emit
      // /F[0-9]+/ keys.
      //
      // If this is an unknown keyboard and we are just using Layout1 as
      // the default, we also do not want to assign any scancode or keycode
      // indexes, as we do not know whether the keyboard can generate special
      // keys, or their location relative to the top row.
  }

  *out_top_row_keys = std::move(top_row_keys);
  out_aux_data->top_row_key_scancode_indexes =
      std::move(top_row_key_scancode_indexes);
}

mojom::KeyboardInfoPtr InputDataProviderKeyboard::ConstructKeyboard(
    const InputDeviceInformation* device_info,
    AuxData* out_aux_data) {
  mojom::KeyboardInfoPtr result = mojom::KeyboardInfo::New();

  result->id = device_info->evdev_id;
  result->connection_type = device_info->connection_type;
  result->name = device_info->event_device_info.name();

  // TODO(crbug.com/1207678): review support for WWCB keyboards, Chromebase
  // keyboards, and Dell KM713 Chrome keyboard.

  ProcessKeyboardTopRowLayout(device_info, device_info->keyboard_top_row_layout,
                              device_info->keyboard_scan_codes,
                              &result->top_row_keys, out_aux_data);

  // Work out the physical layout.
  if (device_info->keyboard_type ==
      ui::KeyboardCapability::DeviceType::kDeviceInternalKeyboard) {
    // Reven boards have unknown keyboard layouts and should not be considered
    // internal keyboards for the purposes of diagnostics.
    if (switches::IsRevenBranding()) {
      result->physical_layout = mojom::PhysicalLayout::kUnknown;
      result->connection_type = mojom::ConnectionType::kUnknown;
    } else if (device_info->keyboard_top_row_layout ==
               ui::KeyboardCapability::KeyboardTopRowLayout::
                   kKbdTopRowLayoutWilco) {
      result->physical_layout =
          mojom::PhysicalLayout::kChromeOSDellEnterpriseWilco;
    } else if (device_info->keyboard_top_row_layout ==
               ui::KeyboardCapability::KeyboardTopRowLayout::
                   kKbdTopRowLayoutDrallion) {
      result->physical_layout =
          mojom::PhysicalLayout::kChromeOSDellEnterpriseDrallion;
    } else {
      result->physical_layout = mojom::PhysicalLayout::kChromeOS;
    }
  } else {
    result->physical_layout = mojom::PhysicalLayout::kUnknown;
  }

  // Get the mechanical and visual layouts, if possible.
  if (result->physical_layout != mojom::PhysicalLayout::kUnknown) {
    result->mechanical_layout = GetSystemMechanicalLayout();
    result->region_code = GetRegionCode();
  } else {
    result->mechanical_layout = mojom::MechanicalLayout::kUnknown;
    result->region_code = std::nullopt;
  }

  // Determine number pad presence.
  if (result->physical_layout != mojom::PhysicalLayout::kUnknown) {
    result->number_pad_present =
        base::CommandLine::ForCurrentProcess()->HasSwitch(
            switches::kHasNumberPad)
            ? mojom::NumberPadPresence::kPresent
            : mojom::NumberPadPresence::kNotPresent;

    // Log if there is contradictory information.
    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            switches::kHasNumberPad) &&
        !device_info->event_device_info.HasNumberpad())
      LOG(ERROR) << "OS believes internal numberpad is implemented, but "
                    "evdev disagrees.";
  } else if (device_info->keyboard_top_row_layout ==
             ui::KeyboardCapability::KeyboardTopRowLayout::
                 kKbdTopRowLayoutCustom) {
    // If keyboard has WWCB top row custom layout (vivaldi) then we can trust
    // the HID descriptor to be accurate about presence of keys.
    result->number_pad_present = device_info->event_device_info.HasNumberpad()
                                     ? mojom::NumberPadPresence::kPresent
                                     : mojom::NumberPadPresence::kNotPresent;
  } else {
    // Without WWCB information, absence of KP keycodes means it definitely
    // doesn't have a numberpad, but the presence isn't a reliable indicator.
    result->number_pad_present = device_info->event_device_info.HasNumberpad()
                                     ? mojom::NumberPadPresence::kUnknown
                                     : mojom::NumberPadPresence::kNotPresent;
  }

  // Logic in InputDataProvider will change kUnknown to the most likely one in
  // cases where we can't be sure.
  result->top_right_key = mojom::TopRightKey::kUnknown;
  if (result->physical_layout != mojom::PhysicalLayout::kUnknown) {
    if (result->physical_layout ==
        mojom::PhysicalLayout::kChromeOSDellEnterpriseWilco) {
      // The first generation of Wilco devices both have lock in the top-right
      // (and a separate power key).
      result->top_right_key = mojom::TopRightKey::kLock;
    } else if (device_info->event_device_info.bustype() == BUS_USB) {
      // It's a detachable keyboard (counted as internal USB), so it definitely
      // has Lock in the top-right.
      result->top_right_key = mojom::TopRightKey::kLock;
    } else if (device_info->event_device_info.HasKeyEvent(KEY_CONTROLPANEL)) {
      // All actual internal keyboards (not detachable) with KEY_CONTROLPANEL
      // (i.e. Eve) have the Control Panel key in the top right.
      result->top_right_key = mojom::TopRightKey::kControlPanel;
    }
  }

  result->has_assistant_key =
      device_info->event_device_info.HasKeyEvent(KEY_ASSISTANT);

  return result;
}

mojom::KeyEventPtr InputDataProviderKeyboard::ConstructInputKeyEvent(
    const mojom::KeyboardInfoPtr& keyboard,
    const AuxData* aux_data,
    uint32_t key_code,
    uint32_t scan_code,
    bool down) {
  mojom::KeyEventPtr event = mojom::KeyEvent::New();
  event->id = keyboard->id;
  event->type =
      down ? mojom::KeyEventType::kPress : mojom::KeyEventType::kRelease;
  event->key_code = key_code;    // evdev code
  event->scan_code = scan_code;  // scan code
  event->top_row_position = -1;

  // If a top row action key was pressed, note its physical index in the row.
  const auto iter =
      aux_data->top_row_key_scancode_indexes.find(event->scan_code);
  if (iter != aux_data->top_row_key_scancode_indexes.end()) {
    event->top_row_position = iter->second;
  }

  // Do the same if F1-F15 was pressed.
  const auto jter = kFKeyOrder.find(event->key_code);
  if (event->top_row_position == -1 && jter != kFKeyOrder.end()) {
    event->top_row_position = jter->second;
  }

  return event;
}

}  // namespace diagnostics
}  // namespace ash