chromium/ash/accelerators/modifier_key_combo_recorder.cc

// Copyright 2024 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/accelerators/modifier_key_combo_recorder.h"

#include <optional>
#include <tuple>

#include "ash/events/event_rewriter_controller_impl.h"
#include "ash/events/prerewritten_event_forwarder.h"
#include "ash/public/cpp/accelerators_util.h"
#include "ash/shell.h"
#include "base/containers/fixed_flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/events/ash/keyboard_capability.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/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"

namespace ash {

namespace {

using DeviceType = ui::KeyboardCapability::DeviceType;
using Modifier = ModifierKeyComboRecorder::Modifier;
using ModifierFlag = ModifierKeyComboRecorder::ModifierFlag;
using ModifierLocation = ModifierKeyComboRecorder::ModifierLocation;

constexpr auto kShiftKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
    ui::VKEY_SHIFT,
    ui::VKEY_LSHIFT,
    ui::VKEY_RSHIFT,
});

constexpr auto kAltKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
    ui::VKEY_MENU,
    ui::VKEY_LMENU,
    ui::VKEY_RMENU,
});

constexpr auto kAltGrKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
    ui::VKEY_ALTGR,
});

constexpr auto kMetaKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
    ui::VKEY_LWIN,
    ui::VKEY_RWIN,
});

constexpr auto kControlKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
    ui::VKEY_CONTROL,
    ui::VKEY_LCONTROL,
    ui::VKEY_RCONTROL,
});

constexpr auto kFunctionKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
    ui::VKEY_FUNCTION,
});

// The ModifierKeyCombo metric is formed as follows:
// The bottom 16 bits are composed of the `AcceleratorKeyInputType` that matches
// what key was pressed. The top 16 bits are composed of bitfields from the
// `ModifierFlag` enum for each modifier (including differentiating between Left
// and Right modifiers).
uint32_t CalculateHash(AcceleratorKeyInputType key_type,
                       uint32_t modifier_flags) {
  static_assert(sizeof(AcceleratorKeyInputType) <= sizeof(uint16_t));
  static_assert(static_cast<uint32_t>(ModifierFlag::kMaxValue) <
                (sizeof(uint16_t) * 8));
  return (modifier_flags << (sizeof(uint16_t) * 8)) +
         static_cast<uint16_t>(key_type);
}

ModifierFlag GetModifierFlagFromModifierAndLocation(Modifier modifier,
                                                    ModifierLocation location) {
  switch (modifier) {
    case Modifier::kMeta:
      return location == ModifierLocation::kLeft ? ModifierFlag::kMetaLeft
                                                 : ModifierFlag::kMetaRight;
    case Modifier::kControl:
      return location == ModifierLocation::kLeft ? ModifierFlag::kControlLeft
                                                 : ModifierFlag::kControlRight;
    case Modifier::kAlt: {
      return location == ModifierLocation::kLeft ? ModifierFlag::kAltLeft
                                                 : ModifierFlag::kAltRight;
    }
    case Modifier::kShift:
      return location == ModifierLocation::kLeft ? ModifierFlag::kShiftLeft
                                                 : ModifierFlag::kShiftRight;
  }
}

std::optional<std::pair<Modifier, ModifierLocation>> GetModifierFromKeyEvent(
    const ui::KeyEvent& key_event) {
  switch (key_event.code()) {
    case ui::DomCode::META_LEFT:
      return std::make_pair(Modifier::kMeta, ModifierLocation::kLeft);
    case ui::DomCode::META_RIGHT:
      return std::make_pair(Modifier::kMeta, ModifierLocation::kRight);
    case ui::DomCode::CONTROL_LEFT:
      return std::make_pair(Modifier::kControl, ModifierLocation::kLeft);
    case ui::DomCode::CONTROL_RIGHT:
      return std::make_pair(Modifier::kControl, ModifierLocation::kRight);
    case ui::DomCode::ALT_LEFT:
      return std::make_pair(Modifier::kAlt, ModifierLocation::kLeft);
    case ui::DomCode::ALT_RIGHT:
      return std::make_pair(Modifier::kAlt, ModifierLocation::kRight);
    case ui::DomCode::SHIFT_LEFT:
      return std::make_pair(Modifier::kShift, ModifierLocation::kLeft);
    case ui::DomCode::SHIFT_RIGHT:
      return std::make_pair(Modifier::kShift, ModifierLocation::kRight);
    default:
      return std::nullopt;
  }
}

void RecordMetricForDeviceType(ui::KeyboardCapability::DeviceType device_type,
                               uint32_t hash) {
  switch (device_type) {
    case ui::KeyboardCapability::DeviceType::kDeviceExternalChromeOsKeyboard:
      UMA_HISTOGRAM_SPARSE("ChromeOS.Inputs.ModifierKeyCombo.CrOSExternal",
                           hash);
      break;
    case ui::KeyboardCapability::DeviceType::kDeviceExternalAppleKeyboard:
    case ui::KeyboardCapability::DeviceType::
        kDeviceExternalNullTopRowChromeOsKeyboard:
    case ui::KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
    case ui::KeyboardCapability::DeviceType::kDeviceExternalUnknown:
    case ui::KeyboardCapability::DeviceType::kDeviceHotrodRemote:
    case ui::KeyboardCapability::DeviceType::kDeviceVirtualCoreKeyboard:
      UMA_HISTOGRAM_SPARSE("ChromeOS.Inputs.ModifierKeyCombo.External", hash);
      break;
    case ui::KeyboardCapability::DeviceType::kDeviceUnknown:
    case ui::KeyboardCapability::DeviceType::kDeviceInternalKeyboard:
    case ui::KeyboardCapability::DeviceType::kDeviceInternalRevenKeyboard:
      UMA_HISTOGRAM_SPARSE("ChromeOS.Inputs.ModifierKeyCombo.Internal", hash);
      break;
  }
}

}  // namespace

