// Copyright 2022 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 "ui/events/ash/keyboard_capability.h"
#include <fcntl.h>
#include <linux/input-event-codes.h>
#include <linux/input.h>
#include <cstring>
#include <functional>
#include <memory>
#include <optional>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/flat_set.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/system/sys_info.h"
#include "device/udev_linux/scoped_udev.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/ash/event_rewriter_ash.h"
#include "ui/events/ash/keyboard_info_metrics.h"
#include "ui/events/ash/keyboard_layout_util.h"
#include "ui/events/ash/modifier_split_dogfood_controller.h"
#include "ui/events/ash/mojom/meta_key.mojom-shared.h"
#include "ui/events/ash/mojom/modifier_key.mojom-shared.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/devices/input_device_event_observer.h"
#include "ui/events/devices/keyboard_device.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/dom_us_layout_data.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/ozone/evdev/event_device_info.h"
namespace ui {
namespace {
using KeyboardTopRowLayout = KeyboardCapability::KeyboardTopRowLayout;
using DeviceType = KeyboardCapability::DeviceType;
struct VendorProductId {
uint16_t vendor_id;
uint16_t product_id;
constexpr bool operator<(const VendorProductId& other) const {
return vendor_id == other.vendor_id ? product_id < other.product_id
: vendor_id < other.vendor_id;
}
};
// Represents scancode value seen in scan code mapping which denotes that the
// FKey is missing on the physical device.
const int kCustomAbsentScanCode = 0x00;
// Represents the "null" scancode used to represent the opting out of Meta +
// F-Key rewrites functionality.
const int kCustomNullScanCode = 0xC0000;
// Hotrod controller vendor/product ids.
const int kHotrodRemoteVendorId = 0x0471;
const int kHotrodRemoteProductId = 0x21cc;
constexpr auto kRightAltBlocklist =
base::MakeFixedFlatSet<std::string_view>({"eve", "nocturne", "atlas"});
constexpr char kLayoutProperty[] = "CROS_KEYBOARD_TOP_ROW_LAYOUT";
constexpr char kCustomTopRowLayoutAttribute[] = "function_row_physmap";
constexpr char kCustomTopRowLayoutProperty[] = "FUNCTION_ROW_PHYSMAP";
constexpr KeyboardCode kFunctionKeys[] = {
VKEY_F1, VKEY_F2, VKEY_F3, VKEY_F4, VKEY_F5,
VKEY_F6, VKEY_F7, VKEY_F8, VKEY_F9, VKEY_F10,
VKEY_F11, VKEY_F12, VKEY_F13, VKEY_F14, VKEY_F15,
};
constexpr KeyboardCode kMaxCustomTopRowLayoutFKeyCode = VKEY_F15;
constexpr size_t kNumCustomTopRowFKeys =
(kMaxCustomTopRowLayoutFKeyCode - VKEY_F1) + 1;
// Map used to convert VKEY -> TopRowActionKey and vice versa.
constexpr auto kVKeyToTopRowActionKeyMap =
base::MakeFixedFlatMap<ui::KeyboardCode, TopRowActionKey>({
{VKEY_BROWSER_BACK, TopRowActionKey::kBack},
{VKEY_BROWSER_FORWARD, TopRowActionKey::kForward},
{VKEY_BROWSER_REFRESH, TopRowActionKey::kRefresh},
{VKEY_ZOOM, TopRowActionKey::kFullscreen},
{VKEY_MEDIA_LAUNCH_APP1, TopRowActionKey::kOverview},
{VKEY_SNAPSHOT, TopRowActionKey::kScreenshot},
{VKEY_BRIGHTNESS_DOWN, TopRowActionKey::kScreenBrightnessDown},
{VKEY_BRIGHTNESS_UP, TopRowActionKey::kScreenBrightnessUp},
{VKEY_MICROPHONE_MUTE_TOGGLE, TopRowActionKey::kMicrophoneMute},
{VKEY_VOLUME_MUTE, TopRowActionKey::kVolumeMute},
{VKEY_VOLUME_DOWN, TopRowActionKey::kVolumeDown},
{VKEY_VOLUME_UP, TopRowActionKey::kVolumeUp},
{VKEY_KBD_BACKLIGHT_TOGGLE, TopRowActionKey::kKeyboardBacklightToggle},
{VKEY_KBD_BRIGHTNESS_DOWN, TopRowActionKey::kKeyboardBacklightDown},
{VKEY_KBD_BRIGHTNESS_UP, TopRowActionKey::kKeyboardBacklightUp},
{VKEY_MEDIA_NEXT_TRACK, TopRowActionKey::kNextTrack},
{VKEY_MEDIA_PREV_TRACK, TopRowActionKey::kPreviousTrack},
{VKEY_MEDIA_PLAY_PAUSE, TopRowActionKey::kPlayPause},
{VKEY_ALL_APPLICATIONS, TopRowActionKey::kAllApplications},
{VKEY_EMOJI_PICKER, TopRowActionKey::kEmojiPicker},
{VKEY_DICTATE, TopRowActionKey::kDictation},
{VKEY_PRIVACY_SCREEN_TOGGLE, TopRowActionKey::kPrivacyScreenToggle},
{VKEY_ACCESSIBILITY, TopRowActionKey::kAccessibility},
});
// Some ChromeOS compatible keyboards have a capslock key.
constexpr auto kChromeOSKeyboardsWithCapsLock =
base::MakeFixedFlatSet<VendorProductId>({
{0x046d, 0xb370} // Logitech Signature K650
});
std::optional<KeyboardDevice> FindKeyboardWithId(int device_id) {
const auto& keyboards =
DeviceDataManager::GetInstance()->GetKeyboardDevices();
for (const auto& keyboard : keyboards) {
if (keyboard.id == device_id) {
return keyboard;
}
}
return std::nullopt;
}
bool GetDeviceProperty(const base::FilePath& device_path,
const char* key,
std::string& value) {
device::ScopedUdevPtr udev(device::udev_new());
if (!udev.get()) {
return false;
}
device::ScopedUdevDevicePtr device(device::udev_device_new_from_syspath(
udev.get(), device_path.value().c_str()));
if (!device.get()) {
return false;
}
value = device::UdevDeviceGetPropertyValue(device.get(), key);
return true;
}
// Parses the custom top row layout string. The string contains a space
// separated list of scan codes in hex. eg "aa ab ac" for F1, F2, F3, etc.
std::vector<uint32_t> ParseCustomTopRowLayoutScancodes(
const std::string& layout) {
std::vector<uint32_t> scancode_vector;
const std::vector<std::string> scan_code_strings = base::SplitString(
layout, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (scan_code_strings.size() == 0 ||
scan_code_strings.size() > kNumCustomTopRowFKeys) {
return {};
}
for (const auto& scan_code_string : scan_code_strings) {
uint32_t scan_code = 0;
if (!base::HexStringToUInt(scan_code_string, &scan_code)) {
return {};
}
scancode_vector.push_back(scan_code);
}
return scancode_vector;
}
// Returns true if |value| is replaced with the specific device attribute value
// without getting an error. |device_path| should be obtained from the
// |KeyboardDevice.sys_path| field.
bool GetDeviceAttributeRecursive(const base::FilePath& device_path,
const char* key,
std::string& value) {
device::ScopedUdevPtr udev(device::udev_new());
if (!udev.get()) {
return false;
}
device::ScopedUdevDevicePtr device(device::udev_device_new_from_syspath(
udev.get(), device_path.value().c_str()));
if (!device.get()) {
return false;
}
value = device::UdevDeviceRecursiveGetSysattrValue(device.get(), key);
return true;
}
base::ScopedFD GetEventDeviceNameFd(const KeyboardDevice& keyboard) {
const char kDevNameProperty[] = "DEVNAME";
std::string dev_name;
if (!GetDeviceProperty(keyboard.sys_path, kDevNameProperty, dev_name) ||
dev_name.empty()) {
return base::ScopedFD();
}
base::ScopedFD fd(open(dev_name.c_str(), O_RDONLY));
if (fd.get() < 0) {
PLOG(ERROR) << "Cannot open " << dev_name.c_str();
return base::ScopedFD();
}
return fd;
}
std::optional<uint32_t> ConvertScanCodeToEvdevKey(const base::ScopedFD& fd,
uint32_t scancode) {
if (fd.get() < 0) {
return std::nullopt;
}
struct input_keymap_entry keymap_entry {
.flags = 0, .len = sizeof(scancode), .keycode = 0
};
memcpy(keymap_entry.scancode, &scancode, sizeof(scancode));
int ret = ioctl(fd.get(), EVIOCGKEYCODE_V2, &keymap_entry);
if (ret < 0) {
LOG(ERROR) << "Failed EVIOCGKEYCODE_V2 syscall";
return std::nullopt;
}
return keymap_entry.keycode;
}
bool GetCustomTopRowLayoutAttribute(const KeyboardDevice& keyboard,
std::string& out_prop) {
bool result = GetDeviceAttributeRecursive(
keyboard.sys_path, kCustomTopRowLayoutAttribute, out_prop);
if (result && out_prop.size() > 0) {
VLOG(1) << "Identified custom top row keyboard layout: sys_path="
<< keyboard.sys_path << " layout=" << out_prop;
return true;
}
return false;
}
bool GetCustomTopRowLayout(const KeyboardDevice& keyboard,
std::string& out_prop) {
if (GetCustomTopRowLayoutAttribute(keyboard, out_prop)) {
return true;
}
return GetDeviceProperty(keyboard.sys_path, kCustomTopRowLayoutProperty,
out_prop);
}
std::vector<uint32_t> GetTopRowScanCodeVector(const KeyboardDevice& keyboard) {
std::string layout;
if (!GetCustomTopRowLayout(keyboard, layout) || layout.empty()) {
return {};
}
return ParseCustomTopRowLayoutScancodes(layout);
}
bool GetTopRowLayoutProperty(const KeyboardDevice& keyboard_device,
std::string& out_prop) {
return GetDeviceProperty(keyboard_device.sys_path, kLayoutProperty, out_prop);
}
// Parses keyboard to row layout string. Returns true if data is valid.
bool ParseKeyboardTopRowLayout(const std::string& layout_string,
KeyboardTopRowLayout& out_layout) {
if (layout_string.empty()) {
out_layout = KeyboardTopRowLayout::kKbdTopRowLayoutDefault;
return true;
}
int layout_id;
if (!base::StringToInt(layout_string, &layout_id)) {
LOG(WARNING) << "Failed to parse layout " << kLayoutProperty << " value '"
<< layout_string << "'";
return false;
}
if (layout_id < static_cast<int>(KeyboardTopRowLayout::kKbdTopRowLayoutMin) ||
layout_id > static_cast<int>(KeyboardTopRowLayout::kKbdTopRowLayoutMax)) {
LOG(WARNING) << "Invalid " << kLayoutProperty << " '" << layout_string
<< "'";
return false;
}
out_layout = static_cast<KeyboardTopRowLayout>(layout_id);
return true;
}
// Determines the type of |keyboard_device| we are dealing with.
// |has_chromeos_top_row| argument indicates that the keyboard's top
// row has "action" keys (such as back, refresh, etc.) instead of the
// standard F1-F12 keys.
KeyboardCapability::DeviceType IdentifyKeyboardType(
const KeyboardDevice& keyboard_device,
bool has_chromeos_top_row,
bool has_null_top_row) {
if (keyboard_device.vendor_id == kHotrodRemoteVendorId &&
keyboard_device.product_id == kHotrodRemoteProductId) {
VLOG(1) << "Hotrod remote '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceHotrodRemote;
}
if (base::EqualsCaseInsensitiveASCII(keyboard_device.name,
"virtual core keyboard")) {
VLOG(1) << "Xorg virtual '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceVirtualCoreKeyboard;
}
if (keyboard_device.type == INPUT_DEVICE_INTERNAL) {
VLOG(1) << "Internal keyboard '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return ash::switches::IsRevenBranding()
? KeyboardCapability::DeviceType::kDeviceInternalRevenKeyboard
: KeyboardCapability::DeviceType::kDeviceInternalKeyboard;
}
if (has_chromeos_top_row) {
if (has_null_top_row) {
VLOG(1) << "External Null Top Row keyboard '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::
kDeviceExternalNullTopRowChromeOsKeyboard;
}
// If the device was tagged as having Chrome OS top row layout it must be a
// Chrome OS keyboard.
VLOG(1) << "External Chrome OS keyboard '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceExternalChromeOsKeyboard;
}
const std::vector<std::string> tokens =
base::SplitString(keyboard_device.name, " .", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
// Parse |device_name| to help classify it.
bool found_apple = false;
bool found_keyboard = false;
for (const auto& token : tokens) {
if (!found_apple && base::EqualsCaseInsensitiveASCII(token, "apple")) {
found_apple = true;
}
if (!found_keyboard &&
base::EqualsCaseInsensitiveASCII(token, "keyboard")) {
found_keyboard = true;
}
}
if (found_apple) {
// If the |device_name| contains the two words, "apple" and "keyboard",
// treat it as an Apple keyboard.
if (found_keyboard) {
VLOG(1) << "Apple keyboard '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceExternalAppleKeyboard;
} else {
VLOG(1) << "Apple device '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceExternalUnknown;
}
} else if (found_keyboard) {
VLOG(1) << "External keyboard '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard;
} else {
VLOG(1) << "External device '" << keyboard_device.name
<< "' connected: id=" << keyboard_device.id;
return KeyboardCapability::DeviceType::kDeviceExternalUnknown;
}
}
std::tuple<DeviceType, KeyboardTopRowLayout, std::vector<uint32_t>>
IdentifyKeyboardInfo(const KeyboardDevice& keyboard) {
std::string layout_string;
KeyboardTopRowLayout layout;
std::vector<uint32_t> top_row_scan_codes = GetTopRowScanCodeVector(keyboard);
bool null_top_row = false;
if (!top_row_scan_codes.empty()) {
layout = KeyboardTopRowLayout::kKbdTopRowLayoutCustom;
null_top_row =
base::ranges::all_of(top_row_scan_codes, [](const uint32_t scancode) {
return scancode == kCustomNullScanCode;
});
} else if (!GetTopRowLayoutProperty(keyboard, layout_string) ||
!ParseKeyboardTopRowLayout(layout_string, layout)) {
return {KeyboardCapability::DeviceType::kDeviceUnknown,
KeyboardTopRowLayout::kKbdTopRowLayoutDefault,
{}};
}
return {IdentifyKeyboardType(
keyboard, !top_row_scan_codes.empty() || !layout_string.empty(),
null_top_row),
layout, std::move(top_row_scan_codes)};
}
std::vector<TopRowActionKey> IdentifyCustomTopRowActionKeys(
const KeyboardCapability::ScanCodeToEvdevKeyConverter&
scan_code_to_evdev_key_converter,
const KeyboardDevice& keyboard,
const std::vector<uint32_t>& top_row_scan_codes) {
base::ScopedFD fd = GetEventDeviceNameFd(keyboard);
// TODO(dpad): Handle privacy screen in scan code mapping.
std::vector<TopRowActionKey> top_row_action_keys;
top_row_action_keys.reserve(top_row_scan_codes.size());
for (const auto& scancode : top_row_scan_codes) {
if (scancode == kCustomAbsentScanCode) {
top_row_action_keys.push_back(TopRowActionKey::kNone);
continue;
}
auto evdev_key_code = scan_code_to_evdev_key_converter.Run(fd, scancode);
if (!evdev_key_code) {
top_row_action_keys.push_back(TopRowActionKey::kUnknown);
continue;
}
const DomCode dom_code =
KeycodeConverter::EvdevCodeToDomCode(*evdev_key_code);
KeyboardCode action_vkey = DomCodeToUsLayoutKeyboardCode(dom_code);
if (action_vkey == VKEY_UNKNOWN) {
if (dom_code == DomCode::SHOW_ALL_WINDOWS) {
// Show all windows is through VKEY_MEDIA_LAUNCH_APP1.
action_vkey = VKEY_MEDIA_LAUNCH_APP1;
}
}
auto action_key = KeyboardCapability::ConvertToTopRowActionKey(action_vkey);
if (action_key) {
top_row_action_keys.push_back(*action_key);
} else {
top_row_action_keys.push_back(TopRowActionKey::kUnknown);
}
}
return top_row_action_keys;
}
std::vector<TopRowActionKey> IdentifyTopRowActionKeys(
const KeyboardCapability::ScanCodeToEvdevKeyConverter&
scan_code_to_evdev_key_converter,
const KeyboardDevice& keyboard,
DeviceType device_type,
KeyboardTopRowLayout layout,
const std::vector<uint32_t>& top_row_scan_codes) {
switch (layout) {
case KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayout1:
return std::vector<TopRowActionKey>(std::begin(kLayout1TopRowActionKeys),
std::end(kLayout1TopRowActionKeys));
case KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayout2:
return std::vector<TopRowActionKey>(std::begin(kLayout2TopRowActionKeys),
std::end(kLayout2TopRowActionKeys));
case KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayoutWilco:
case KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayoutDrallion:
return std::vector<TopRowActionKey>(
std::begin(kLayoutWilcoDrallionTopRowActionKeys),
std::end(kLayoutWilcoDrallionTopRowActionKeys));
case KeyboardCapability::KeyboardTopRowLayout::kKbdTopRowLayoutCustom:
return IdentifyCustomTopRowActionKeys(scan_code_to_evdev_key_converter,
keyboard, top_row_scan_codes);
}
}
bool IsInternalKeyboard(const ui::KeyboardDevice& keyboard) {
return keyboard.type == INPUT_DEVICE_INTERNAL;
}
bool HasExternalKeyboardConnected() {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (!keyboard.suspected_keyboard_imposter &&
!IsInternalKeyboard(keyboard)) {
return true;
}
}
return false;
}
} // namespace
KeyboardCapability::KeyboardCapability()
: scan_code_to_evdev_key_converter_(
base::BindRepeating(&ConvertScanCodeToEvdevKey)),
board_name_(base::ToLowerASCII(base::SysInfo::HardwareModelName())),
modifier_split_dogfood_controller_(
std::make_unique<ModifierSplitDogfoodController>()) {
DeviceDataManager::GetInstance()->AddObserver(this);
}
KeyboardCapability::KeyboardCapability(
ScanCodeToEvdevKeyConverter scan_code_to_evdev_key_converter)
: scan_code_to_evdev_key_converter_(
std::move(scan_code_to_evdev_key_converter)),
modifier_split_dogfood_controller_(
std::make_unique<ModifierSplitDogfoodController>()) {
DeviceDataManager::GetInstance()->AddObserver(this);
}
KeyboardCapability::~KeyboardCapability() {
DeviceDataManager::GetInstance()->RemoveObserver(this);
}
KeyboardCapability::KeyboardInfo::KeyboardInfo() = default;
KeyboardCapability::KeyboardInfo::KeyboardInfo(KeyboardInfo&&) = default;
KeyboardCapability::KeyboardInfo& KeyboardCapability::KeyboardInfo::operator=(
KeyboardInfo&&) = default;
KeyboardCapability::KeyboardInfo::~KeyboardInfo() = default;
// static
std::unique_ptr<KeyboardCapability>
KeyboardCapability::CreateStubKeyboardCapability() {
return std::make_unique<KeyboardCapability>();
}
// static
std::unique_ptr<EventDeviceInfo>
KeyboardCapability::CreateEventDeviceInfoFromInputDevice(
const KeyboardDevice& keyboard) {
base::ScopedFD fd = GetEventDeviceNameFd(keyboard);
if (fd.get() < 0) {
return nullptr;
}
std::unique_ptr<EventDeviceInfo> event_device_info =
std::make_unique<EventDeviceInfo>();
if (!event_device_info->Initialize(fd.get(), keyboard.sys_path)) {
LOG(ERROR) << "Failed to get device information for "
<< keyboard.sys_path.value();
return nullptr;
}
return event_device_info;
}
// static
std::optional<TopRowActionKey> KeyboardCapability::ConvertToTopRowActionKey(
ui::KeyboardCode key_code) {
const auto action_key = kVKeyToTopRowActionKeyMap.find(key_code);
return (action_key != kVKeyToTopRowActionKeyMap.end())
? std::make_optional<TopRowActionKey>(action_key->second)
: std::nullopt;
}
// static
std::optional<KeyboardCode> KeyboardCapability::ConvertToKeyboardCode(
TopRowActionKey action_key) {
for (const auto& [key_code, mapped_action_key] : kVKeyToTopRowActionKeyMap) {
if (mapped_action_key == action_key) {
return key_code;
}
}
return std::nullopt;
}
// static
bool KeyboardCapability::IsSixPackKey(const KeyboardCode& key_code) {
return base::Contains(kSixPackKeyToSearchSystemKeyMap, key_code);
}
std::optional<KeyboardCode> KeyboardCapability::GetMappedFKeyIfExists(
const KeyboardCode& key_code,
const KeyboardDevice& keyboard) const {
// TODO(zhangwenyu): Cache the layout for currently connected keyboards and
// observe the keyboard changes.
KeyboardTopRowLayout layout = GetTopRowLayout(keyboard);
switch (layout) {
case KeyboardTopRowLayout::kKbdTopRowLayout1:
if (kLayout1TopRowKeyToFKeyMap.contains(key_code)) {
return kLayout1TopRowKeyToFKeyMap.at(key_code);
}
break;
case KeyboardTopRowLayout::kKbdTopRowLayout2:
if (kLayout2TopRowKeyToFKeyMap.contains(key_code)) {
return kLayout2TopRowKeyToFKeyMap.at(key_code);
}
break;
case KeyboardTopRowLayout::kKbdTopRowLayoutWilco:
case KeyboardTopRowLayout::kKbdTopRowLayoutDrallion:
if (kLayoutWilcoDrallionTopRowKeyToFKeyMap.contains(key_code)) {
return kLayoutWilcoDrallionTopRowKeyToFKeyMap.at(key_code);
}
break;
case KeyboardTopRowLayout::kKbdTopRowLayoutCustom:
// TODO(zhangwenyu): Handle custom vivaldi layout.
return std::nullopt;
}
return std::nullopt;
}
std::optional<KeyboardCode> KeyboardCapability::GetCorrespondingFunctionKey(
const KeyboardDevice& keyboard,
TopRowActionKey action_key) const {
auto* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return std::nullopt;
}
auto iter =
base::ranges::find(keyboard_info->top_row_action_keys, action_key);
if (iter == keyboard_info->top_row_action_keys.end()) {
return std::nullopt;
}
return kFunctionKeys[std::distance(keyboard_info->top_row_action_keys.begin(),
iter)];
}
std::optional<TopRowActionKey>
KeyboardCapability::GetCorrespondingActionKeyForFKey(
const KeyboardDevice& keyboard,
KeyboardCode key_code) const {
auto* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return std::nullopt;
}
if (key_code > VKEY_F24 || key_code < VKEY_F1) {
return std::nullopt;
}
const size_t index = key_code - VKEY_F1;
if (keyboard_info->top_row_action_keys.size() <= index) {
return std::nullopt;
}
return keyboard_info->top_row_action_keys[index];
}
// TODO(dpad): Remove once modifier split launches.
bool KeyboardCapability::HasLauncherButton(
const KeyboardDevice& keyboard) const {
// TODO(dpad): This is not entirely correct. Some devices which have custom
// top rows have a search icon on their keyboard (ie jinlon).
// In general, only chromebooks with layout1 top rows use the search icon.
auto top_row_layout = GetTopRowLayout(keyboard);
switch (top_row_layout) {
case KeyboardTopRowLayout::kKbdTopRowLayout1:
// Some external keyboards report the wrong layout type.
return !IsInternalKeyboard(keyboard);
case KeyboardTopRowLayout::kKbdTopRowLayout2:
case KeyboardTopRowLayout::kKbdTopRowLayoutWilco:
case KeyboardTopRowLayout::kKbdTopRowLayoutDrallion:
case KeyboardTopRowLayout::kKbdTopRowLayoutCustom:
return true;
}
}
// TODO(dpad): Remove once modifier split launches.
bool KeyboardCapability::HasLauncherButtonOnAnyKeyboard() const {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (HasLauncherButton(keyboard)) {
return true;
}
}
return false;
}
// static
bool KeyboardCapability::IsTopRowKey(const KeyboardCode& key_code) {
// A set that includes all top row keys from different keyboards.
const auto action = kVKeyToTopRowActionKeyMap.find(key_code);
return action != kVKeyToTopRowActionKeyMap.end();
}
// static
bool KeyboardCapability::HasSixPackKey(const KeyboardDevice& keyboard) {
// If the keyboard is an internal keyboard, return false. Otherwise, return
// true. This is correct for most of the keyboards. Edge cases will be handled
// later.
// TODO(zhangwenyu): handle edge cases when this logic doesn't apply.
return keyboard.type != InputDeviceType::INPUT_DEVICE_INTERNAL;
}
// static
bool KeyboardCapability::HasSixPackOnAnyKeyboard() {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (ui::KeyboardCapability::HasSixPackKey(keyboard)) {
return true;
}
}
return false;
}
// static
bool KeyboardCapability::IsFunctionKey(ui::KeyboardCode code) {
return ui::KeyboardCode::VKEY_F1 <= code &&
code <= ui::KeyboardCode::VKEY_F24;
}
// static
bool KeyboardCapability::IsF11OrF12(ui::KeyboardCode code) {
return code == ui::KeyboardCode::VKEY_F11 ||
code == ui::KeyboardCode::VKEY_F12;
}
std::vector<mojom::ModifierKey> KeyboardCapability::GetModifierKeys(
const KeyboardDevice& keyboard) const {
// This set of modifier keys is available on every keyboard.
std::vector<mojom::ModifierKey> modifier_keys = {
mojom::ModifierKey::kBackspace, mojom::ModifierKey::kControl,
mojom::ModifierKey::kMeta, mojom::ModifierKey::kEscape,
mojom::ModifierKey::kAlt,
};
if (HasCapsLockKey(keyboard)) {
modifier_keys.push_back(mojom::ModifierKey::kCapsLock);
}
if (HasAssistantKey(keyboard)) {
modifier_keys.push_back(mojom::ModifierKey::kAssistant);
}
if (HasFunctionKey(keyboard)) {
modifier_keys.push_back(mojom::ModifierKey::kFunction);
}
if (HasRightAltKey(keyboard)) {
modifier_keys.push_back(mojom::ModifierKey::kRightAlt);
}
return modifier_keys;
}
std::vector<mojom::ModifierKey> KeyboardCapability::GetModifierKeys(
int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return {};
}
return GetModifierKeys(*keyboard);
}
DeviceType KeyboardCapability::GetDeviceType(
const KeyboardDevice& keyboard) const {
const auto* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return DeviceType::kDeviceUnknown;
}
return keyboard_info->device_type;
}
DeviceType KeyboardCapability::GetDeviceType(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return DeviceType::kDeviceUnknown;
}
return GetDeviceType(*keyboard);
}
KeyboardTopRowLayout KeyboardCapability::GetTopRowLayout(
const KeyboardDevice& keyboard) const {
const auto* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return KeyboardTopRowLayout::kKbdTopRowLayoutDefault;
}
return keyboard_info->top_row_layout;
}
KeyboardTopRowLayout KeyboardCapability::GetTopRowLayout(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return KeyboardTopRowLayout::kKbdTopRowLayoutDefault;
}
return GetTopRowLayout(*keyboard);
}
void KeyboardCapability::SetKeyboardInfoForTesting(
const KeyboardDevice& keyboard,
KeyboardInfo keyboard_info) {
keyboard_info_map_.insert_or_assign(keyboard.id, std::move(keyboard_info));
}
void KeyboardCapability::DisableKeyboardInfoTrimmingForTesting() {
should_disable_trimming_ = true;
}
const KeyboardCapability::KeyboardInfo* KeyboardCapability::GetKeyboardInfo(
const KeyboardDevice& keyboard) const {
auto iter = keyboard_info_map_.find(keyboard.id);
if (iter != keyboard_info_map_.end()) {
return &iter->second;
}
// Insert new keyboard info into the map.
auto& keyboard_info = keyboard_info_map_[keyboard.id];
std::tie(keyboard_info.device_type, keyboard_info.top_row_layout,
keyboard_info.top_row_scan_codes) = IdentifyKeyboardInfo(keyboard);
keyboard_info.top_row_action_keys = IdentifyTopRowActionKeys(
scan_code_to_evdev_key_converter_, keyboard, keyboard_info.device_type,
keyboard_info.top_row_layout, keyboard_info.top_row_scan_codes);
// If we are unable to identify the device, erase the entry from the map.
if (keyboard_info.device_type == DeviceType::kDeviceUnknown) {
keyboard_info_map_.erase(keyboard.id);
return nullptr;
}
// This metrics recording will happen once per keyboard per connection, since
// GetKeyboardInfo is cached and isn't recomputed unless the keyboard
// disconnects and reconnects.
RecordKeyboardInfoMetrics(keyboard_info,
/*has_assistant_key=*/HasAssistantKey(keyboard),
/*has_right_alt_key=*/HasRightAltKey(keyboard));
return &keyboard_info;
}
const std::vector<uint32_t>* KeyboardCapability::GetTopRowScanCodes(
const KeyboardDevice& keyboard) const {
const KeyboardInfo* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return nullptr;
}
return &keyboard_info->top_row_scan_codes;
}
const std::vector<uint32_t>* KeyboardCapability::GetTopRowScanCodes(
int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard.has_value()) {
return nullptr;
}
return GetTopRowScanCodes(*keyboard);
}
bool KeyboardCapability::HasGlobeKey(const KeyboardDevice& keyboard) const {
const KeyboardInfo* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return false;
}
// TODO(jimmyxgong): VKEY_MODECHANGE (globe key) for now we should assume
// can be available for external keyboards or Wilco/Drallion device. Will
// need a better way to determine if the key is available in non
// Wilco/Drallion keyboards.
return !IsInternalKeyboard(keyboard) ||
keyboard_info->top_row_layout ==
KeyboardTopRowLayout::kKbdTopRowLayoutDrallion ||
keyboard_info->top_row_layout ==
KeyboardTopRowLayout::kKbdTopRowLayoutWilco;
}
bool KeyboardCapability::HasGlobeKeyOnAnyKeyboard() const {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (HasGlobeKey(keyboard)) {
return true;
}
}
return false;
}
bool KeyboardCapability::HasCalculatorKey(
const KeyboardDevice& keyboard) const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return !IsInternalKeyboard(keyboard);
}
bool KeyboardCapability::HasCalculatorKeyOnAnyKeyboard() const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return HasExternalKeyboardConnected();
}
bool KeyboardCapability::HasBrowserSearchKey(
const KeyboardDevice& keyboard) const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return !IsInternalKeyboard(keyboard);
}
bool KeyboardCapability::HasBrowserSearchKeyOnAnyKeyboard() const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return HasExternalKeyboardConnected();
}
bool KeyboardCapability::HasHelpKey(const KeyboardDevice& keyboard) const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return !IsInternalKeyboard(keyboard);
}
bool KeyboardCapability::HasHelpKeyOnAnyKeyboard() const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return HasExternalKeyboardConnected();
}
bool KeyboardCapability::HasSettingsKey(const KeyboardDevice& keyboard) const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return !IsInternalKeyboard(keyboard);
}
bool KeyboardCapability::HasSettingsKeyOnAnyKeyboard() const {
// TODO(dpad): Many external keyboards do not have this key, but currently we
// do not have a good way to detect these situations.
return HasExternalKeyboardConnected();
}
bool KeyboardCapability::HasMediaKeys(const KeyboardDevice& keyboard) const {
// TODO(dpad): Many external keyboards do not have these keys, but currently
// we do not have a good way to detect these situations.
return !IsInternalKeyboard(keyboard);
}
bool KeyboardCapability::HasMediaKeysOnAnyKeyboard() const {
// TODO(dpad): Many external keyboards do not have these keys, but currently
// we do not have a good way to detect these situations.
return HasExternalKeyboardConnected();
}
const std::vector<TopRowActionKey>* KeyboardCapability::GetTopRowActionKeys(
const KeyboardDevice& keyboard) const {
const auto* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return nullptr;
}
return &keyboard_info->top_row_action_keys;
}
const std::vector<TopRowActionKey>* KeyboardCapability::GetTopRowActionKeys(
int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return nullptr;
}
return GetTopRowActionKeys(*keyboard);
}
bool KeyboardCapability::HasAssistantKey(const KeyboardDevice& keyboard) const {
if (HasRightAltKey(keyboard)) {
return false;
}
if (ash::features::IsSplitKeyboardRefactorEnabled()) {
return false;
}
// Some external keyboards falsely claim to have assistant keys. However, this
// can be trusted for internal + ChromeOS external keyboards.
return keyboard.has_assistant_key && IsChromeOSKeyboard(keyboard.id);
}
bool KeyboardCapability::HasAssistantKey(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return false;
}
return HasAssistantKey(*keyboard);
}
bool KeyboardCapability::HasAssistantKeyOnAnyKeyboard() const {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (HasAssistantKey(keyboard)) {
return true;
}
}
return false;
}
bool KeyboardCapability::HasCapsLockKey(const KeyboardDevice& keyboard) const {
return !IsChromeOSKeyboard(keyboard.id) ||
kChromeOSKeyboardsWithCapsLock.contains(
{keyboard.vendor_id, keyboard.product_id});
}
bool KeyboardCapability::HasFunctionKey(const KeyboardDevice& keyboard) const {
if (!modifier_split_dogfood_controller_->IsEnabled()) {
return false;
}
if (ash::features::IsSplitKeyboardRefactorEnabled()) {
return true;
}
return ash::features::IsModifierSplitEnabled() &&
keyboard.type == InputDeviceType::INPUT_DEVICE_INTERNAL &&
keyboard.has_function_key;
}
bool KeyboardCapability::HasFunctionKey(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return false;
}
return HasFunctionKey(*keyboard);
}
bool KeyboardCapability::HasFunctionKeyOnAnyKeyboard() const {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (HasFunctionKey(keyboard)) {
return true;
}
}
return false;
}
bool KeyboardCapability::HasRightAltKey(const KeyboardDevice& keyboard) const {
if (!modifier_split_dogfood_controller_->IsEnabled()) {
return false;
}
if (ash::features::IsSplitKeyboardRefactorEnabled()) {
return true;
}
if (kRightAltBlocklist.contains(board_name_)) {
return false;
}
return keyboard.type == InputDeviceType::INPUT_DEVICE_INTERNAL &&
keyboard.has_assistant_key;
}
bool KeyboardCapability::HasRightAltKey(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return false;
}
return HasRightAltKey(*keyboard);
}
bool KeyboardCapability::HasRightAltKeyForOobe(
const KeyboardDevice& keyboard) const {
if (modifier_split_dogfood_controller_->IsEnabled()) {
return false;
}
if (ash::features::IsSplitKeyboardRefactorEnabled()) {
return true;
}
if (kRightAltBlocklist.contains(board_name_)) {
return false;
}
return keyboard.type == InputDeviceType::INPUT_DEVICE_INTERNAL &&
keyboard.has_assistant_key;
}
bool KeyboardCapability::HasRightAltKeyForOobe(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return false;
}
return HasRightAltKeyForOobe(*keyboard);
}
bool KeyboardCapability::IsSplitModifierKeyboardForOverride(
const KeyboardDevice& keyboard) const {
if (kRightAltBlocklist.contains(board_name_)) {
return false;
}
return ash::features::IsModifierSplitEnabled() &&
keyboard.type == InputDeviceType::INPUT_DEVICE_INTERNAL &&
keyboard.has_function_key && keyboard.has_assistant_key;
}
ui::mojom::MetaKey KeyboardCapability::GetMetaKey(
const KeyboardDevice& keyboard) const {
const auto device_type = GetDeviceType(keyboard);
switch (device_type) {
case ui::KeyboardCapability::DeviceType::kDeviceExternalAppleKeyboard:
return mojom::MetaKey::kCommand;
case ui::KeyboardCapability::DeviceType::kDeviceUnknown:
case ui::KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
case ui::KeyboardCapability::DeviceType::kDeviceExternalUnknown:
case ui::KeyboardCapability::DeviceType::kDeviceInternalRevenKeyboard:
case ui::KeyboardCapability::DeviceType::
kDeviceExternalNullTopRowChromeOsKeyboard:
return mojom::MetaKey::kExternalMeta;
case ui::KeyboardCapability::DeviceType::kDeviceInternalKeyboard:
case ui::KeyboardCapability::DeviceType::kDeviceExternalChromeOsKeyboard:
case ui::KeyboardCapability::DeviceType::kDeviceHotrodRemote:
case ui::KeyboardCapability::DeviceType::kDeviceVirtualCoreKeyboard:
break;
};
if (IsSplitModifierKeyboard(keyboard)) {
return mojom::MetaKey::kLauncherRefresh;
}
// TODO(dpad): This is not entirely correct. Some devices which have custom
// top rows have a search icon on their keyboard (ie jinlon).
// In general, only chromebooks with layout1 top rows use the search icon.
auto top_row_layout = GetTopRowLayout(keyboard);
switch (top_row_layout) {
case KeyboardTopRowLayout::kKbdTopRowLayout1:
return IsInternalKeyboard(keyboard) ? mojom::MetaKey::kSearch
: mojom::MetaKey::kLauncher;
case KeyboardTopRowLayout::kKbdTopRowLayout2:
case KeyboardTopRowLayout::kKbdTopRowLayoutWilco:
case KeyboardTopRowLayout::kKbdTopRowLayoutDrallion:
case KeyboardTopRowLayout::kKbdTopRowLayoutCustom:
return mojom::MetaKey::kLauncher;
}
}
ui::mojom::MetaKey KeyboardCapability::GetMetaKey(int device_id) const {
auto keyboard = FindKeyboardWithId(device_id);
if (!keyboard) {
return mojom::MetaKey::kLauncher;
}
return GetMetaKey(*keyboard);
}
ui::mojom::MetaKey KeyboardCapability::GetMetaKeyToDisplay() const {
ui::mojom::MetaKey current_best = ui::mojom::MetaKey::kExternalMeta;
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
const ui::mojom::MetaKey meta_key = GetMetaKey(keyboard);
// Ordered in priority order. If a keyboard is connected with a refreshed
// launcher key, it should have ultimate priority.
switch (meta_key) {
case mojom::MetaKey::kLauncherRefresh:
current_best = mojom::MetaKey::kLauncherRefresh;
break;
case mojom::MetaKey::kLauncher:
if (current_best != mojom::MetaKey::kLauncherRefresh) {
current_best = mojom::MetaKey::kLauncher;
}
break;
case mojom::MetaKey::kSearch:
if (current_best == mojom::MetaKey::kExternalMeta) {
current_best = mojom::MetaKey::kSearch;
}
break;
case mojom::MetaKey::kExternalMeta:
case mojom::MetaKey::kCommand:
break;
}
}
if (current_best != mojom::MetaKey::kExternalMeta &&
current_best != mojom::MetaKey::kCommand) {
return current_best;
}
// Override meta key icon for external keyboards to be the highest priority
// icon.
if (modifier_split_dogfood_controller_->IsEnabled()) {
return mojom::MetaKey::kLauncherRefresh;
} else {
return mojom::MetaKey::kLauncher;
}
}
bool KeyboardCapability::UseRefreshedIcons() const {
return GetMetaKeyToDisplay() == mojom::MetaKey::kLauncherRefresh;
}
void KeyboardCapability::OnDeviceListsComplete() {
TrimKeyboardInfoMap();
}
void KeyboardCapability::OnInputDeviceConfigurationChanged(
uint8_t input_device_types) {
if (input_device_types & InputDeviceEventObserver::kKeyboard) {
TrimKeyboardInfoMap();
}
}
void KeyboardCapability::TrimKeyboardInfoMap() {
// When `should_disable_trimming_` is true, skip removal of removed devices
// from our cache of `KeyboardInfo`.
if (should_disable_trimming_) {
return;
}
auto sorted_keyboards =
DeviceDataManager::GetInstance()->GetKeyboardDevices();
base::ranges::sort(sorted_keyboards, [](const ui::KeyboardDevice& device1,
const ui::KeyboardDevice& device2) {
return device1.id < device2.id;
});
// Generate a vector with only the device ids from the
// `keyboard_info_map_` map. Guaranteed to be sorted as flat_map is always
// in sorted order by key.
std::vector<int> cached_keyboard_info_ids;
cached_keyboard_info_ids.reserve(keyboard_info_map_.size());
base::ranges::transform(keyboard_info_map_,
std::back_inserter(cached_keyboard_info_ids),
[](const auto& pair) { return pair.first; });
DCHECK(base::ranges::is_sorted(cached_keyboard_info_ids));
// Compares the `cached_keyboard_info_ids` to the id field of
// `sorted_keyboards`. Ids that are in `cached_keyboard_info_ids` but not
// in `sorted_keyboards` are inserted into `keyboard_ids_to_remove`.
// `sorted_keyboards` and `cached_keyboard_info_ids` must be sorted.
std::vector<int> keyboard_ids_to_remove;
base::ranges::set_difference(
cached_keyboard_info_ids, sorted_keyboards,
std::back_inserter(keyboard_ids_to_remove),
/*Comp=*/base::ranges::less(),
/*Proj1=*/std::identity(),
/*Proj2=*/[](const KeyboardDevice& device) { return device.id; });
for (const auto& id : keyboard_ids_to_remove) {
keyboard_info_map_.erase(id);
}
}
bool KeyboardCapability::HasKeyEvent(const KeyboardCode& key_code,
const KeyboardDevice& keyboard) const {
// Handle top row keys.
std::optional<TopRowActionKey> top_row_action_key =
ConvertToTopRowActionKey(key_code);
if (top_row_action_key.has_value()) {
return HasTopRowActionKey(keyboard, top_row_action_key.value());
}
// Handle six pack keys.
if (IsSixPackKey(key_code)) {
return HasSixPackKey(keyboard);
}
// Handle assistant key.
if (key_code == KeyboardCode::VKEY_ASSISTANT) {
return HasAssistantKey(keyboard);
}
// TODO(zhangwenyu): check other specific keys, e.g. assistant key.
return true;
}
bool KeyboardCapability::HasKeyEventOnAnyKeyboard(
const KeyboardCode& key_code) const {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (HasKeyEvent(key_code, keyboard)) {
return true;
}
}
return false;
}
bool KeyboardCapability::HasTopRowActionKey(const KeyboardDevice& keyboard,
TopRowActionKey action_key) const {
const auto* keyboard_info = GetKeyboardInfo(keyboard);
if (!keyboard_info) {
return base::Contains(kLayout1TopRowActionKeys, action_key);
}
return base::Contains(keyboard_info->top_row_action_keys, action_key);
}
bool KeyboardCapability::HasTopRowActionKeyOnAnyKeyboard(
TopRowActionKey action_key) const {
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (HasTopRowActionKey(keyboard, action_key)) {
return true;
}
}
return false;
}
bool KeyboardCapability::IsSplitModifierKeyboard(
const KeyboardDevice& keyboard) const {
return HasRightAltKey(keyboard) && HasFunctionKey(keyboard);
}
bool KeyboardCapability::IsChromeOSKeyboard(int device_id) const {
const auto device_type = GetDeviceType(device_id);
return device_type == DeviceType::kDeviceInternalKeyboard ||
device_type == DeviceType::kDeviceExternalChromeOsKeyboard;
}
void KeyboardCapability::SetBoardNameForTesting(const std::string& board_name) {
board_name_ = board_name;
}
void KeyboardCapability::ForceEnableFeature() {
modifier_split_dogfood_controller_->ForceEnableFeature();
}
void KeyboardCapability::ResetModifierSplitDogfoodControllerForTesting() {
modifier_split_dogfood_controller_ =
std::make_unique<ModifierSplitDogfoodController>(); // IN-TEST
}
} // namespace ui