chromium/ui/events/win/modifier_keyboard_hook_win.cc

// Copyright 2018 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/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <optional>
#include <utility>

#include "base/logging.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/win/events_win_utils.h"
#include "ui/events/win/keyboard_hook_monitor_impl.h"
#include "ui/events/win/keyboard_hook_win_base.h"
#include "ui/gfx/native_widget_types.h"

namespace ui {

namespace {

// The Windows KeyboardHook implementation uses a low-level hook in order to
// intercept system key combinations such as Alt + Tab.  A standard hook or
// other window message filtering mechanisms will allow us to observe key events
// but we would be unable to block them or the corresponding system action.
// There are downsides to this approach as described in the following blurbs.

// Low-level hooks are not given a repeat state for each key event.  This is
// because the events are intercepted prior to the OS handling them and tracking
// their states the usual way.  We solve this by tracking the last key seen and
// modifying the event manually to indicate it is a repeat.  This works for
// every key except escape which is used to exit fullscreen and tear down the
// hook.  The quirk is that after the hook is torn down, the first escape key
// event passed to the window will appear like an initial keydown.  This is
// because it *is* an initial keydown from Windows' perspective as it was being
// intercepted before then.

// Intercepting modifier keys (Ctrl, Shift, Win, Etc.) within a low-level
// keyboard hook will result in an incorrect modifier state reported by the OS
// for that key.  This is because the hook intercepts the event before the OS
// has a chance to observe the event and update its internal state.  This
// trade-off is necessary as we also want to intercept and prevent specific key
// combos from taking effect.

// A related side-effect occurs when intercepting printable characters.  Since
// the key event is intercepted before the OS handles it, no char events are
// produced.  So if we intercept scan codes which should generate a printable
// character, the result is that the key up/down events are sent correctly but
// no WM_CHAR message is generated.  This is unacceptable for some usages of the
// hook as the behavior is very different between locked and unlocked states.

// This is a fair number of downsides however the ability to block system key
// combos is a hard requirement so we need to work around them.

// The solution we have adopted is:
// - Only intercept modifiers and allow all other keys to pass through
//   Note: Shift is not included as otherwise it is not applied by the OS and
//         the printable characters generated by the key event will be wrong.
// - Track the repeat state ourselves
// - Update the per-thread key state for the tracked modifiers using
//   SetKeyboardState().

// In practice this works well as intercepting the modifiers allows us to block
// system keyboard combos and all other keys generate the proper events and
// printable chars.

// Using SetKeyboardState() allows us to tell Windows the current state for the
// modifiers we intercept.  This state only works for the current thread,
// meaning that other applications / threads which check the key state for the
// modifiers will not receive an accurate value.  One constraint for the
// KeyboardHook is that it is only engaged when our window is fullscreen and
// focused so we don't need to worry too much about other apps.

// Represents a VK_LCONTROL scancode with the 0x02 flag in the high word.
// The 0x02 flag does not seem to be documented on MSDN or in the public Windows
// headers.  I suspect it is related to the KF_ family of constants which skips
// from 0x0100 to 0x0800 with 0x0200 and 0x0400 undocumented (likely reserved).
constexpr DWORD kSynthesizedControlScanCodeForAltGr = 0x021D;

// {Get|Set}KeyboardState() receives an array with 256 elements.  This is
// described in MSDN but no constant exists for this value.  Function reference:
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getkeyboardstate
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-setkeyboardstate
constexpr int kKeyboardStateArraySize = 256;

// Used for setting and testing the bits in the KeyboardState array.
constexpr BYTE kKeyDown = 0x80;
constexpr BYTE kKeyUp = 0x00;

bool IsAltKey(DWORD vk) {
  return vk == VK_MENU || vk == VK_LMENU || vk == VK_RMENU;
}

bool IsControlKey(DWORD vk) {
  return vk == VK_CONTROL || vk == VK_LCONTROL || vk == VK_RCONTROL;
}

bool IsWindowsKey(DWORD vk) {
  return vk == VK_LWIN || vk == VK_RWIN;
}

bool IsModifierKey(DWORD vk) {
  // We don't track the state of the shift key as we want to allow the OS to
  // know about its state so it is correctly applied to printable characters.
  return IsAltKey(vk) || IsControlKey(vk) || IsWindowsKey(vk);
}

class ModifierKeyboardHookWinImpl : public KeyboardHookWinBase {
 public:
  ModifierKeyboardHookWinImpl(std::optional<base::flat_set<DomCode>> dom_codes,
                              KeyEventCallback callback,
                              bool enable_hook_registration);