ModifierKeyComboRecorder::ModifierKeyComboRecorder() = default;
ModifierKeyComboRecorder::~ModifierKeyComboRecorder() {
  CHECK(Shell::Get());
  auto* event_forwarder =
      Shell::Get()->event_rewriter_controller()->prerewritten_event_forwarder();
  if (initialized_ && event_forwarder) {
    event_forwarder->RemoveObserver(this);
  }
}

void ModifierKeyComboRecorder::Initialize() {
  CHECK(Shell::Get());
  auto* event_forwarder =
      Shell::Get()->event_rewriter_controller()->prerewritten_event_forwarder();
  if (!event_forwarder) {
    LOG(ERROR) << "Attempted to initialiaze ModifierKeyComboRecorder before "
               << "PrerewrittenEventForwarder was initialized.";
    return;
  }

  initialized_ = true;
  event_forwarder->AddObserver(this);
}

void ModifierKeyComboRecorder::OnPrerewriteKeyInputEvent(
    const ui::KeyEvent& key_event) {
  if (key_event.type() == ui::EventType::kKeyReleased ||
      key_event.is_repeat()) {
    return;
  }

  UpdateModifierLocations(key_event);

  const AcceleratorKeyInputType type = GetKeyInputTypeFromKeyEvent(key_event);
  const uint32_t modifier_flags = GenerateModifierFlagsFromKeyEvent(key_event);
  const uint32_t hash = CalculateHash(type, modifier_flags);

  // True if Shift is the only modifier key present on the event.
  const bool only_shift_present =
      modifier_flags == GetModifierFlagFromModifier(Modifier::kShift);

  // Do not emit the metric if its only an alpha or digit key pressed (with or
  // without Shift held).
  if ((type == AcceleratorKeyInputType::kAlpha ||
       type == AcceleratorKeyInputType::kDigit) &&
      (modifier_flags == 0 || only_shift_present)) {
    return;
  }

  const auto device_type = Shell::Get()->keyboard_capability()->GetDeviceType(
      key_event.source_device_id());
  RecordMetricForDeviceType(device_type, hash);
}

uint32_t ModifierKeyComboRecorder::GenerateModifierFlagsFromKeyEvent(
    const ui::KeyEvent& event) {
  uint32_t modifier_flags = 0;
  if (event.flags() & ui::EF_ALT_DOWN && !kAltKeys.contains(event.key_code())) {
    modifier_flags += GetModifierFlagFromModifier(Modifier::kAlt);
  }

  // AltGr can be check purely from the modifier flags.
  if (event.flags() & ui::EF_ALTGR_DOWN &&
      !kAltGrKeys.contains(event.key_code())) {
    modifier_flags += 1 << static_cast<uint32_t>(ModifierFlag::kAltGr);
  }

  if (event.flags() & ui::EF_CONTROL_DOWN &&
      !kControlKeys.contains(event.key_code())) {
    modifier_flags += GetModifierFlagFromModifier(Modifier::kControl);
  }

  if (event.flags() & ui::EF_COMMAND_DOWN &&
      !kMetaKeys.contains(event.key_code())) {
    modifier_flags += GetModifierFlagFromModifier(Modifier::kMeta);
  }

  if (event.flags() & ui::EF_SHIFT_DOWN &&
      !kShiftKeys.contains(event.key_code())) {
    modifier_flags += GetModifierFlagFromModifier(Modifier::kShift);
  }

  if (event.flags() & ui::EF_FUNCTION_DOWN &&
      !kFunctionKeys.contains(event.key_code())) {
    modifier_flags += 1 << static_cast<uint32_t>(ModifierFlag::kFunction);
  }

  return modifier_flags;
}

uint32_t ModifierKeyComboRecorder::GetModifierFlagFromModifier(
    Modifier modifier) {
  const ModifierFlag modifier_flag = GetModifierFlagFromModifierAndLocation(
      modifier, modifier_locations_[static_cast<uint32_t>(modifier)]);
  return 1 << static_cast<uint32_t>(modifier_flag);
}

void ModifierKeyComboRecorder::UpdateModifierLocations(
    const ui::KeyEvent& event) {
  auto modifier_modifier_location_pair = GetModifierFromKeyEvent(event);
  if (!modifier_modifier_location_pair) {
    return;
  }

  const auto& [modifier, modifier_location] = *modifier_modifier_location_pair;
  modifier_locations_[static_cast<uint32_t>(modifier)] = modifier_location;
}

}  // namespace ash