// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/input_device_settings/input_device_settings_utils.h"
#include <string_view>
#include "ash/public/cpp/accelerators_util.h"
#include "ash/public/mojom/input_device_settings.mojom-shared.h"
#include "ash/public/mojom/input_device_settings.mojom.h"
#include "ash/shell.h"
#include "ash/system/input_device_settings/input_device_settings_pref_names.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/flat_set.h"
#include "base/export_template.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/known_user.h"
#include "ui/events/ash/keyboard_capability.h"
#include "ui/events/ash/mojom/extended_fkeys_modifier.mojom-shared.h"
#include "ui/events/ash/mojom/meta_key.mojom-shared.h"
#include "ui/events/ash/mojom/modifier_key.mojom.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/ozone/evdev/keyboard_mouse_combo_device_metrics.h"
#include "ui/events/types/event_type.h"
namespace ash {
namespace {
const char kRedactedButtonName[] = "REDACTED";
std::string HexEncode(uint16_t v) {
// Load the bytes into the bytes array in reverse order as hex number should
// be read from left to right.
uint8_t bytes[sizeof(uint16_t)];
bytes[1] = v & 0xFF;
bytes[0] = v >> 8;
return base::ToLowerASCII(base::HexEncode(bytes));
}
bool ExistingSettingsHasValue(std::string_view setting_key,
const base::Value::Dict* existing_settings_dict) {
if (!existing_settings_dict) {
return false;
}
return existing_settings_dict->Find(setting_key) != nullptr;
}
bool IsAlphaKeyboardCode(ui::KeyboardCode key_code) {
return GetKeyInputTypeFromKeyEvent(ui::KeyEvent(
ui::EventType::kKeyPressed, key_code, ui::DomCode::NONE,
ui::EF_NONE)) == AcceleratorKeyInputType::kAlpha;
}
bool IsNumberKeyboardCode(ui::KeyboardCode key_code) {
return GetKeyInputTypeFromKeyEvent(ui::KeyEvent(
ui::EventType::kKeyPressed, key_code, ui::DomCode::NONE,
ui::EF_NONE)) == AcceleratorKeyInputType::kDigit;
}
// Verify if the customization restriction blocks the button remapping.
// Block button remapping in the following cases:
// 1. Customization restriction is kAllowCustomizations.
// 2. Customization restriction is kDisableKeyEventRewrites and button is not
// a keyboard key.
// 3. Customization restriction is kAllowAlphabetKeyEventRewrites and button
// is a mouse button or alphabet/punctuation keyboard key.
// 4. Customization restriction is kAllowAlphabetOrNumberKeyEventRewrites and
// button is a mouse button or alphabet, punctuation, or number keyboard key.
// In other cases, block button remapping.
bool RestrictionBlocksRemapping(
const mojom::ButtonRemapping& remapping,
mojom::CustomizationRestriction customization_restriction) {
switch (customization_restriction) {
case mojom::CustomizationRestriction::kAllowCustomizations:
return false;
case mojom::CustomizationRestriction::kDisallowCustomizations:
return true;
case mojom::CustomizationRestriction::kDisableKeyEventRewrites:
// No keyboard keys are allowed to be remapped.
if (remapping.button->is_vkey()) {
return true;
}
// No horizontal scroll events are allowed to be remapped.
if (remapping.button->is_customizable_button()) {
const auto& customizable_button =
remapping.button->get_customizable_button();
return customizable_button == mojom::CustomizableButton::kScrollLeft ||
customizable_button == mojom::CustomizableButton::kScrollRight;
}
return false;
case mojom::CustomizationRestriction::kAllowAlphabetKeyEventRewrites:
if (remapping.button->is_vkey() &&
!IsAlphaKeyboardCode(remapping.button->get_vkey())) {
return true;
}
return false;
case mojom::CustomizationRestriction::
kAllowAlphabetOrNumberKeyEventRewrites:
if (remapping.button->is_vkey() &&
!IsAlphaKeyboardCode(remapping.button->get_vkey()) &&
!IsNumberKeyboardCode(remapping.button->get_vkey())) {
return true;
}
return false;
case mojom::CustomizationRestriction::kAllowHorizontalScrollWheelRewrites:
return remapping.button->is_vkey();
case mojom::CustomizationRestriction::kAllowTabEventRewrites:
if (remapping.button->is_customizable_button()) {
return false;
}
return remapping.button->get_vkey() != ui::VKEY_TAB;
case mojom::CustomizationRestriction::kAllowFKeyRewrites:
if (remapping.button->is_customizable_button()) {
return false;
}
return !(remapping.button->get_vkey() >= ui::VKEY_F1 &&
remapping.button->get_vkey() <= ui::VKEY_F15);
}
}
// "0111:185a" is from the list of supported device keys listed here:
// google3/chrome/chromeos/apps_foundation/almanac/fondue/boq/
// peripherals_service/manual_config/companion_apps.h
constexpr char kWelcomeExperienceTestDeviceKey[] = "0111:185a";
} // namespace
bool VendorProductId::operator==(const VendorProductId& other) const {
return vendor_id == other.vendor_id && product_id == other.product_id;
}
// `kIsoLevel5ShiftMod3` is not a valid modifier value.
bool IsValidModifier(int val) {
return val >= static_cast<int>(ui::mojom::ModifierKey::kMinValue) &&
val <= static_cast<int>(ui::mojom::ModifierKey::kMaxValue) &&
val != static_cast<int>(ui::mojom::ModifierKey::kIsoLevel5ShiftMod3);
}
std::string BuildDeviceKey(const ui::InputDevice& device) {
return BuildDeviceKey(device.vendor_id, device.product_id);
}
std::string BuildDeviceKey(uint16_t vendor_id, uint16_t product_id) {
return base::StrCat({HexEncode(vendor_id), ":", HexEncode(product_id)});
}
template <typename T>
bool ShouldPersistSetting(std::string_view setting_key,
T new_value,
T default_value,
bool force_persistence,
const base::Value::Dict* existing_settings_dict) {
return ExistingSettingsHasValue(setting_key, existing_settings_dict) ||
new_value != default_value || force_persistence;
}
bool ShouldPersistSetting(const mojom::InputDeviceSettingsPolicyPtr& policy,
std::string_view setting_key,
bool new_value,
bool default_value,
bool force_persistence,
const base::Value::Dict* existing_settings_dict) {
if (force_persistence) {
return true;
}
if (!policy) {
return ShouldPersistSetting(setting_key, new_value, default_value,
force_persistence, existing_settings_dict);
}
switch (policy->policy_status) {
case mojom::PolicyStatus::kRecommended:
return ExistingSettingsHasValue(setting_key, existing_settings_dict) ||
new_value != policy->value;
case mojom::PolicyStatus::kManaged:
return false;
}
}
bool ShouldPersistFkeySetting(
const mojom::InputDeviceSettingsFkeyPolicyPtr& policy,
std::string_view setting_key,
std::optional<ui::mojom::ExtendedFkeysModifier> new_value,
ui::mojom::ExtendedFkeysModifier default_value,
const base::Value::Dict* existing_settings_dict) {
if (!new_value.has_value()) {
return false;
}
if (!policy) {
return ShouldPersistSetting(setting_key, new_value.value(), default_value,
/*force_persistence=*/{},
existing_settings_dict);
}
switch (policy->policy_status) {
case mojom::PolicyStatus::kRecommended:
return ExistingSettingsHasValue(setting_key, existing_settings_dict) ||
new_value.value() != policy->value;
case mojom::PolicyStatus::kManaged:
return false;
}
}
template EXPORT_TEMPLATE_DEFINE(ASH_EXPORT) bool ShouldPersistSetting(
std::string_view setting_key,
bool new_value,
bool default_value,
bool force_persistence,
const base::Value::Dict* existing_settings_dict);
template EXPORT_TEMPLATE_DEFINE(ASH_EXPORT) bool ShouldPersistSetting(
std::string_view setting_key,
int value,
int default_value,
bool force_persistence,
const base::Value::Dict* existing_settings_dict);
const base::Value::Dict* GetLoginScreenSettingsDict(
PrefService* local_state,
AccountId account_id,
const std::string& pref_name) {
const auto* dict_value =
user_manager::KnownUser(local_state).FindPath(account_id, pref_name);
if (!dict_value || !dict_value->is_dict()) {
return nullptr;
}
return &dict_value->GetDict();
}
const base::Value::List* GetLoginScreenButtonRemappingList(
PrefService* local_state,
AccountId account_id,
const std::string& pref_name) {
const auto* list_value =
user_manager::KnownUser(local_state).FindPath(account_id, pref_name);
if (!list_value || !list_value->is_list()) {
return nullptr;
}
return &list_value->GetList();
}
base::Value::Dict ConvertButtonRemappingToDict(
const mojom::ButtonRemapping& remapping,
mojom::CustomizationRestriction customization_restriction,
bool redact_button_names) {
base::Value::Dict dict;
if (RestrictionBlocksRemapping(remapping, customization_restriction)) {
return dict;
}
dict.Set(prefs::kButtonRemappingName,
redact_button_names ? kRedactedButtonName : remapping.name);
if (remapping.button->is_customizable_button()) {
dict.Set(prefs::kButtonRemappingCustomizableButton,
static_cast<int>(remapping.button->get_customizable_button()));
} else if (remapping.button->is_vkey()) {
dict.Set(prefs::kButtonRemappingKeyboardCode,
static_cast<int>(remapping.button->get_vkey()));
}
if (!remapping.remapping_action) {
return dict;
}
if (remapping.remapping_action->is_key_event()) {
base::Value::Dict key_event;
key_event.Set(prefs::kButtonRemappingDomCode,
static_cast<int>(
remapping.remapping_action->get_key_event()->dom_code));
key_event.Set(
prefs::kButtonRemappingDomKey,
static_cast<int>(remapping.remapping_action->get_key_event()->dom_key));
key_event.Set(prefs::kButtonRemappingModifiers,
static_cast<int>(
remapping.remapping_action->get_key_event()->modifiers));
key_event.Set(
prefs::kButtonRemappingKeyboardCode,
static_cast<int>(remapping.remapping_action->get_key_event()->vkey));
dict.Set(prefs::kButtonRemappingKeyEvent, std::move(key_event));
} else if (remapping.remapping_action->is_accelerator_action()) {
dict.Set(
prefs::kButtonRemappingAcceleratorAction,
static_cast<int>(remapping.remapping_action->get_accelerator_action()));
} else if (remapping.remapping_action->is_static_shortcut_action()) {
dict.Set(prefs::kButtonRemappingStaticShortcutAction,
static_cast<int>(
remapping.remapping_action->get_static_shortcut_action()));
}
return dict;
}
base::Value::List ConvertButtonRemappingArrayToList(
const std::vector<mojom::ButtonRemappingPtr>& remappings,
mojom::CustomizationRestriction customization_restriction,
bool redact_button_names) {
base::Value::List list;
for (const auto& remapping : remappings) {
base::Value::Dict dict = ConvertButtonRemappingToDict(
*remapping, customization_restriction, redact_button_names);
// Remove empty dicts.
if (dict.empty()) {
continue;
}
list.Append(std::move(dict));
}
return list;
}
std::vector<mojom::ButtonRemappingPtr> ConvertListToButtonRemappingArray(
const base::Value::List& list,
mojom::CustomizationRestriction customization_restriction) {
std::vector<mojom::ButtonRemappingPtr> array;
for (const auto& element : list) {
if (!element.is_dict()) {
continue;
}
const auto& dict = element.GetDict();
auto remapping =
ConvertDictToButtonRemapping(dict, customization_restriction);
if (remapping) {
array.push_back(std::move(remapping));
}
}
return array;
}
mojom::ButtonRemappingPtr ConvertDictToButtonRemapping(
const base::Value::Dict& dict,
mojom::CustomizationRestriction customization_restriction) {
if (customization_restriction ==
mojom::CustomizationRestriction::kDisallowCustomizations) {
return nullptr;
}
const std::string* name = dict.FindString(prefs::kButtonRemappingName);
if (!name) {
return nullptr;
}
// button is a union.
mojom::ButtonPtr button;
const std::optional<int> customizable_button =
dict.FindInt(prefs::kButtonRemappingCustomizableButton);
const std::optional<int> key_code =
dict.FindInt(prefs::kButtonRemappingKeyboardCode);
// Button can't be both a keyboard key and a customization button.
if (customizable_button && key_code) {
return nullptr;
}
// Button must exist.
if (!customizable_button && !key_code) {
return nullptr;
}
// Button can be either a keyboard key or a customization button. If
// the customization_restriction is not kDisableKeyEventRewrites,
// the button is allowed to be a keyboard key.
if (customizable_button) {
button = mojom::Button::NewCustomizableButton(
static_cast<mojom::CustomizableButton>(*customizable_button));
} else if (key_code &&
customization_restriction !=
mojom::CustomizationRestriction::kDisableKeyEventRewrites) {
button = mojom::Button::NewVkey(static_cast<::ui::KeyboardCode>(*key_code));
} else {
return nullptr;
}
// remapping_action is an optional union.
mojom::RemappingActionPtr remapping_action;
const base::Value::Dict* key_event =
dict.FindDict(prefs::kButtonRemappingKeyEvent);
const std::optional<int> accelerator_action =
dict.FindInt(prefs::kButtonRemappingAcceleratorAction);
const std::optional<int> static_shortcut_action =
dict.FindInt(prefs::kButtonRemappingStaticShortcutAction);
// Remapping action can only have one value at most.
if ((key_event && accelerator_action) ||
(key_event && static_shortcut_action) ||
(accelerator_action && static_shortcut_action)) {
return nullptr;
}
// Remapping action can be either a keyboard event or an accelerator action
// or static shortcut action or null.
if (key_event) {
const std::optional<int> dom_code =
key_event->FindInt(prefs::kButtonRemappingDomCode);
const std::optional<int> vkey =
key_event->FindInt(prefs::kButtonRemappingKeyboardCode);
const std::optional<int> dom_key =
key_event->FindInt(prefs::kButtonRemappingDomKey);
const std::optional<int> modifiers =
key_event->FindInt(prefs::kButtonRemappingModifiers);
if (!dom_code || !vkey || !dom_key || !modifiers) {
return nullptr;
}
ui::KeyboardCode vkey_value = static_cast<ui::KeyboardCode>(*vkey);
remapping_action = mojom::RemappingAction::NewKeyEvent(
mojom::KeyEvent::New(vkey_value, *dom_code, *dom_key, *modifiers,
base::UTF16ToUTF8(GetKeyDisplay(vkey_value))));
} else if (accelerator_action) {
remapping_action = mojom::RemappingAction::NewAcceleratorAction(
static_cast<ash::AcceleratorAction>(*accelerator_action));
} else if (static_shortcut_action) {
remapping_action = mojom::RemappingAction::NewStaticShortcutAction(
static_cast<mojom::StaticShortcutAction>(*static_shortcut_action));
}
return mojom::ButtonRemapping::New(*name, std::move(button),
std::move(remapping_action));
}
bool IsChromeOSKeyboard(const mojom::Keyboard& keyboard) {
return keyboard.meta_key == ui::mojom::MetaKey::kLauncher ||
keyboard.meta_key == ui::mojom::MetaKey::kSearch;
}
bool IsSplitModifierKeyboard(const mojom::Keyboard& keyboard) {
return keyboard.meta_key == ui::mojom::MetaKey::kLauncherRefresh;
}
bool IsSplitModifierKeyboard(int device_id) {
return Shell::Get()->keyboard_capability()->HasFunctionKey(device_id) &&
Shell::Get()->keyboard_capability()->HasRightAltKey(device_id);
}
std::string GetDeviceKeyForMetadataRequest(const std::string& device_key) {
if (features::IsWelcomeExperienceTestUnsupportedDevicesEnabled()) {
return kWelcomeExperienceTestDeviceKey;
}
return device_key;
}
} // namespace ash