  ModifierKeyboardHookWinImpl(const ModifierKeyboardHookWinImpl&) = delete;
  ModifierKeyboardHookWinImpl& operator=(const ModifierKeyboardHookWinImpl&) =
      delete;

  ~ModifierKeyboardHookWinImpl() override;

  // KeyboardHookWinBase implementation.
  bool ProcessKeyEventMessage(WPARAM w_param,
                              DWORD vk,
                              DWORD scan_code,
                              DWORD time_stamp) override;

  bool Register();

 private:
  static LRESULT CALLBACK ProcessKeyEvent(int code,
                                          WPARAM w_param,
                                          LPARAM l_param);

  void UpdateModifierState(DWORD vk, bool key_down);

  void ClearModifierStates();

  KeyboardHookMonitorImpl* GetKeyboardHookMonitor();

  static ModifierKeyboardHookWinImpl* instance_;

  // Tracks the last non-located key down seen in order to determine if the
  // current key event should be marked as a repeated key press.
  DWORD last_key_down_ = 0;

  // Tracks the number of AltGr key sequences seen since the start of the most
  // recent AltGr key down.  When the AltGr key is pressed, Windows injects a
  // synthesized left control key event followed by the right alt key event.
  // This sequence occurs on the initial keypress and every repeat.
  int altgr_sequence_count_ = 0;
};

// static
ModifierKeyboardHookWinImpl* ModifierKeyboardHookWinImpl::instance_ = nullptr;

ModifierKeyboardHookWinImpl::ModifierKeyboardHookWinImpl(
    std::optional<base::flat_set<DomCode>> dom_codes,
    KeyEventCallback callback,
    bool enable_hook_registration)
    : KeyboardHookWinBase(std::move(dom_codes),
                          std::move(callback),
                          enable_hook_registration) {}

ModifierKeyboardHookWinImpl::~ModifierKeyboardHookWinImpl() {
  ClearModifierStates();

  if (!enable_hook_registration())
    return;

  DCHECK_EQ(instance_, this);
  instance_ = nullptr;

  KeyboardHookMonitorImpl::GetInstance()->NotifyHookUnregistered();
}

bool ModifierKeyboardHookWinImpl::Register() {
  // Only one instance of this class can be registered at a time.
  DCHECK(!instance_);
  instance_ = this;

  KeyboardHookMonitorImpl::GetInstance()->NotifyHookRegistered();

  return KeyboardHookWinBase::Register(reinterpret_cast<HOOKPROC>(
      &ModifierKeyboardHookWinImpl::ProcessKeyEvent));
}

void ModifierKeyboardHookWinImpl::ClearModifierStates() {
  BYTE keyboard_state[kKeyboardStateArraySize] = {0};
  if (!GetKeyboardState(keyboard_state)) {
    PLOG(ERROR) << "GetKeyboardState() failed: ";
    return;
  }

  // Reset each modifier position.
  keyboard_state[VK_CONTROL] = kKeyUp;
  keyboard_state[VK_LCONTROL] = kKeyUp;
  keyboard_state[VK_RCONTROL] = kKeyUp;
  keyboard_state[VK_MENU] = kKeyUp;
  keyboard_state[VK_LMENU] = kKeyUp;
  keyboard_state[VK_RMENU] = kKeyUp;
  keyboard_state[VK_LWIN] = kKeyUp;
  keyboard_state[VK_RWIN] = kKeyUp;

  if (!SetKeyboardState(keyboard_state))
    PLOG(ERROR) << "SetKeyboardState() failed: ";
}

bool ModifierKeyboardHookWinImpl::ProcessKeyEventMessage(WPARAM w_param,
                                                         DWORD vk,
                                                         DWORD scan_code,
                                                         DWORD time_stamp) {
  // The |vk| delivered to the low-level hook includes a location which is
  // needed to track individual keystates such as when both left and right
  // control keys are pressed.  Make sure that location information was retained
  // when the vkey was passed into this method.
  DCHECK_NE(vk, static_cast<DWORD>(VK_CONTROL));
  DCHECK_NE(vk, static_cast<DWORD>(VK_MENU));

  if (!IsModifierKey(vk))
    return false;

  // Windows synthesizes an additional key event when AltGr is pressed.  The
  // event has the left control scan code in the low word and a 0x02 flag set in
  // the high word.  The VK_RMENU event will be sent once this key is processed.
  bool is_altgr_sequence = false;
  if (scan_code == kSynthesizedControlScanCodeForAltGr) {
    is_altgr_sequence = true;
    scan_code = KeycodeConverter::DomCodeToNativeKeycode(DomCode::CONTROL_LEFT);
  }

  // If the caller has only requested that Alt be captured, then we don't want
  // to bail early on the injected control event.
  DomCode dom_code = DomCode::NONE;
  if (is_altgr_sequence)
    dom_code = DomCode::ALT_RIGHT;
  else
    dom_code = KeycodeConverter::NativeKeycodeToDomCode(scan_code);

  if (!ShouldCaptureKeyEvent(dom_code))
    return false;

  if (is_altgr_sequence)
    altgr_sequence_count_++;
  else if (vk != VK_RMENU)
    altgr_sequence_count_ = 0;

  // The Windows key is always located, the other modifiers are not.
  DWORD non_located_vk =
      IsWindowsKey(vk)
          ? vk
          : LocatedToNonLocatedKeyboardCode(static_cast<KeyboardCode>(vk));

  bool is_repeat = false;
  CHROME_MSG msg = {nullptr, static_cast<UINT>(w_param), non_located_vk,
                    GetLParamFromScanCode(scan_code), time_stamp};
  EventType event_type = EventTypeFromMSG(msg);
  if (event_type == EventType::kKeyPressed) {
    UpdateModifierState(vk, /*key_down=*/true);
    // We use the non-located vkey to determine whether a key event is a repeat
    // or not.  The exception is for AltGr which has a two key sequence which
    // alternates.
    is_repeat = (last_key_down_ == non_located_vk) || altgr_sequence_count_ > 1;
    last_key_down_ = non_located_vk;
  } else {
    DCHECK_EQ(event_type, EventType::kKeyReleased);
    UpdateModifierState(vk, /*key_down=*/false);
    altgr_sequence_count_ = 0;
    last_key_down_ = 0;
  }

  std::unique_ptr<KeyEvent> key_event =
      std::make_unique<KeyEvent>(KeyEventFromMSG(msg));
  if (is_repeat)
    key_event->SetFlags(key_event->flags() | EF_IS_REPEAT);
  ForwardCapturedKeyEvent(key_event.get());

  return true;
}

void ModifierKeyboardHookWinImpl::UpdateModifierState(DWORD vk,
                                                      bool is_key_down) {
  BYTE keyboard_state[kKeyboardStateArraySize] = {0};
  if (!GetKeyboardState(keyboard_state)) {
    PLOG(ERROR) << "GetKeyboardState() failed: ";
    return;
  }

  // Update the located virtual key first.
  keyboard_state[vk] = is_key_down ? kKeyDown : kKeyUp;

  // Now update the non-located virtual key.
  keyboard_state[VK_CONTROL] = (keyboard_state[VK_LCONTROL] == kKeyDown ||
                                keyboard_state[VK_RCONTROL] == kKeyDown)
                                   ? kKeyDown
                                   : kKeyUp;
  keyboard_state[VK_MENU] = (keyboard_state[VK_LMENU] == kKeyDown ||
                             keyboard_state[VK_RMENU] == kKeyDown)
                                ? kKeyDown
                                : kKeyUp;

  if (!SetKeyboardState(keyboard_state))
    PLOG(ERROR) << "SetKeyboardState() failed: ";
}

// static
LRESULT CALLBACK ModifierKeyboardHookWinImpl::ProcessKeyEvent(int code,
                                                              WPARAM w_param,
                                                              LPARAM l_param) {
  return KeyboardHookWinBase::ProcessKeyEvent(instance_, code, w_param,
                                              l_param);
}

}  // namespace

// static
std::unique_ptr<KeyboardHook> KeyboardHook::CreateModifierKeyboardHook(
    std::optional<base::flat_set<DomCode>> dom_codes,
    gfx::AcceleratedWidget accelerated_widget,
    KeyEventCallback callback) {
  std::unique_ptr<ModifierKeyboardHookWinImpl> keyboard_hook =
      std::make_unique<ModifierKeyboardHookWinImpl>(
          std::move(dom_codes), std::move(callback),
          /*enable_hook_registration=*/true);

  if (!keyboard_hook->Register())
    return nullptr;

  return keyboard_hook;
}

// static
std::unique_ptr<KeyboardHookWinBase>
KeyboardHookWinBase::CreateModifierKeyboardHookForTesting(
    std::optional<base::flat_set<DomCode>> dom_codes,
    KeyEventCallback callback) {
  return std::make_unique<ModifierKeyboardHookWinImpl>(
      std::move(dom_codes), std::move(callback),
      /*enable_hook_registration=*/false);
}

}  // namespace ui