Reland: Keyboard events (#87174)

diff --git a/dev/tools/gen_keycodes/data/keyboard_key.tmpl b/dev/tools/gen_keycodes/data/keyboard_key.tmpl
index eaed68c..e0b8f0f 100644
--- a/dev/tools/gen_keycodes/data/keyboard_key.tmpl
+++ b/dev/tools/gen_keycodes/data/keyboard_key.tmpl
@@ -327,6 +327,9 @@
 
 @@@LOGICAL_KEY_DEFINITIONS@@@
 
+  /// A list of all predefined constant [LogicalKeyboardKey]s.
+  static Iterable<LogicalKeyboardKey> get knownLogicalKeys => _knownLogicalKeys.values;
+
   // A list of all predefined constant LogicalKeyboardKeys so they can be
   // searched.
   static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
@@ -489,6 +492,9 @@
 
 @@@PHYSICAL_KEY_DEFINITIONS@@@
 
+  /// A list of all predefined constant [PhysicalKeyboardKey]s.
+  static Iterable<PhysicalKeyboardKey> get knownPhysicalKeys => _knownPhysicalKeys.values;
+
   // A list of all the predefined constant PhysicalKeyboardKeys so that they
   // can be searched.
   static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{
diff --git a/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json b/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json
index e4803cb..5d8b68d 100644
--- a/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json
+++ b/dev/tools/gen_keycodes/data/windows_logical_to_window_vk.json
@@ -176,41 +176,5 @@
   "Zoom": ["ZOOM"],
   "Noname": ["NONAME"],
   "Pa1": ["PA1"],
-  "OemClear": ["OEM_CLEAR"],
-  "0": ["0"],
-  "1": ["1"],
-  "2": ["2"],
-  "3": ["3"],
-  "4": ["4"],
-  "5": ["5"],
-  "6": ["6"],
-  "7": ["7"],
-  "8": ["8"],
-  "9": ["9"],
-  "KeyA": ["A"],
-  "KeyB": ["B"],
-  "KeyC": ["C"],
-  "KeyD": ["D"],
-  "KeyE": ["E"],
-  "KeyF": ["F"],
-  "KeyG": ["G"],
-  "KeyH": ["H"],
-  "KeyI": ["I"],
-  "KeyJ": ["J"],
-  "KeyK": ["K"],
-  "KeyL": ["L"],
-  "KeyM": ["M"],
-  "KeyN": ["N"],
-  "KeyO": ["O"],
-  "KeyP": ["P"],
-  "KeyQ": ["Q"],
-  "KeyR": ["R"],
-  "KeyS": ["S"],
-  "KeyT": ["T"],
-  "KeyU": ["U"],
-  "KeyV": ["V"],
-  "KeyW": ["W"],
-  "KeyX": ["X"],
-  "KeyY": ["Y"],
-  "KeyZ": ["Z"]
+  "OemClear": ["OEM_CLEAR"]
 }
diff --git a/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart b/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
index 6a21b99..27eee82 100644
--- a/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
+++ b/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
@@ -24,6 +24,16 @@
       || (charCode >= charLowerA && charCode <= charLowerZ);
 }
 
+bool _isDigit(String? char) {
+  if (char == null)
+    return false;
+  final int charDigit0 = '0'.codeUnitAt(0);
+  final int charDigit9 = '9'.codeUnitAt(0);
+  assert(char.length == 1);
+  final int charCode = char.codeUnitAt(0);
+  return charCode >= charDigit0 && charCode <= charDigit9;
+}
+
 /// Generates the keyboard_maps.dart files, based on the information in the key
 /// data structure given to it.
 class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
@@ -171,7 +181,9 @@
       // because they are not used by the embedding. Add them manually.
       final List<int>? keyCodes = entry.windowsValues.isNotEmpty
         ? entry.windowsValues
-        : (_isAsciiLetter(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] : null);
+        : (_isAsciiLetter(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
+           _isDigit(entry.keyLabel)       ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
+           null);
       if (keyCodes != null) {
         for (final int code in keyCodes) {
           lines.add(code, '  $code: LogicalKeyboardKey.${entry.constantName},');
diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart
index e6a266f..0d7ec01 100644
--- a/packages/flutter/lib/services.dart
+++ b/packages/flutter/lib/services.dart
@@ -15,9 +15,11 @@
 export 'src/services/binary_messenger.dart';
 export 'src/services/binding.dart';
 export 'src/services/clipboard.dart';
+export 'src/services/debug.dart';
 export 'src/services/deferred_component.dart';
 export 'src/services/font_loader.dart';
 export 'src/services/haptic_feedback.dart';
+export 'src/services/hardware_keyboard.dart';
 export 'src/services/keyboard_key.dart';
 export 'src/services/keyboard_maps.dart';
 export 'src/services/message_codec.dart';
diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart
index b5d2c39..c7858db 100644
--- a/packages/flutter/lib/src/services/binding.dart
+++ b/packages/flutter/lib/src/services/binding.dart
@@ -14,7 +14,9 @@
 
 import 'asset_bundle.dart';
 import 'binary_messenger.dart';
+import 'hardware_keyboard.dart';
 import 'message_codec.dart';
+import 'raw_keyboard.dart';
 import 'restoration.dart';
 import 'system_channels.dart';
 
@@ -31,6 +33,7 @@
     _instance = this;
     _defaultBinaryMessenger = createBinaryMessenger();
     _restorationManager = createRestorationManager();
+    _initKeyboard();
     initLicenses();
     SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));
     SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
@@ -42,6 +45,23 @@
   static ServicesBinding? get instance => _instance;
   static ServicesBinding? _instance;
 
+  /// The global singleton instance of [HardwareKeyboard], which can be used to
+  /// query keyboard states.
+  HardwareKeyboard get keyboard => _keyboard;
+  late final HardwareKeyboard _keyboard;
+
+  /// The global singleton instance of [KeyEventManager], which is used
+  /// internally to dispatch key messages.
+  KeyEventManager get keyEventManager => _keyEventManager;
+  late final KeyEventManager _keyEventManager;
+
+  void _initKeyboard() {
+    _keyboard = HardwareKeyboard();
+    _keyEventManager = KeyEventManager(_keyboard, RawKeyboard.instance);
+    window.onKeyData = _keyEventManager.handleKeyData;
+    SystemChannels.keyEvent.setMessageHandler(_keyEventManager.handleRawKeyMessage);
+  }
+
   /// The default instance of [BinaryMessenger].
   ///
   /// This is used to send messages from the application to the platform, and
diff --git a/packages/flutter/lib/src/services/debug.dart b/packages/flutter/lib/src/services/debug.dart
new file mode 100644
index 0000000..3e0421a
--- /dev/null
+++ b/packages/flutter/lib/src/services/debug.dart
@@ -0,0 +1,30 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+
+import 'hardware_keyboard.dart';
+
+/// Override the transit mode with which key events are simulated.
+///
+/// Setting [debugKeyEventSimulatorTransitModeOverride] is a good way to make
+/// certain tests simulate the behavior of different type of platforms in terms
+/// of their extent of support for keyboard API.
+KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride;
+
+/// Returns true if none of the widget library debug variables have been changed.
+///
+/// This function is used by the test framework to ensure that debug variables
+/// haven't been inadvertently changed.
+///
+/// See [the services library](services/services-library.html) for a complete list.
+bool debugAssertAllServicesVarsUnset(String reason) {
+  assert(() {
+    if (debugKeyEventSimulatorTransitModeOverride != null) {
+      throw FlutterError(reason);
+    }
+    return true;
+  }());
+  return true;
+}
diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart
new file mode 100644
index 0000000..dc57992
--- /dev/null
+++ b/packages/flutter/lib/src/services/hardware_keyboard.dart
@@ -0,0 +1,941 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' as ui;
+
+import 'package:flutter/foundation.dart';
+import 'binding.dart';
+import 'keyboard_key.dart';
+import 'raw_keyboard.dart';
+
+/// Represents a lock mode of a keyboard, such as [KeyboardLockMode.capsLock].
+///
+/// A lock mode locks some of a keyboard's keys into a distinct mode of operation,
+/// depending on the lock settings selected. The status of the mode is toggled
+/// with each key down of its corresponding logical key. A [KeyboardLockMode]
+/// object is used to query whether this mode is enabled on the keyboard.
+///
+/// Only a limited number of modes are supported, which are enumerated as
+/// static members of this class. Manual constructing of this class is
+/// prohibited.
+@immutable
+class KeyboardLockMode {
+  // KeyboardLockMode has a fixed pool of supported keys, enumerated as static
+  // members of this class, therefore constructing is prohibited.
+  const KeyboardLockMode._(this.logicalKey);
+
+  /// The logical key that triggers this lock mode.
+  final LogicalKeyboardKey logicalKey;
+
+  /// Represents the number lock mode on the keyboard.
+  ///
+  /// On supporting systems, enabling number lock mode usually allows key
+  /// presses of the number pad to input numbers, instead of acting as up, down,
+  /// left, right, page up, end, etc.
+  static const KeyboardLockMode numLock = KeyboardLockMode._(LogicalKeyboardKey.numLock);
+
+  /// Represents the scrolling lock mode on the keyboard.
+  ///
+  /// On supporting systems and applications (such as a spreadsheet), enabling
+  /// scrolling lock mode usually allows key presses of the cursor keys to
+  /// scroll the document instead of the cursor.
+  static const KeyboardLockMode scrollLock = KeyboardLockMode._(LogicalKeyboardKey.scrollLock);
+
+  /// Represents the capital letters lock mode on the keyboard.
+  ///
+  /// On supporting systems, enabling capital lock mode allows key presses of
+  /// the letter keys to input uppercase letters instead of lowercase.
+  static const KeyboardLockMode capsLock = KeyboardLockMode._(LogicalKeyboardKey.capsLock);
+
+  static final Map<int, KeyboardLockMode> _knownLockModes = <int, KeyboardLockMode>{
+    numLock.logicalKey.keyId: numLock,
+    scrollLock.logicalKey.keyId: scrollLock,
+    capsLock.logicalKey.keyId: capsLock,
+  };
+
+  /// Returns the [KeyboardLockMode] constant from the logical key, or
+  /// null, if not found.
+  static KeyboardLockMode? findLockByLogicalKey(LogicalKeyboardKey logicalKey) => _knownLockModes[logicalKey.keyId];
+}
+
+/// Defines the interface for keyboard key events.
+///
+/// The [KeyEvent] provides a universal model for key event information from a
+/// hardware keyboard across platforms.
+///
+/// See also:
+///
+///  * [HardwareKeyboard] for full introduction to key event model and handling.
+///  * [KeyDownEvent], a subclass for events representing the user pressing a
+///    key.
+///  * [KeyRepeatEvent], a subclass for events representing the user holding a
+///    key, causing repeated events.
+///  * [KeyUpEvent], a subclass for events representing the user releasing a
+///    key.
+@immutable
+abstract class KeyEvent with Diagnosticable {
+  /// Create a const KeyEvent by providing each field.
+  const KeyEvent({
+    required this.physicalKey,
+    required this.logicalKey,
+    this.character,
+    required this.timeStamp,
+    this.synthesized = false,
+  });
+
+  /// Returns an object representing the physical location of this key.
+  ///
+  /// A [PhysicalKeyboardKey] represents a USB HID code sent from the keyboard,
+  /// ignoring the key map, modifier keys (like SHIFT), and the label on the key.
+  ///
+  /// [PhysicalKeyboardKey]s are used to describe and test for keys in a
+  /// particular location. A [PhysicalKeyboardKey] may have a name, but the name
+  /// is a mnemonic ("keyA" is easier to remember than 0x70004), derived from the
+  /// key's effect on a QWERTY keyboard. The name does not represent the key's
+  /// effect whatsoever (a physical "keyA" can be the Q key on an AZERTY
+  /// keyboard.)
+  ///
+  /// For instance, if you wanted to make a game where the key to the right of
+  /// the CAPS LOCK key made the player move left, you would be comparing a
+  /// physical key with [PhysicalKeyboardKey.keyA], since that is the key next to
+  /// the CAPS LOCK key on a QWERTY keyboard. This would return the same thing
+  /// even on an AZERTY keyboard where the key next to the CAPS LOCK produces a
+  /// "Q" when pressed.
+  ///
+  /// If you want to make your app respond to a key with a particular character
+  /// on it regardless of location of the key, use [KeyEvent.logicalKey] instead.
+  ///
+  /// Also, even though physical keys are defined with USB HID codes, their
+  /// values are not necessarily the same HID codes produced by the hardware and
+  /// presented to the driver, because on most platforms Flutter has to map the
+  /// platform representation back to an HID code since the original HID
+  /// code is not provided. USB HID is simply a conveniently well-defined
+  /// standard to list possible keys that a Flutter app can encounter.
+  ///
+  /// See also:
+  ///
+  ///  * [logicalKey] for the non-location specific key generated by this event.
+  ///  * [character] for the character generated by this keypress (if any).
+  final PhysicalKeyboardKey physicalKey;
+
+  /// Returns an object representing the logical key that was pressed.
+  ///
+  /// {@template flutter.services.KeyEvent.logicalKey}
+  /// This method takes into account the key map and modifier keys (like SHIFT)
+  /// to determine which logical key to return.
+  ///
+  /// If you are looking for the character produced by a key event, use
+  /// [KeyEvent.character] instead.
+  ///
+  /// If you are collecting text strings, use the [TextField] or
+  /// [CupertinoTextField] widgets, since those automatically handle many of the
+  /// complexities of managing keyboard input, like showing a soft keyboard or
+  /// interacting with an input method editor (IME).
+  /// {@endtemplate}
+  final LogicalKeyboardKey logicalKey;
+
+  /// Returns the Unicode character (grapheme cluster) completed by this
+  /// keystroke, if any.
+  ///
+  /// This will only return a character if this keystroke, combined with any
+  /// preceding keystroke(s), generats a character, and only on a "key down"
+  /// event. It will return null if no character has been generated by the
+  /// keystroke (e.g. a "dead" or "combining" key), or if the corresponding key
+  /// is a key without a visual representation, such as a modifier key or a
+  /// control key. It will also return null if this is a "key up" event.
+  ///
+  /// This can return multiple Unicode code points, since some characters (more
+  /// accurately referred to as grapheme clusters) are made up of more than one
+  /// code point.
+  ///
+  /// The [character] doesn't take into account edits by an input method editor
+  /// (IME), or manage the visibility of the soft keyboard on touch devices. For
+  /// composing text, use the [TextField] or [CupertinoTextField] widgets, since
+  /// those automatically handle many of the complexities of managing keyboard
+  /// input.
+  ///
+  /// The [character] is not available on [KeyUpEvent]s.
+  final String? character;
+
+  /// Time of event, relative to an arbitrary start point.
+  ///
+  /// All events share the same timeStamp origin.
+  final Duration timeStamp;
+
+  /// Whether this event is synthesized by Flutter to synchronize key states.
+  ///
+  /// An non-[synthesized] event is converted from a native event, and a native
+  /// event can only be converted to one non-[synthesized] event. Some properties
+  /// might be changed during the conversion (for example, a native repeat event
+  /// might be converted to a Flutter down event when necessary.)
+  ///
+  /// A [synthesized] event is created without a source native event in order to
+  /// synthronize key states. For example, if the native platform shows that a
+  /// shift key that was previously held has been released somehow without the
+  /// key up event dispatched (probably due to loss of focus), a synthesized key
+  /// up event will be added to regularized the event stream.
+  ///
+  /// For detailed introduction to the regularized event model, see
+  /// [HardwareKeyboard].
+  ///
+  /// Defaults to false.
+  final bool synthesized;
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<PhysicalKeyboardKey>('physicalKey', physicalKey));
+    properties.add(DiagnosticsProperty<LogicalKeyboardKey>('logicalKey', logicalKey));
+    properties.add(StringProperty('character', character));
+    properties.add(DiagnosticsProperty<Duration>('timeStamp', timeStamp));
+    properties.add(FlagProperty('synthesized', value: synthesized, ifTrue: 'synthesized'));
+  }
+}
+
+/// An event indicating that the user has pressed a key down on the keyboard.
+///
+/// See also:
+///
+///  * [KeyRepeatEvent], a key event representing the user
+///    holding a key, causing repeated events.
+///  * [KeyUpEvent], a key event representing the user
+///    releasing a key.
+///  * [HardwareKeyboard], which produces this event.
+class KeyDownEvent extends KeyEvent {
+  /// Creates a key event that represents the user pressing a key.
+  const KeyDownEvent({
+    required PhysicalKeyboardKey physicalKey,
+    required LogicalKeyboardKey logicalKey,
+    String? character,
+    required Duration timeStamp,
+    bool synthesized = false,
+  }) : super(
+         physicalKey: physicalKey,
+         logicalKey: logicalKey,
+         character: character,
+         timeStamp: timeStamp,
+         synthesized: synthesized,
+       );
+}
+
+/// An event indicating that the user has released a key on the keyboard.
+///
+/// See also:
+///
+///  * [KeyDownEvent], a key event representing the user
+///    pressing a key.
+///  * [KeyRepeatEvent], a key event representing the user
+///    holding a key, causing repeated events.
+///  * [HardwareKeyboard], which produces this event.
+class KeyUpEvent extends KeyEvent {
+  /// Creates a key event that represents the user pressing a key.
+  const KeyUpEvent({
+    required PhysicalKeyboardKey physicalKey,
+    required LogicalKeyboardKey logicalKey,
+    required Duration timeStamp,
+    bool synthesized = false,
+  }) : super(
+         physicalKey: physicalKey,
+         logicalKey: logicalKey,
+         timeStamp: timeStamp,
+         synthesized: synthesized,
+       );
+}
+
+/// An event indicating that the user has been holding a key on the keyboard
+/// and causing repeated events.
+///
+/// See also:
+///
+///  * [KeyDownEvent], a key event representing the user
+///    pressing a key.
+///  * [KeyUpEvent], a key event representing the user
+///    releasing a key.
+///  * [HardwareKeyboard], which produces this event.
+class KeyRepeatEvent extends KeyEvent {
+  /// Creates a key event that represents the user pressing a key.
+  const KeyRepeatEvent({
+    required PhysicalKeyboardKey physicalKey,
+    required LogicalKeyboardKey logicalKey,
+    String? character,
+    required Duration timeStamp,
+  }) : super(
+         physicalKey: physicalKey,
+         logicalKey: logicalKey,
+         character: character,
+         timeStamp: timeStamp,
+       );
+}
+
+/// The signature for [HardwareKeyboard.addHandler], a callback to to decide whether
+/// the entire framework handles a key event.
+typedef KeyEventCallback = bool Function(KeyEvent event);
+
+/// Manages key events from hardware keyboards.
+///
+/// [HardwareKeyboard] manages all key events of the Flutter application from
+/// hardware keyboards (in contrast to on-screen keyboards). It receives key
+/// data from the native platform, dispatches key events to registered
+/// handlers, and records the keyboard state.
+///
+/// To stay notified whenever keys are pressed, held, or released, add a
+/// handler with [addHandler]. To only be notified when a specific part of the
+/// app is focused, use a [Focus] widget's `onFocusChanged` attribute instead
+/// of [addHandler]. Handlers should be removed with [removeHandler] when
+/// notification is no longer necessary, or when the handler is being disposed.
+///
+/// To query whether a key is being held, or a lock mode is enabled, use
+/// [physicalKeysPressed], [logicalKeysPressed], or [lockModesEnabled].
+/// These states will have been updated with the event when used during a key
+/// event handler.
+///
+/// The singleton [HardwareKeyboard] instance is held by the [ServicesBinding]
+/// as [ServicesBinding.keyboard], and can be conveniently accessed using the
+/// [HardwareKeyboard.instance] static accessor.
+///
+/// ## Event model
+///
+/// Flutter uses a universal event model ([KeyEvent]) and key options
+/// ([LogicalKeyboardKey] and [PhysicalKeyboardKey]) regardless of the native
+/// platform, while preserving platform-specific features as much as
+/// possible.
+///
+/// [HardwareKeyboard] guarantees that the key model is "regularized": The key
+/// event stream consists of "key tap sequences", where a key tap sequence is
+/// defined as one [KeyDownEvent], zero or more [KeyRepeatEvent]s, and one
+/// [KeyUpEvent] in order, all with the same physical key and logical key.
+///
+/// Example:
+///
+///  * Tap and hold key A, US layout:
+///     * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "a")
+///     * KeyRepeatEvent(physicalKey: keyA, logicalKey: keyA, character: "a")
+///     * KeyUpEvent(physicalKey: keyA, logicalKey: keyA)
+///  * Press ShiftLeft, tap key A, then release ShiftLeft, US layout:
+///     * KeyDownEvent(physicalKey: shiftLeft, logicalKey: shiftLeft)
+///     * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "A")
+///     * KeyRepeatEvent(physicalKey: keyA, logicalKey: keyA, character: "A")
+///     * KeyUpEvent(physicalKey: keyA, logicalKey: keyA)
+///     * KeyUpEvent(physicalKey: shiftLeft, logicalKey: shiftLeft)
+///  * Tap key Q, French layout:
+///     * KeyDownEvent(physicalKey: keyA, logicalKey: keyQ, character: "q")
+///     * KeyUpEvent(physicalKey: keyA, logicalKey: keyQ)
+///  * Tap CapsLock:
+///     * KeyDownEvent(physicalKey: capsLock, logicalKey: capsLock)
+///     * KeyUpEvent(physicalKey: capsLock, logicalKey: capsLock)
+///
+/// When the Flutter application starts, all keys are released, and all lock
+/// modes are disabled. Upon key events, [HardwareKeyboard] will update its
+/// states, then dispatch callbacks: [KeyDownEvent]s and [KeyUpEvent]s set
+/// or reset the pressing state, while [KeyDownEvent]s also toggle lock modes.
+///
+/// Flutter will try to synchronize with the ground truth of keyboard states
+/// using synthesized events ([KeyEvent.synthesized]), subject to the
+/// availability of the platform. The desynchronization can be caused by
+/// non-empty initial state or a change in the focused window or application.
+/// For example, if CapsLock is enabled when the application starts, then
+/// immediately before the first key event, a synthesized [KeyDownEvent] and
+/// [KeyUpEvent] of CapsLock will be dispatched.
+///
+/// The resulting event stream does not map one-to-one to the native key event
+/// stream. Some native events might be skipped, while some events might be
+/// synthesized and do not correspond to native events. Synthesized events will
+/// be indicated by [KeyEvent.synthesized].
+///
+/// Example:
+///
+///  * Flutter starts with CapsLock on, the first press of keyA:
+///     * KeyDownEvent(physicalKey: capsLock, logicalKey: capsLock, synthesized: true)
+///     * KeyUpEvent(physicalKey: capsLock, logicalKey: capsLock, synthesized: true)
+///     * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "a")
+///  * While holding ShiftLeft, lose window focus, release shiftLeft, then focus
+///    back and press keyA:
+///     * KeyUpEvent(physicalKey: shiftLeft, logicalKey: shiftLeft, synthesized: true)
+///     * KeyDownEvent(physicalKey: keyA, logicalKey: keyA, character: "a")
+///
+/// Flutter does not distinguish between multiple keyboards. Flutter will
+/// process all events as if they come from a single keyboard, and try to
+/// resolve any conflicts and provide a regularized key event stream, which
+/// can deviate from the ground truth.
+///
+/// ## Compared to [RawKeyboard]
+///
+/// [RawKeyboard] is the legacy API, and will be deprecated and removed in the
+/// future.  It is recommended to always use [HardwareKeyboard] and [KeyEvent]
+/// APIs (such as [FocusNode.onKeyEvent]) to handle key events.
+///
+/// Behavior-wise, [RawKeyboard] provides a less unified, less regular
+/// event model than [HardwareKeyboard]. For example:
+///
+///  * Down events might not be matched with an up event, and vice versa (the
+///    set of pressed keys is silently updated).
+///  * The logical key of the down event might not be the same as that of the up
+///    event.
+///  * Down events and repeat events are not easily distinguishable (must be
+///    tracked manually).
+///  * Lock modes (such as CapsLock) only have their "enabled" state recorded.
+///    There's no way to acquire their pressing state.
+///
+/// See also:
+///
+///  * [KeyDownEvent], [KeyRepeatEvent], and [KeyUpEvent], the classes used to
+///    describe specific key events.
+///  * [instance], the singleton instance of this class.
+///  * [RawKeyboard], the legacy API that dispatches key events containing raw
+///    system data.
+class HardwareKeyboard {
+  /// Provides convenient access to the current [HardwareKeyboard] singleton from
+  /// the [ServicesBinding] instance.
+  static HardwareKeyboard get instance => ServicesBinding.instance!.keyboard;
+
+  final Map<PhysicalKeyboardKey, LogicalKeyboardKey> _pressedKeys = <PhysicalKeyboardKey, LogicalKeyboardKey>{};
+
+  /// The set of [PhysicalKeyboardKey]s that are pressed.
+  ///
+  /// If called from a key event handler, the result will already include the effect
+  /// of the event.
+  ///
+  /// See also:
+  ///
+  ///  * [logicalKeysPressed], which tells if a logical key is being pressed.
+  Set<PhysicalKeyboardKey> get physicalKeysPressed => _pressedKeys.keys.toSet();
+
+  /// The set of [LogicalKeyboardKey]s that are pressed.
+  ///
+  /// If called from a key event handler, the result will already include the effect
+  /// of the event.
+  ///
+  /// See also:
+  ///
+  ///  * [physicalKeysPressed], which tells if a physical key is being pressed.
+  Set<LogicalKeyboardKey> get logicalKeysPressed => _pressedKeys.values.toSet();
+
+  /// Returns the logical key that corresponds to the given pressed physical key.
+  ///
+  /// Returns null if the physical key is not currently pressed.
+  LogicalKeyboardKey? lookUpLayout(PhysicalKeyboardKey physicalKey) => _pressedKeys[physicalKey];
+
+  final Set<KeyboardLockMode> _lockModes = <KeyboardLockMode>{};
+  /// The set of [KeyboardLockMode] that are enabled.
+  ///
+  /// Lock keys, such as CapsLock, are logical keys that toggle their
+  /// respective boolean states on key down events. Such flags are usually used
+  /// as modifier to other keys or events.
+  ///
+  /// If called from a key event handler, the result will already include the effect
+  /// of the event.
+  Set<KeyboardLockMode> get lockModesEnabled => _lockModes;
+
+  void _assertEventIsRegular(KeyEvent event) {
+    assert(() {
+      const String common = 'If this occurs in real application, please report this '
+        'bug to Flutter. If this occurs in unit tests, please ensure that '
+        "simulated events follow Flutter's event model as documented in "
+        '`HardwareKeyboard`. This was the event: ';
+      if (event is KeyDownEvent) {
+        assert(!_pressedKeys.containsKey(event.physicalKey),
+          'A ${event.runtimeType} is dispatched, but the state shows that the physical '
+          'key is already pressed. $common$event');
+      } else if (event is KeyRepeatEvent || event is KeyUpEvent) {
+        assert(_pressedKeys.containsKey(event.physicalKey),
+          'A ${event.runtimeType} is dispatched, but the state shows that the physical '
+          'key is not pressed. $common$event');
+        assert(_pressedKeys[event.physicalKey] == event.logicalKey,
+          'A ${event.runtimeType} is dispatched, but the state shows that the physical '
+          'key is pressed on a different logical key. $common$event '
+          'and the recorded logical key ${_pressedKeys[event.physicalKey]}');
+      } else {
+        assert(false, 'Unexpected key event class ${event.runtimeType}');
+      }
+      return true;
+    }());
+  }
+
+  List<KeyEventCallback> _handlers = <KeyEventCallback>[];
+  bool _duringDispatch = false;
+  List<KeyEventCallback>? _modifiedHandlers;
+
+  /// Register a listener that is called every time a hardware key event
+  /// occurs.
+  ///
+  /// All registered handlers will be invoked in order regardless of
+  /// their return value. The return value indicates whether Flutter
+  /// "handles" the event. If any handler returns true, the event
+  /// will not be propagated to other native components in the add-to-app
+  /// scenario.
+  ///
+  /// If an object added a handler, it must remove the handler before it is
+  /// disposed.
+  ///
+  /// If used during event dispatching, the addition will not take effect
+  /// until after the dispatching.
+  ///
+  /// See also:
+  ///
+  ///  * [removeHandler], which removes the handler.
+  void addHandler(KeyEventCallback handler) {
+    if (_duringDispatch) {
+      _modifiedHandlers ??= <KeyEventCallback>[..._handlers];
+      _modifiedHandlers!.add(handler);
+    } else {
+      _handlers.add(handler);
+    }
+  }
+
+  /// Stop calling the given listener every time a hardware key event
+  /// occurs.
+  ///
+  /// The `handler` argument must be [identical] to the one used in
+  /// [addHandler]. If multiple exist, the first one will be removed.
+  /// If none is found, then this method is a no-op.
+  ///
+  /// If used during event dispatching, the removal will not take effect
+  /// until after the event has been dispatched.
+  void removeHandler(KeyEventCallback handler) {
+    if (_duringDispatch) {
+      _modifiedHandlers ??= <KeyEventCallback>[..._handlers];
+      _modifiedHandlers!.remove(handler);
+    } else {
+      _handlers.remove(handler);
+    }
+  }
+
+  bool _dispatchKeyEvent(KeyEvent event) {
+    // This dispatching could have used the same algorithm as [ChangeNotifier],
+    // but since 1) it shouldn't be necessary to support reentrantly
+    // dispatching, 2) there shouldn't be many handlers (most apps should use
+    // only 1, this function just uses a simpler algorithm.
+    assert(!_duringDispatch, 'Nested keyboard dispatching is not supported');
+    _duringDispatch = true;
+    bool handled = false;
+    for (final KeyEventCallback handler in _handlers) {
+      try {
+        final bool thisResult = handler(event);
+        handled = handled || thisResult;
+      } catch (exception, stack) {
+        FlutterError.reportError(FlutterErrorDetails(
+          exception: exception,
+          stack: stack,
+          library: 'services library',
+          context: ErrorDescription('while dispatching notifications for $runtimeType'),
+          informationCollector: () sync* {
+            yield DiagnosticsProperty<HardwareKeyboard>(
+              'The $runtimeType sending notification was',
+              this,
+              style: DiagnosticsTreeStyle.errorProperty,
+            );
+          },
+        ));
+      }
+    }
+    _duringDispatch = false;
+    if (_modifiedHandlers != null) {
+      _handlers = _modifiedHandlers!;
+      _modifiedHandlers = null;
+    }
+    return handled;
+  }
+
+  /// Process a new [KeyEvent] by recording the state changes and dispatching
+  /// to handlers.
+  bool handleKeyEvent(KeyEvent event) {
+    _assertEventIsRegular(event);
+    final PhysicalKeyboardKey physicalKey = event.physicalKey;
+    final LogicalKeyboardKey logicalKey = event.logicalKey;
+    if (event is KeyDownEvent) {
+      _pressedKeys[physicalKey] = logicalKey;
+      final KeyboardLockMode? lockMode = KeyboardLockMode.findLockByLogicalKey(event.logicalKey);
+      if (lockMode != null) {
+        if (_lockModes.contains(lockMode)) {
+          _lockModes.remove(lockMode);
+        } else {
+          _lockModes.add(lockMode);
+        }
+      }
+    } else if (event is KeyUpEvent) {
+      _pressedKeys.remove(physicalKey);
+    } else if (event is KeyRepeatEvent) {
+      // Empty
+    }
+
+    return _dispatchKeyEvent(event);
+  }
+
+  /// Clear all keyboard states and additional handlers.
+  ///
+  /// All handlers are removed except for the first one, which is added by
+  /// [ServicesBinding].
+  ///
+  /// This is used by the testing framework to make sure that tests are hermetic.
+  @visibleForTesting
+  void clearState() {
+    _pressedKeys.clear();
+    _lockModes.clear();
+    _handlers.clear();
+    assert(_modifiedHandlers == null);
+  }
+}
+
+/// The mode in which information of key messages is delivered.
+///
+/// Different platforms use different methods, classes, and models to inform the
+/// framework of native key events, which is called "transit mode".
+///
+/// The framework must determine which transit mode the current platform
+/// implements and behave accordingly (such as transforming and synthesizing
+/// events if necessary). Unit tests related to keyboard might also want to
+/// simulate platform of each transit mode.
+///
+/// The transit mode of the current platform is inferred by [KeyEventManager] at
+/// the start of the application.
+///
+/// See also:
+///
+///  * [KeyEventManager], which infers the transit mode of the current platform
+///    and guides how key messages are dispatched.
+///  * [debugKeyEventSimulatorTransitModeOverride], overrides the transit mode
+///    used to simulate key events.
+///  * [KeySimulatorTransitModeVariant], an easier way to set
+///    [debugKeyEventSimulatorTransitModeOverride] in widget tests.
+enum KeyDataTransitMode {
+  /// Key event information is delivered as raw key data.
+  ///
+  /// Raw key data is platform's native key event information sent in JSON
+  /// through a method channel, which is then interpreted as a platform subclass
+  /// of [RawKeyEventData].
+  ///
+  /// If the current transit mode is [rawKeyData], the raw key data is converted
+  /// to both [KeyMessage.events] and [KeyMessage.rawEvent].
+  rawKeyData,
+
+  /// Key event information is delivered as converted key data, followed
+  /// by raw key data.
+  ///
+  /// Key data ([ui.KeyData]) is a standardized event stream converted from
+  /// platform's native key event information, sent through the embedder
+  /// API. Its event model is described in [HardwareKeyboard].
+  ///
+  /// Raw key data is platform's native key event information sent in JSON
+  /// through a method channel. It is interpreted by subclasses of
+  /// [RawKeyEventData].
+  ///
+  /// If the current transit mode is [rawKeyData], the key data is converted to
+  /// [KeyMessage.events], and the raw key data is converted to
+  /// [KeyMessage.rawEvent].
+  keyDataThenRawKeyData,
+}
+
+/// The assumbled information corresponding to a native key message.
+///
+/// While Flutter's [KeyEvent]s are created from key messages from the native
+/// platform, every native message might result in multiple [KeyEvent]s. For
+/// example, this might happen in order to synthesize missed modifier key
+/// presses or releases.
+/// A [KeyMessage] bundles all information related to a native key message
+/// together for the convenience of propagation on the [FocusNode] tree.
+///
+/// When dispatched to handlers or listeners, or propagated through the
+/// [FocusNode] tree, all handlers or listeners belonging to a node are
+/// executed regardless of their [KeyEventResult], and all results are combined
+/// into the result of the node using [combineKeyEventResults].
+///
+/// In very rare cases, a native key message might not result in a [KeyMessage].
+/// For example, key messages for Fn key are ignored on macOS for the
+/// convenience of cross-platform code.
+@immutable
+class KeyMessage {
+  /// Create a [KeyMessage] by providing all information.
+  ///
+  /// The [events] might be empty.
+  const KeyMessage(this.events, this.rawEvent);
+
+  /// The list of [KeyEvent]s converted from the native key message.
+  ///
+  /// A native key message is converted into multiple [KeyEvent]s in a regular
+  /// event model.  The [events] might contain zero or any number of
+  /// [KeyEvent]s.
+  ///
+  /// See also:
+  ///
+  ///  * [HardwareKeyboard], which describes the regular event model.
+  ///  * [HardwareKeyboard.addHandler], [KeyboardListener], [Focus.onKeyEvent],
+  ///    where [KeyEvent]s are commonly used.
+  final List<KeyEvent> events;
+
+  /// The native key message in the form of a raw key event.
+  ///
+  /// A native key message is sent to the framework in JSON and represented
+  /// in a platform-specific form as [RawKeyEventData] and a platform-neutral
+  /// form as [RawKeyEvent]. Their stream is not as regular as [KeyEvent]'s,
+  /// but keeps as much native information and structure as possible.
+  ///
+  /// The [rawEvent] will be deprecated in the future.
+  ///
+  /// See also:
+  ///
+  ///  * [RawKeyboard.addListener], [RawKeyboardListener], [Focus.onKey],
+  ///    where [RawKeyEvent]s are commonly used.
+  final RawKeyEvent rawEvent;
+
+  @override
+  String toString() {
+    return 'KeyMessage($events)';
+  }
+}
+
+/// The signature for [KeyEventManager.keyMessageHandler].
+///
+/// A [KeyMessageHandler] processes a [KeyMessage] and returns whether
+/// the message is considered handled. Handled messages should not be
+/// propagated to other native components.
+typedef KeyMessageHandler = bool Function(KeyMessage message);
+
+/// A singleton class that processes key messages from the platform and
+/// dispatches converted messages accordingly.
+///
+/// [KeyEventManager] receives platform key messages by [handleKeyData]
+/// and [handleRawKeyMessage], sends converted events to [HardwareKeyboard]
+/// and [RawKeyboard] for recording keeping, and then dispatches the [KeyMessage]
+/// to [keyMessageHandler], the global message handler.
+///
+/// [KeyEventManager] also resolves cross-platform compatibility of keyboard
+/// implementations. Legacy platforms might have not implemented the new key
+/// data API and only send raw key data on each key message. [KeyEventManager]
+/// recognize platform types as [KeyDataTransitMode] and dispatches events in
+/// different ways accordingly.
+///
+/// [KeyEventManager] is typically created, owned, and invoked by
+/// [ServicesBinding].
+class KeyEventManager {
+  /// Create an instance.
+  ///
+  /// This is typically only called by [ServicesBinding].
+  KeyEventManager(this._hardwareKeyboard, this._rawKeyboard);
+
+  /// The global handler for all hardware key messages of Flutter.
+  ///
+  /// Key messages received from the platform are first sent to [RawKeyboard]'s
+  /// listeners and [HardwareKeyboard]'s handlers, then sent to
+  /// [keyMessageHandler], regardless of the results of [HardwareKeyboard]'s
+  /// handlers. The result from the handlers and [keyMessageHandler] are
+  /// combined and returned to the platform. The handler result is explained below.
+  ///
+  /// This handler is normally set by the [FocusManager] so that it can control
+  /// the key event propagation to focused widgets. Applications that use the
+  /// focus system (see [Focus] and [FocusManager]) to receive key events
+  /// do not need to set this field.
+  ///
+  /// ## Handler result
+  ///
+  /// Key messages on the platform are given to Flutter to be handled by the
+  /// engine. If they are not handled, then the platform will continue to
+  /// distribute the keys (i.e. propagate them) to other (possibly non-Flutter)
+  /// components in the application. The return value from this handler tells
+  /// the platform to either stop propagation (by returning true: "event
+  /// handled"), or pass the event on to other controls (false: "event not
+  /// handled"). Some platforms might trigger special alerts if the event
+  /// is not handled by other controls either (such as the "bonk" noise on
+  /// macOS).
+  ///
+  /// If you are not using the [FocusManager] to manage focus, set this
+  /// attribute to a [KeyMessageHandler] that returns true if the propagation
+  /// on the platform should not be continued. Otherwise, key events will be
+  /// assumed to not have been handled by Flutter, and will also be sent to
+  /// other (possibly non-Flutter) controls in the application.
+  ///
+  /// See also:
+  ///
+  ///  * [Focus.onKeyEvent], a [Focus] callback attribute that will be given
+  ///    key events distributed by the [FocusManager] based on the current
+  ///    primary focus.
+  ///  * [HardwareKeyboard.addHandler], which accepts multiple handlers to
+  ///    control the handler result but only accepts [KeyEvent] instead of
+  ///    [KeyMessage].
+  KeyMessageHandler? keyMessageHandler;
+
+  final HardwareKeyboard _hardwareKeyboard;
+  final RawKeyboard _rawKeyboard;
+
+  // The [KeyDataTransitMode] of the current platform.
+  //
+  // The `_transitMode` is guaranteed to be non-null during key event callbacks.
+  //
+  // The `_transitMode` is null before the first event, after which it is inferred
+  // and will not change throughout the lifecycle of the application.
+  //
+  // The `_transitMode` can be reset to null in tests using [clearState].
+  KeyDataTransitMode? _transitMode;
+
+  // The accumulated [KeyEvent]s since the last time the message is dispatched.
+  //
+  // If _transitMode is [KeyDataTransitMode.keyDataThenRawKeyData], then [KeyEvent]s
+  // are directly received from [handleKeyData]. If _transitMode is
+  // [KeyDataTransitMode.rawKeyData], then [KeyEvent]s are converted from the raw
+  // key data of [handleRawKeyMessage]. Either way, the accumulated key
+  // events are only dispatched along with the next [KeyMessage] when a
+  // dispatchable [RawKeyEvent] is available.
+  final List<KeyEvent> _keyEventsSinceLastMessage = <KeyEvent>[];
+
+  /// Dispatch a key data to global and leaf listeners.
+  ///
+  /// This method is the handler to the global `onKeyData` API.
+  bool handleKeyData(ui.KeyData data) {
+    _transitMode ??= KeyDataTransitMode.keyDataThenRawKeyData;
+    switch (_transitMode!) {
+      case KeyDataTransitMode.rawKeyData:
+        assert(false, 'Should never encounter KeyData when transitMode is rawKeyData.');
+        return false;
+      case KeyDataTransitMode.keyDataThenRawKeyData:
+        // Postpone key event dispatching until the handleRawKeyMessage.
+        _keyEventsSinceLastMessage.add(_eventFromData(data));
+        return false;
+    }
+  }
+
+  /// Handles a raw key message.
+  ///
+  /// This method is the handler to [SystemChannels.keyEvent], processing
+  /// the JSON form of the native key message and returns the responds for the
+  /// channel.
+  Future<Map<String, dynamic>> handleRawKeyMessage(dynamic message) async {
+    if (_transitMode == null) {
+      _transitMode = KeyDataTransitMode.rawKeyData;
+      // Convert raw events using a listener so that conversion only occurs if
+      // the raw event should be dispatched.
+      _rawKeyboard.addListener(_convertRawEventAndStore);
+    }
+    final RawKeyEvent rawEvent = RawKeyEvent.fromMessage(message as Map<String, dynamic>);
+    // The following `handleRawKeyEvent` will call `_convertRawEventAndStore`
+    // unless the event is not dispatched.
+    bool handled = _rawKeyboard.handleRawKeyEvent(rawEvent);
+
+    for (final KeyEvent event in _keyEventsSinceLastMessage) {
+      handled = _hardwareKeyboard.handleKeyEvent(event) || handled;
+    }
+    if (_transitMode == KeyDataTransitMode.rawKeyData) {
+      assert(setEquals(_rawKeyboard.physicalKeysPressed, _hardwareKeyboard.physicalKeysPressed),
+        'RawKeyboard reported ${_rawKeyboard.physicalKeysPressed}, '
+        'while HardwareKeyboard reported ${_hardwareKeyboard.physicalKeysPressed}');
+    }
+
+    if (keyMessageHandler != null) {
+      handled = keyMessageHandler!(KeyMessage(_keyEventsSinceLastMessage, rawEvent)) || handled;
+    }
+    _keyEventsSinceLastMessage.clear();
+
+    return <String, dynamic>{ 'handled': handled };
+  }
+
+  // Convert the raw event to key events, including synthesizing events for
+  // modifiers, and store the key events in `_keyEventsSinceLastMessage`.
+  //
+  // This is only called when `_transitMode` is `rawKeyEvent` and if the raw
+  // event should be dispatched.
+  void _convertRawEventAndStore(RawKeyEvent rawEvent) {
+    final PhysicalKeyboardKey physicalKey = rawEvent.physicalKey;
+    final LogicalKeyboardKey logicalKey = rawEvent.logicalKey;
+    final Set<PhysicalKeyboardKey> physicalKeysPressed = _hardwareKeyboard.physicalKeysPressed;
+    final KeyEvent? mainEvent;
+    final LogicalKeyboardKey? recordedLogicalMain = _hardwareKeyboard.lookUpLayout(physicalKey);
+    final Duration timeStamp = ServicesBinding.instance!.currentSystemFrameTimeStamp;
+    final String? character = rawEvent.character == '' ? null : rawEvent.character;
+    if (rawEvent is RawKeyDownEvent) {
+      if (recordedLogicalMain == null) {
+        mainEvent = KeyDownEvent(
+          physicalKey: physicalKey,
+          logicalKey: logicalKey,
+          character: character,
+          timeStamp: timeStamp,
+        );
+        physicalKeysPressed.add(physicalKey);
+      } else {
+        assert(physicalKeysPressed.contains(physicalKey));
+        mainEvent = KeyRepeatEvent(
+          physicalKey: physicalKey,
+          logicalKey: recordedLogicalMain,
+          character: character,
+          timeStamp: timeStamp,
+        );
+      }
+    } else {
+      assert(rawEvent is RawKeyUpEvent, 'Unexpected subclass of RawKeyEvent: ${rawEvent.runtimeType}');
+      if (recordedLogicalMain == null) {
+        mainEvent = null;
+      } else {
+        mainEvent = KeyUpEvent(
+          logicalKey: recordedLogicalMain,
+          physicalKey: physicalKey,
+          timeStamp: timeStamp,
+        );
+        physicalKeysPressed.remove(physicalKey);
+      }
+    }
+    for (final PhysicalKeyboardKey key in physicalKeysPressed.difference(_rawKeyboard.physicalKeysPressed)) {
+      _keyEventsSinceLastMessage.add(KeyUpEvent(
+        physicalKey: key,
+        logicalKey: _hardwareKeyboard.lookUpLayout(physicalKey)!,
+        timeStamp: timeStamp,
+        synthesized: true,
+      ));
+    }
+    for (final PhysicalKeyboardKey key in _rawKeyboard.physicalKeysPressed.difference(physicalKeysPressed)) {
+      _keyEventsSinceLastMessage.add(KeyDownEvent(
+        physicalKey: key,
+        logicalKey: _rawKeyboard.lookUpLayout(physicalKey)!,
+        timeStamp: timeStamp,
+        synthesized: true,
+      ));
+    }
+    if (mainEvent != null)
+      _keyEventsSinceLastMessage.add(mainEvent);
+  }
+
+  /// Reset the inferred platform transit mode and related states.
+  ///
+  /// This method is only used in unit tests. In release mode, this is a no-op.
+  @visibleForTesting
+  void clearState() {
+    assert(() {
+      _transitMode = null;
+      _rawKeyboard.removeListener(_convertRawEventAndStore);
+      _keyEventsSinceLastMessage.clear();
+      return true;
+    }());
+  }
+
+  static KeyEvent _eventFromData(ui.KeyData keyData) {
+    final PhysicalKeyboardKey physicalKey =
+        PhysicalKeyboardKey.findKeyByCode(keyData.physical) ??
+            PhysicalKeyboardKey(keyData.physical);
+    final LogicalKeyboardKey logicalKey =
+        LogicalKeyboardKey.findKeyByKeyId(keyData.logical) ??
+            LogicalKeyboardKey(keyData.logical);
+    final Duration timeStamp = keyData.timeStamp;
+    switch (keyData.type) {
+      case ui.KeyEventType.down:
+        return KeyDownEvent(
+          physicalKey: physicalKey,
+          logicalKey: logicalKey,
+          timeStamp: timeStamp,
+          character: keyData.character,
+          synthesized: keyData.synthesized,
+        );
+      case ui.KeyEventType.up:
+        assert(keyData.character == null);
+        return KeyUpEvent(
+          physicalKey: physicalKey,
+          logicalKey: logicalKey,
+          timeStamp: timeStamp,
+          synthesized: keyData.synthesized,
+        );
+      case ui.KeyEventType.repeat:
+        return KeyRepeatEvent(
+          physicalKey: physicalKey,
+          logicalKey: logicalKey,
+          timeStamp: timeStamp,
+          character: keyData.character,
+        );
+    }
+  }
+}
diff --git a/packages/flutter/lib/src/services/keyboard_key.dart b/packages/flutter/lib/src/services/keyboard_key.dart
index de4d6cd..7262413 100644
--- a/packages/flutter/lib/src/services/keyboard_key.dart
+++ b/packages/flutter/lib/src/services/keyboard_key.dart
@@ -2624,6 +2624,9 @@
   /// See the function [RawKeyEvent.logicalKey] for more information.
   static const LogicalKeyboardKey gameButtonZ = LogicalKeyboardKey(0x0020000031f);
 
+  /// A list of all predefined constant [LogicalKeyboardKey]s.
+  static Iterable<LogicalKeyboardKey> get knownLogicalKeys => _knownLogicalKeys.values;
+
   // A list of all predefined constant LogicalKeyboardKeys so they can be
   // searched.
   static const Map<int, LogicalKeyboardKey> _knownLogicalKeys = <int, LogicalKeyboardKey>{
@@ -5136,6 +5139,9 @@
   /// See the function [RawKeyEvent.physicalKey] for more information.
   static const PhysicalKeyboardKey showAllWindows = PhysicalKeyboardKey(0x000c029f);
 
+  /// A list of all predefined constant [PhysicalKeyboardKey]s.
+  static Iterable<PhysicalKeyboardKey> get knownPhysicalKeys => _knownPhysicalKeys.values;
+
   // A list of all the predefined constant PhysicalKeyboardKeys so that they
   // can be searched.
   static const Map<int, PhysicalKeyboardKey> _knownPhysicalKeys = <int, PhysicalKeyboardKey>{
diff --git a/packages/flutter/lib/src/services/keyboard_maps.dart b/packages/flutter/lib/src/services/keyboard_maps.dart
index d8940f1..74cea74 100644
--- a/packages/flutter/lib/src/services/keyboard_maps.dart
+++ b/packages/flutter/lib/src/services/keyboard_maps.dart
@@ -2953,6 +2953,16 @@
   45: LogicalKeyboardKey.insert,
   46: LogicalKeyboardKey.delete,
   47: LogicalKeyboardKey.help,
+  48: LogicalKeyboardKey.digit0,
+  49: LogicalKeyboardKey.digit1,
+  50: LogicalKeyboardKey.digit2,
+  51: LogicalKeyboardKey.digit3,
+  52: LogicalKeyboardKey.digit4,
+  53: LogicalKeyboardKey.digit5,
+  54: LogicalKeyboardKey.digit6,
+  55: LogicalKeyboardKey.digit7,
+  56: LogicalKeyboardKey.digit8,
+  57: LogicalKeyboardKey.digit9,
   65: LogicalKeyboardKey.keyA,
   66: LogicalKeyboardKey.keyB,
   67: LogicalKeyboardKey.keyC,
diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart
index ccacfea..ba67416 100644
--- a/packages/flutter/lib/src/services/raw_keyboard.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard.dart
@@ -3,10 +3,12 @@
 // found in the LICENSE file.
 
 import 'dart:io';
-import 'dart:ui';
+import 'dart:ui' as ui;
 
 import 'package:flutter/foundation.dart';
 
+import 'binding.dart';
+import 'hardware_keyboard.dart';
 import 'keyboard_key.dart';
 import 'raw_keyboard_android.dart';
 import 'raw_keyboard_fuchsia.dart';
@@ -286,17 +288,20 @@
     final RawKeyEventData data;
     String? character;
 
-    if (kIsWeb) {
+    RawKeyEventData _dataFromWeb() {
       final String? key = message['key'] as String?;
-      data = RawKeyEventDataWeb(
+      if (key != null && key.isNotEmpty) {
+        character = key;
+      }
+      return RawKeyEventDataWeb(
         code: message['code'] as String? ?? '',
         key: key ?? '',
         location: message['location'] as int? ?? 0,
         metaState: message['metaState'] as int? ?? 0,
       );
-      if (key != null && key.isNotEmpty) {
-        character = key;
-      }
+    }
+    if (kIsWeb) {
+      data = _dataFromWeb();
     } else {
       final String keymap = message['keymap'] as String;
       switch (keymap) {
@@ -373,16 +378,7 @@
           }
           break;
         case 'web':
-          final String? key = message['key'] as String?;
-          data = RawKeyEventDataWeb(
-            code: message['code'] as String? ?? '',
-            key: key ?? '',
-            location: message['location'] as int? ?? 0,
-            metaState: message['metaState'] as int? ?? 0,
-          );
-          if (key != null && key.isNotEmpty) {
-            character = key;
-          }
+          data = _dataFromWeb();
           break;
         default:
           /// This exception would only be hit on platforms that haven't yet
@@ -573,9 +569,7 @@
 ///  * [SystemChannels.keyEvent], the low-level channel used for receiving
 ///    events from the system.
 class RawKeyboard {
-  RawKeyboard._() {
-    SystemChannels.keyEvent.setMessageHandler(_handleKeyEvent);
-  }
+  RawKeyboard._();
 
   /// The shared instance of [RawKeyboard].
   static final RawKeyboard instance = RawKeyboard._();
@@ -606,39 +600,45 @@
     _listeners.remove(listener);
   }
 
-  /// A handler for hardware keyboard events that will stop propagation if the
-  /// handler returns true.
+  /// A handler for raw hardware keyboard events that will stop propagation if
+  /// the handler returns true.
   ///
-  /// Key events on the platform are given to Flutter to be handled by the
-  /// engine. If they are not handled, then the platform will continue to
-  /// distribute the keys (i.e. propagate them) to other (possibly non-Flutter)
-  /// components in the application. The return value from this handler tells
-  /// the platform to either stop propagation (by returning true: "event
-  /// handled"), or pass the event on to other controls (false: "event not
-  /// handled").
-  ///
-  /// This handler is normally set by the [FocusManager] so that it can control
-  /// the key event propagation to focused widgets.
-  ///
-  /// Most applications can use the focus system (see [Focus] and
-  /// [FocusManager]) to receive key events. If you are not using the
-  /// [FocusManager] to manage focus, then to be able to stop propagation of the
-  /// event by indicating that the event was handled, set this attribute to a
-  /// [RawKeyEventHandler]. Otherwise, key events will be assumed to not have
-  /// been handled by Flutter, and will also be sent to other (possibly
-  /// non-Flutter) controls in the application.
-  ///
-  /// See also:
-  ///
-  ///  * [Focus.onKey], a [Focus] callback attribute that will be given key
-  ///    events distributed by the [FocusManager] based on the current primary
-  ///    focus.
-  ///  * [addListener], to add passive key event listeners that do not stop event
-  ///    propagation.
-  RawKeyEventHandler? keyEventHandler;
+  /// This property is only a wrapper over [KeyEventManager.keyMessageHandler],
+  /// and is kept only for backward compatibility. New code should use
+  /// [KeyEventManager.keyMessageHandler] to set custom global key event
+  /// handler.  Setting [keyEventHandler] will cause
+  /// [KeyEventManager.keyMessageHandler] to be set with a converted handler.
+  /// If [KeyEventManager.keyMessageHandler] is set by [FocusManager] (the most
+  /// common situation), then the exact value of [keyEventHandler] is a dummy
+  /// callback and must not be invoked.
+  RawKeyEventHandler? get keyEventHandler {
+    if (ServicesBinding.instance!.keyEventManager.keyMessageHandler != _cachedKeyMessageHandler) {
+      _cachedKeyMessageHandler = ServicesBinding.instance!.keyEventManager.keyMessageHandler;
+      _cachedKeyEventHandler = _cachedKeyMessageHandler == null ?
+        null :
+        (RawKeyEvent event) {
+          assert(false,
+              'The RawKeyboard.instance.keyEventHandler assigned by Flutter is a dummy '
+              'callback kept for compatibility and should not be directly called. Use '
+              'ServicesBinding.instance!.keyMessageHandler instead.');
+          return true;
+        };
+    }
+    return _cachedKeyEventHandler;
+  }
+  RawKeyEventHandler? _cachedKeyEventHandler;
+  KeyMessageHandler? _cachedKeyMessageHandler;
+  set keyEventHandler(RawKeyEventHandler? handler) {
+    _cachedKeyEventHandler = handler;
+    _cachedKeyMessageHandler = handler == null ?
+      null :
+      (KeyMessage message) => handler(message.rawEvent);
+    ServicesBinding.instance!.keyEventManager.keyMessageHandler = _cachedKeyMessageHandler;
+  }
 
-  Future<dynamic> _handleKeyEvent(dynamic message) async {
-    final RawKeyEvent event = RawKeyEvent.fromMessage(message as Map<String, dynamic>);
+  /// Process a new [RawKeyEvent] by recording the state changes and
+  /// dispatching to listeners.
+  bool handleRawKeyEvent(RawKeyEvent event) {
     bool shouldDispatch = true;
     if (event is RawKeyDownEvent) {
       if (event.data.shouldDispatchEvent()) {
@@ -659,7 +659,7 @@
       }
     }
     if (!shouldDispatch) {
-      return <String, dynamic>{ 'handled': true };
+      return true;
     }
     // Make sure that the modifiers reflect reality, in case a modifier key was
     // pressed/released while the app didn't have focus.
@@ -678,12 +678,7 @@
       }
     }
 
-    // Send the key event to the keyEventHandler, then send the appropriate
-    // response to the platform so that it can resolve the event's handling.
-    // Defaults to false if keyEventHandler is null.
-    final bool handled = keyEventHandler != null && keyEventHandler!(event);
-    assert(handled != null, 'keyEventHandler returned null, which is not allowed');
-    return <String, dynamic>{ 'handled': handled };
+    return false;
   }
 
   static final Map<_ModifierSidePair, Set<PhysicalKeyboardKey>> _modifierKeyMap = <_ModifierSidePair, Set<PhysicalKeyboardKey>>{
@@ -808,6 +803,11 @@
   /// Returns the set of physical keys currently pressed.
   Set<PhysicalKeyboardKey> get physicalKeysPressed => _keysPressed.keys.toSet();
 
+  /// Returns the logical key that corresponds to the given pressed physical key.
+  ///
+  /// Returns null if the physical key is not currently pressed.
+  LogicalKeyboardKey? lookUpLayout(PhysicalKeyboardKey physicalKey) => _keysPressed[physicalKey];
+
   /// Clears the list of keys returned from [keysPressed].
   ///
   /// This is used by the testing framework to make sure tests are hermetic.
@@ -832,5 +832,5 @@
   }
 
   @override
-  int get hashCode => hashValues(modifier, side);
+  int get hashCode => ui.hashValues(modifier, side);
 }
diff --git a/packages/flutter/lib/src/services/raw_keyboard_web.dart b/packages/flutter/lib/src/services/raw_keyboard_web.dart
index 257346d..70e2b94 100644
--- a/packages/flutter/lib/src/services/raw_keyboard_web.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard_web.dart
@@ -83,13 +83,11 @@
 
   @override
   LogicalKeyboardKey get logicalKey {
-    // Look to see if the keyCode is a printable number pad key, so that a
-    // difference between regular keys (e.g. ".") and the number pad version
-    // (e.g. the "." on the number pad) can be determined.
-    final LogicalKeyboardKey? numPadKey = kWebNumPadMap[code];
-    if (numPadKey != null) {
-      return numPadKey;
-    }
+    // Look to see if the keyCode is a key based on location. Typically they are
+    // numpad keys (versus main area keys) and left/right modifiers.
+    final LogicalKeyboardKey? maybeLocationKey = kWebLocationMap[key]?[location];
+    if (maybeLocationKey != null)
+      return maybeLocationKey;
 
     // Look to see if the [code] is one we know about and have a mapping for.
     final LogicalKeyboardKey? newKey = kWebToLogicalKey[code];
diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart
index 6c442f7..cfd2100 100644
--- a/packages/flutter/lib/src/widgets/focus_manager.dart
+++ b/packages/flutter/lib/src/widgets/focus_manager.dart
@@ -36,7 +36,7 @@
 }
 
 /// An enum that describes how to handle a key event handled by a
-/// [FocusOnKeyCallback].
+/// [FocusOnKeyCallback] or [FocusOnKeyEventCallback].
 enum KeyEventResult {
   /// The key event has been handled, and the event should not be propagated to
   /// other key event handlers.
@@ -52,6 +52,32 @@
   skipRemainingHandlers,
 }
 
+/// Combine the results returned by multiple [FocusOnKeyCallback]s or
+/// [FocusOnKeyEventCallback]s.
+///
+/// If any callback returns [KeyEventResult.handled], the node considers the
+/// message handled; otherwise, if any callback returns
+/// [KeyEventResult.skipRemainingHandlers], the node skips the remaining
+/// handlers without preventing the platform to handle; otherwise the node is
+/// ignored.
+KeyEventResult combineKeyEventResults(Iterable<KeyEventResult> results) {
+  bool hasSkipRemainingHandlers = false;
+  for (final KeyEventResult result in results) {
+    switch (result) {
+      case KeyEventResult.handled:
+        return KeyEventResult.handled;
+      case KeyEventResult.skipRemainingHandlers:
+        hasSkipRemainingHandlers = true;
+        break;
+      default:
+        break;
+    }
+  }
+  return hasSkipRemainingHandlers ?
+      KeyEventResult.skipRemainingHandlers :
+      KeyEventResult.ignored;
+}
+
 /// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey]
 /// to receive key events.
 ///
@@ -61,6 +87,15 @@
 /// was handled.
 typedef FocusOnKeyCallback = KeyEventResult Function(FocusNode node, RawKeyEvent event);
 
+/// Signature of a callback used by [Focus.onKeyEvent] and [FocusScope.onKeyEvent]
+/// to receive key events.
+///
+/// The [node] is the node that received the event.
+///
+/// Returns a [KeyEventResult] that describes how, and whether, the key event
+/// was handled.
+typedef FocusOnKeyEventCallback = KeyEventResult Function(FocusNode node, KeyEvent event);
+
 /// An attachment point for a [FocusNode].
 ///
 /// Using a [FocusAttachment] is rarely needed, unless you are building
@@ -271,12 +306,13 @@
 /// {@template flutter.widgets.FocusNode.keyEvents}
 /// ## Key Event Propagation
 ///
-/// The [FocusManager] receives key events from [RawKeyboard] and will pass them
-/// to the focused nodes. It starts with the node with the primary focus, and
-/// will call the [onKey] callback for that node. If the callback returns false,
-/// indicating that it did not handle the event, the [FocusManager] will move to
-/// the parent of that node and call its [onKey]. If that [onKey] returns true,
-/// then it will stop propagating the event. If it reaches the root
+/// The [FocusManager] receives key events from [RawKeyboard] and
+/// [HardwareKeyboard] and will pass them to the focused nodes. It starts with
+/// the node with the primary focus, and will call the [onKey] or [onKeyEvent]
+/// callback for that node. If the callback returns false, indicating that it did
+/// not handle the event, the [FocusManager] will move to the parent of that node
+/// and call its [onKey] or [onKeyEvent]. If that [onKey] or [onKeyEvent] returns
+/// true, then it will stop propagating the event. If it reaches the root
 /// [FocusScopeNode], [FocusManager.rootScope], the event is discarded.
 /// {@endtemplate}
 ///
@@ -433,9 +469,14 @@
   ///
   /// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus]
   /// arguments must not be null.
+  ///
+  /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
+  /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
+  /// in the future.
   FocusNode({
     String? debugLabel,
     this.onKey,
+    this.onKeyEvent,
     bool skipTraversal = false,
     bool canRequestFocus = true,
     bool descendantsAreFocusable = true,
@@ -574,9 +615,18 @@
   /// Called if this focus node receives a key event while focused (i.e. when
   /// [hasFocus] returns true).
   ///
+  /// This is a legacy API based on [RawKeyEvent] and will be deprecated in the
+  /// future. Prefer [onKeyEvent] instead.
+  ///
   /// {@macro flutter.widgets.FocusNode.keyEvents}
   FocusOnKeyCallback? onKey;
 
+  /// Called if this focus node receives a key event while focused (i.e. when
+  /// [hasFocus] returns true).
+  ///
+  /// {@macro flutter.widgets.FocusNode.keyEvents}
+  FocusOnKeyEventCallback? onKeyEvent;
+
   FocusManager? _manager;
   List<FocusNode>? _ancestors;
   List<FocusNode>? _descendants;
@@ -1028,10 +1078,19 @@
   /// need to be attached. [FocusAttachment.detach] should be called on the old
   /// node, and then [attach] called on the new node. This typically happens in
   /// the [State.didUpdateWidget] method.
+  ///
+  /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
+  /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
+  /// in the future.
   @mustCallSuper
-  FocusAttachment attach(BuildContext? context, {FocusOnKeyCallback? onKey}) {
+  FocusAttachment attach(
+    BuildContext? context, {
+    FocusOnKeyEventCallback? onKeyEvent,
+    FocusOnKeyCallback? onKey,
+  }) {
     _context = context;
     this.onKey = onKey ?? this.onKey;
+    this.onKeyEvent = onKeyEvent ?? this.onKeyEvent;
     _attachment = FocusAttachment._(this);
     return _attachment!;
   }
@@ -1225,6 +1284,7 @@
   /// All parameters are optional.
   FocusScopeNode({
     String? debugLabel,
+    FocusOnKeyEventCallback? onKeyEvent,
     FocusOnKeyCallback? onKey,
     bool skipTraversal = false,
     bool canRequestFocus = true,
@@ -1232,6 +1292,7 @@
         assert(canRequestFocus != null),
         super(
           debugLabel: debugLabel,
+          onKeyEvent: onKeyEvent,
           onKey: onKey,
           canRequestFocus: canRequestFocus,
           descendantsAreFocusable: true,
@@ -1463,15 +1524,15 @@
   /// When this focus manager is no longer needed, calling [dispose] on it will
   /// unregister these handlers.
   void registerGlobalHandlers() {
-    assert(RawKeyboard.instance.keyEventHandler == null);
-    RawKeyboard.instance.keyEventHandler = _handleRawKeyEvent;
+    assert(ServicesBinding.instance!.keyEventManager.keyMessageHandler == null);
+    ServicesBinding.instance!.keyEventManager.keyMessageHandler = _handleKeyMessage;
     GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
   }
 
   @override
   void dispose() {
-    if (RawKeyboard.instance.keyEventHandler == _handleRawKeyEvent) {
-      RawKeyboard.instance.keyEventHandler = null;
+    if (ServicesBinding.instance!.keyEventManager.keyMessageHandler == _handleKeyMessage) {
+      ServicesBinding.instance!.keyEventManager.keyMessageHandler = null;
       GestureBinding.instance!.pointerRouter.removeGlobalRoute(_handlePointerEvent);
     }
     super.dispose();
@@ -1660,15 +1721,15 @@
     }
   }
 
-  bool _handleRawKeyEvent(RawKeyEvent event) {
+  bool _handleKeyMessage(KeyMessage message) {
     // Update highlightMode first, since things responding to the keys might
     // look at the highlight mode, and it should be accurate.
     _lastInteractionWasTouch = false;
     _updateHighlightMode();
 
-    assert(_focusDebug('Received key event ${event.logicalKey}'));
+    assert(_focusDebug('Received key event $message'));
     if (_primaryFocus == null) {
-      assert(_focusDebug('No primary focus for key event, ignored: $event'));
+      assert(_focusDebug('No primary focus for key event, ignored: $message'));
       return false;
     }
 
@@ -1677,25 +1738,35 @@
     // stop propagation, stop.
     bool handled = false;
     for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) {
-      if (node.onKey != null) {
-        final KeyEventResult result = node.onKey!(node, event);
-        switch (result) {
-          case KeyEventResult.handled:
-            assert(_focusDebug('Node $node handled key event $event.'));
-            handled = true;
-            break;
-          case KeyEventResult.skipRemainingHandlers:
-            assert(_focusDebug('Node $node stopped key event propagation: $event.'));
-            handled = false;
-            break;
-          case KeyEventResult.ignored:
-            continue;
+      final List<KeyEventResult> results = <KeyEventResult>[];
+      if (node.onKeyEvent != null) {
+        for (final KeyEvent event in message.events) {
+          results.add(node.onKeyEvent!(node, event));
         }
-        break;
       }
+      if (node.onKey != null) {
+        results.add(node.onKey!(node, message.rawEvent));
+      }
+      final KeyEventResult result = combineKeyEventResults(results);
+      switch (result) {
+        case KeyEventResult.ignored:
+          continue;
+        case KeyEventResult.handled:
+          assert(_focusDebug('Node $node handled key event $message.'));
+          handled = true;
+          break;
+        case KeyEventResult.skipRemainingHandlers:
+          assert(_focusDebug('Node $node stopped key event propagation: $message.'));
+          handled = false;
+          break;
+      }
+      // Only KeyEventResult.ignored will continue the for loop.  All other
+      // options will stop the event propagation.
+      assert(result != KeyEventResult.ignored);
+      break;
     }
     if (!handled) {
-      assert(_focusDebug('Key event not handled by anyone: $event.'));
+      assert(_focusDebug('Key event not handled by anyone: $message.'));
     }
     return handled;
   }
diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart
index 25c171e..673429a 100644
--- a/packages/flutter/lib/src/widgets/focus_scope.dart
+++ b/packages/flutter/lib/src/widgets/focus_scope.dart
@@ -283,6 +283,7 @@
     this.autofocus = false,
     this.onFocusChange,
     this.onKey,
+    this.onKeyEvent,
     this.debugLabel,
     this.canRequestFocus,
     this.descendantsAreFocusable = true,
@@ -315,6 +316,24 @@
   /// focus.
   ///
   /// Key events are first given to the [FocusNode] that has primary focus, and
+  /// if its [onKeyEvent] method return false, then they are given to each
+  /// ancestor node up the focus hierarchy in turn. If an event reaches the root
+  /// of the hierarchy, it is discarded.
+  ///
+  /// This is not the way to get text input in the manner of a text field: it
+  /// leaves out support for input method editors, and doesn't support soft
+  /// keyboards in general. For text input, consider [TextField],
+  /// [EditableText], or [CupertinoTextField] instead, which do support these
+  /// things.
+  final FocusOnKeyEventCallback? onKeyEvent;
+
+  /// Handler for keys pressed when this object or one of its children has
+  /// focus.
+  ///
+  /// This is a legacy API based on [RawKeyEvent] and will be deprecated in the
+  /// future. Prefer [onKeyEvent] instead.
+  ///
+  /// Key events are first given to the [FocusNode] that has primary focus, and
   /// if its [onKey] method return false, then they are given to each ancestor
   /// node up the focus hierarchy in turn. If an event reaches the root of the
   /// hierarchy, it is discarded.
@@ -540,7 +559,7 @@
   }
 
   @override
-  State<Focus>  createState() => _FocusState();
+  State<Focus> createState() => _FocusState();
 }
 
 class _FocusState extends State<Focus> {
@@ -575,7 +594,7 @@
     _canRequestFocus = focusNode.canRequestFocus;
     _descendantsAreFocusable = focusNode.descendantsAreFocusable;
     _hasPrimaryFocus = focusNode.hasPrimaryFocus;
-    _focusAttachment = focusNode.attach(context, onKey: widget.onKey);
+    _focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
 
     // Add listener even if the _internalNode existed before, since it should
     // not be listening now if we're re-using a previous one because it should
@@ -914,6 +933,7 @@
     ValueChanged<bool>? onFocusChange,
     bool? canRequestFocus,
     bool? skipTraversal,
+    FocusOnKeyEventCallback? onKeyEvent,
     FocusOnKeyCallback? onKey,
     String? debugLabel,
   })  : assert(child != null),
@@ -926,6 +946,7 @@
           onFocusChange: onFocusChange,
           canRequestFocus: canRequestFocus,
           skipTraversal: skipTraversal,
+          onKeyEvent: onKeyEvent,
           onKey: onKey,
           debugLabel: debugLabel,
         );
diff --git a/packages/flutter/lib/src/widgets/keyboard_listener.dart b/packages/flutter/lib/src/widgets/keyboard_listener.dart
new file mode 100644
index 0000000..6c1bb82
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/keyboard_listener.dart
@@ -0,0 +1,95 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'focus_manager.dart';
+import 'focus_scope.dart';
+import 'framework.dart';
+
+export 'package:flutter/services.dart' show KeyEvent;
+
+/// A widget that calls a callback whenever the user presses or releases a key
+/// on a keyboard.
+///
+/// A [KeyboardListener] is useful for listening to key events and
+/// hardware buttons that are represented as keys. Typically used by games and
+/// other apps that use keyboards for purposes other than text entry.
+///
+/// For text entry, consider using a [EditableText], which integrates with
+/// on-screen keyboards and input method editors (IMEs).
+///
+/// The [KeyboardListener] is different from [RawKeyboardListener] in that
+/// [KeyboardListener] uses the newer [HardwareKeyboard] API, which is
+/// preferrable.
+///
+/// See also:
+///
+///  * [EditableText], which should be used instead of this widget for text
+///    entry.
+///  * [RawKeyboardListener], a similar widget based on the old [RawKeyboard]
+///    API.
+class KeyboardListener extends StatelessWidget {
+  /// Creates a widget that receives  keyboard events.
+  ///
+  /// For text entry, consider using a [EditableText], which integrates with
+  /// on-screen keyboards and input method editors (IMEs).
+  ///
+  /// The [focusNode] and [child] arguments are required and must not be null.
+  ///
+  /// The [autofocus] argument must not be null.
+  ///
+  /// The `key` is an identifier for widgets, and is unrelated to keyboards.
+  /// See [Widget.key].
+  const KeyboardListener({
+    Key? key,
+    required this.focusNode,
+    this.autofocus = false,
+    this.includeSemantics = true,
+    this.onKeyEvent,
+    required this.child,
+  }) : assert(focusNode != null),
+       assert(autofocus != null),
+       assert(includeSemantics != null),
+       assert(child != null),
+       super(key: key);
+
+  /// Controls whether this widget has keyboard focus.
+  final FocusNode focusNode;
+
+  /// {@macro flutter.widgets.Focus.autofocus}
+  final bool autofocus;
+
+  /// {@macro flutter.widgets.Focus.includeSemantics}
+  final bool includeSemantics;
+
+  /// Called whenever this widget receives a keyboard event.
+  final ValueChanged<KeyEvent>? onKeyEvent;
+
+  /// The widget below this widget in the tree.
+  ///
+  /// {@macro flutter.widgets.ProxyWidget.child}
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    return Focus(
+      focusNode: focusNode,
+      autofocus: autofocus,
+      includeSemantics: includeSemantics,
+      onKeyEvent: (FocusNode node, KeyEvent event) {
+        onKeyEvent?.call(event);
+        return KeyEventResult.ignored;
+      },
+      child: child,
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
+  }
+}
diff --git a/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart b/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
index 21d7e33..db8f5be 100644
--- a/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
+++ b/packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
@@ -21,10 +21,16 @@
 /// For text entry, consider using a [EditableText], which integrates with
 /// on-screen keyboards and input method editors (IMEs).
 ///
+/// The [RawKeyboardListener] is different from [KeyboardListener] in that
+/// [RawKeyboardListener] uses the legacy [RawKeyboard] API. Use
+/// [KeyboardListener] if possible.
+///
 /// See also:
 ///
 ///  * [EditableText], which should be used instead of this widget for text
 ///    entry.
+///  * [KeyboardListener], a similar widget based on the newer
+///    [HardwareKeyboard] API.
 class RawKeyboardListener extends StatefulWidget {
   /// Creates a widget that receives raw keyboard events.
   ///
diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart
index 36297c8..941f7e9 100644
--- a/packages/flutter/lib/src/widgets/shortcuts.dart
+++ b/packages/flutter/lib/src/widgets/shortcuts.dart
@@ -194,13 +194,13 @@
   /// event.
   ///
   /// For example, for `Ctrl-A`, it has to check if the event is a
-  /// [RawKeyDownEvent], if either side of the Ctrl key is pressed, and none of
+  /// [KeyDownEvent], if either side of the Ctrl key is pressed, and none of
   /// the Shift keys, Alt keys, or Meta keys are pressed; it doesn't have to
   /// check if KeyA is pressed, since it's already guaranteed.
   ///
   /// This method must not cause any side effects for the `state`. Typically
-  /// this is only used to query whether [RawKeyboard.keysPressed] contains
-  /// a key.
+  /// this is only used to query whether [HardwareKeyboard.logicalKeysPressed]
+  /// contains a key.
   ///
   /// Since [ShortcutActivator] accepts all event types, subclasses might want
   /// to check the event type in [accepts].
@@ -314,11 +314,13 @@
 
   @override
   bool accepts(RawKeyEvent event, RawKeyboard state) {
+    if (event is! RawKeyDownEvent)
+      return false;
     final Set<LogicalKeyboardKey> collapsedRequired = LogicalKeyboardKey.collapseSynonyms(keys);
     final Set<LogicalKeyboardKey> collapsedPressed = LogicalKeyboardKey.collapseSynonyms(state.keysPressed);
     final bool keysEqual = collapsedRequired.difference(collapsedPressed).isEmpty
       && collapsedRequired.length == collapsedPressed.length;
-    return event is RawKeyDownEvent && keysEqual;
+    return keysEqual;
   }
 
   static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
@@ -425,7 +427,8 @@
 ///  * [CharacterActivator], an activator that represents key combinations
 ///    that result in the specified character, such as question mark.
 class SingleActivator with Diagnosticable implements ShortcutActivator {
-  /// Create an activator of a trigger key and modifiers.
+  /// Triggered when the [trigger] key is pressed or repeated when the
+  /// modifiers are held.
   ///
   /// The `trigger` should be the non-modifier key that is pressed after all the
   /// modifiers, such as [LogicalKeyboardKey.keyC] as in `Ctrl+C`. It must not be
@@ -434,6 +437,9 @@
   /// The `control`, `shift`, `alt`, and `meta` flags represent whether
   /// the respect modifier keys should be held (true) or released (false)
   ///
+  /// On each [RawKeyDownEvent] of the [trigger] key, this activator checks
+  /// whether the specified modifier conditions are met.
+  ///
   /// {@tool dartpad --template=stateful_widget_scaffold_center}
   /// In the following example, the shortcut `Control + C` increases the counter:
   ///
@@ -811,17 +817,7 @@
   /// must be mapped to an [Action], and the [Action] must be enabled.
   @protected
   KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
-    if (event is! RawKeyDownEvent) {
-      return KeyEventResult.ignored;
-    }
     assert(context != null);
-    assert(
-      RawKeyboard.instance.keysPressed.isNotEmpty,
-      'Received a key down event when no keys are in keysPressed. '
-      "This state can occur if the key event being sent doesn't properly "
-      'set its modifier flags. This was the event: $event and its data: '
-      '${event.data}',
-    );
     final Intent? matchedIntent = _find(event, RawKeyboard.instance);
     if (matchedIntent != null) {
       final BuildContext? primaryContext = primaryFocus?.context;
@@ -1285,4 +1281,4 @@
       child: child,
     );
   }
-}
\ No newline at end of file
+}
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index c4205df..ed09312 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -63,6 +63,7 @@
 export 'src/widgets/inherited_notifier.dart';
 export 'src/widgets/inherited_theme.dart';
 export 'src/widgets/interactive_viewer.dart';
+export 'src/widgets/keyboard_listener.dart';
 export 'src/widgets/layout_builder.dart';
 export 'src/widgets/list_wheel_scroll_view.dart';
 export 'src/widgets/localizations.dart';
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index 085a180..ed2c875 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -4348,7 +4348,7 @@
     await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
     await tester.pump();
     expect(focusNode3.hasPrimaryFocus, isTrue);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Scrolling shortcuts are disabled in text fields', (WidgetTester tester) async {
     bool scrollInvoked = false;
@@ -4381,7 +4381,7 @@
 
     await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
     expect(scrollInvoked, isFalse);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Cupertino text field semantics', (WidgetTester tester) async {
     await tester.pumpWidget(
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index d1e5255..176094c 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -4731,7 +4731,7 @@
       await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
       await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
       expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Shift test 2', (WidgetTester tester) async {
       await setupWidget(tester);
@@ -4749,7 +4749,7 @@
       await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
       await tester.pumpAndSettle();
       expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Control Shift test', (WidgetTester tester) async {
       await setupWidget(tester);
@@ -4766,7 +4766,7 @@
       await tester.pumpAndSettle();
 
       expect(controller.selection.extentOffset - controller.selection.baseOffset, 5);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Down and up test', (WidgetTester tester) async {
       await setupWidget(tester);
@@ -4793,7 +4793,7 @@
       await tester.pumpAndSettle();
 
       expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Down and up test 2', (WidgetTester tester) async {
       await setupWidget(tester);
@@ -4849,7 +4849,7 @@
       await tester.pumpAndSettle();
 
       expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Read only keyboard selection test', (WidgetTester tester) async {
       final TextEditingController controller = TextEditingController(text: 'readonly');
@@ -4869,7 +4869,7 @@
       await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
       await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
       expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
   }, skip: areKeyEventsHandledByPlatform);
 
   testWidgets('Copy paste test', (WidgetTester tester) async {
@@ -4944,7 +4944,7 @@
 
     const String expected = 'a biga big house\njumped over a mouse';
     expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Copy paste obscured text test', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -5018,7 +5018,7 @@
 
     const String expected = 'a biga big house jumped over a mouse';
     expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   // Regressing test for https://github.com/flutter/flutter/issues/78219
   testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
@@ -5069,7 +5069,7 @@
     // Do nothing.
     expect(find.text(clipboardContent), findsNothing);
     expect(controller.selection, const TextSelection.collapsed(offset: -1));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Cut test', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -5145,7 +5145,7 @@
 
     const String expected = ' housa bige\njumped over a mouse';
     expect(find.text(expected), findsOneWidget);
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Cut obscured text test', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -5220,7 +5220,7 @@
 
     const String expected = ' housa bige jumped over a mouse';
     expect(find.text(expected), findsOneWidget);
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Select all test', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -5269,7 +5269,7 @@
 
     const String expected = '';
     expect(find.text(expected), findsOneWidget);
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Delete test', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -5321,7 +5321,7 @@
 
     const String expected2 = '';
     expect(find.text(expected2), findsOneWidget);
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Changing positions of text fields', (WidgetTester tester) async {
 
@@ -5413,7 +5413,7 @@
     await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
 
     expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
 
   testWidgets('Changing focus test', (WidgetTester tester) async {
@@ -5488,7 +5488,7 @@
 
     expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
     expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
-  }, skip: areKeyEventsHandledByPlatform);
+  }, skip: areKeyEventsHandledByPlatform, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
     final TextEditingController controller = TextEditingController();
diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart
index ff11a53..d68bcae 100644
--- a/packages/flutter/test/rendering/editable_test.dart
+++ b/packages/flutter/test/rendering/editable_test.dart
@@ -16,6 +16,11 @@
 import 'package:flutter/rendering.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:meta/meta.dart';
+
+// The test_api package is not for general use... it's literally for our use.
+// ignore: deprecated_member_use
+import 'package:test_api/test_api.dart' as test_package;
 
 import '../rendering/mock_canvas.dart';
 import '../rendering/recording_canvas.dart';
@@ -35,6 +40,38 @@
   void bringIntoView(TextPosition position) { }
 }
 
+@isTest
+void testVariants(
+  String description,
+  AsyncValueGetter<void> callback, {
+  bool? skip,
+  test_package.Timeout? timeout,
+  TestVariant<Object?> variant = const DefaultTestVariant(),
+  dynamic tags,
+}) {
+  assert(variant != null);
+  assert(variant.values.isNotEmpty, 'There must be at least one value to test in the testing variant.');
+  for (final dynamic value in variant.values) {
+    final String variationDescription = variant.describeValue(value);
+    final String combinedDescription = variationDescription.isNotEmpty ? '$description ($variationDescription)' : description;
+    test(
+      combinedDescription,
+      () async {
+        Object? memento;
+        try {
+          memento = await variant.setUp(value);
+          await callback();
+        } finally {
+          await variant.tearDown(value, memento);
+        }
+      },
+      skip: skip,
+      timeout: timeout,
+      tags: tags,
+    );
+  }
+}
+
 void main() {
   test('RenderEditable respects clipBehavior', () {
     const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
@@ -1343,7 +1380,7 @@
     expect(currentSelection.extentOffset, 1);
   }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
 
-  test('respects enableInteractiveSelection', () async {
+  testVariants('respects enableInteractiveSelection', () async {
     const String text = '012345';
     final TextSelectionDelegate delegate = FakeEditableTextState()
       ..textEditingValue = const TextEditingValue(text: text);
@@ -1403,7 +1440,7 @@
 
     await simulateKeyUpEvent(wordModifier);
     await simulateKeyUpEvent(LogicalKeyboardKey.shift);
-  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
+  }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/58068
 
   group('delete', () {
     test('when as a non-collapsed selection, it should delete a selection', () async {
diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart
new file mode 100644
index 0000000..634a289
--- /dev/null
+++ b/packages/flutter/test/services/hardware_keyboard_test.dart
@@ -0,0 +1,165 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('HardwareKeyboard records pressed keys and enabled locks', (WidgetTester tester) async {
+    await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+    await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+    await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+    await simulateKeyUpEvent(LogicalKeyboardKey.numLock);
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+
+    await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{}));
+
+    await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{}));
+
+    await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows');
+    expect(HardwareKeyboard.instance.physicalKeysPressed,
+      equals(<PhysicalKeyboardKey>{}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed,
+      equals(<LogicalKeyboardKey>{}));
+    expect(HardwareKeyboard.instance.lockModesEnabled,
+      equals(<KeyboardLockMode>{}));
+  }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());
+
+  testWidgets('Dispatch events to all handlers', (WidgetTester tester) async {
+    final FocusNode focusNode = FocusNode();
+    final List<int> logs = <int>[];
+
+    await tester.pumpWidget(
+      KeyboardListener(
+        autofocus: true,
+        focusNode: focusNode,
+        child: Container(),
+        onKeyEvent: (KeyEvent event) {
+          logs.add(1);
+        },
+      ),
+    );
+
+    // Only the Service binding handler.
+
+    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+      false);
+    expect(logs, <int>[1]);
+    logs.clear();
+
+    // Add a handler.
+
+    bool handler2Result = false;
+    bool handler2(KeyEvent event) {
+      logs.add(2);
+      return handler2Result;
+    }
+    HardwareKeyboard.instance.addHandler(handler2);
+
+    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+      false);
+    expect(logs, <int>[2, 1]);
+    logs.clear();
+
+    handler2Result = true;
+
+    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+      true);
+    expect(logs, <int>[2, 1]);
+    logs.clear();
+
+    // Add another handler.
+
+    handler2Result = false;
+    bool handler3Result = false;
+    bool handler3(KeyEvent event) {
+      logs.add(3);
+      return handler3Result;
+    }
+    HardwareKeyboard.instance.addHandler(handler3);
+
+    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+      false);
+    expect(logs, <int>[2, 3, 1]);
+    logs.clear();
+
+    handler2Result = true;
+
+    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+      true);
+    expect(logs, <int>[2, 3, 1]);
+    logs.clear();
+
+    handler3Result = true;
+
+    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+      true);
+    expect(logs, <int>[2, 3, 1]);
+    logs.clear();
+
+    // Add handler2 again.
+
+    HardwareKeyboard.instance.addHandler(handler2);
+
+    handler3Result = false;
+    handler2Result = false;
+    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+      false);
+    expect(logs, <int>[2, 3, 2, 1]);
+    logs.clear();
+
+    handler2Result = true;
+    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+      true);
+    expect(logs, <int>[2, 3, 2, 1]);
+    logs.clear();
+
+    // Remove handler2 once.
+
+    HardwareKeyboard.instance.removeHandler(handler2);
+    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+      true);
+    expect(logs, <int>[3, 2, 1]);
+    logs.clear();
+  }, variant: KeySimulatorTransitModeVariant.all());
+}
diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart
index 44dc576..f5c227c 100644
--- a/packages/flutter/test/services/raw_keyboard_test.dart
+++ b/packages/flutter/test/services/raw_keyboard_test.dart
@@ -703,6 +703,70 @@
         )),
       );
     });
+
+    testWidgets('Dispatch events to all handlers', (WidgetTester tester) async {
+      final FocusNode focusNode = FocusNode();
+      final List<int> logs = <int>[];
+
+      await tester.pumpWidget(
+        RawKeyboardListener(
+          autofocus: true,
+          focusNode: focusNode,
+          child: Container(),
+          onKey: (RawKeyEvent event) {
+            logs.add(1);
+          },
+        ),
+      );
+
+      // Only the Service binding handler.
+
+      expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+        false);
+      expect(logs, <int>[1]);
+      logs.clear();
+
+      // Add a handler.
+
+      void handler2(RawKeyEvent event) {
+        logs.add(2);
+      }
+      RawKeyboard.instance.addListener(handler2);
+
+      expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+        false);
+      expect(logs, <int>[1, 2]);
+      logs.clear();
+
+      // Add another handler.
+
+      void handler3(RawKeyEvent event) {
+        logs.add(3);
+      }
+      RawKeyboard.instance.addListener(handler3);
+
+      expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+        false);
+      expect(logs, <int>[1, 2, 3]);
+      logs.clear();
+
+      // Add handler2 again.
+
+      RawKeyboard.instance.addListener(handler2);
+
+      expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
+        false);
+      expect(logs, <int>[1, 2, 3, 2]);
+      logs.clear();
+
+      // Remove handler2 once.
+
+      RawKeyboard.instance.removeListener(handler2);
+      expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
+        false);
+      expect(logs, <int>[1, 3, 2]);
+      logs.clear();
+    }, variant: KeySimulatorTransitModeVariant.all());
   });
 
   group('RawKeyEventDataAndroid', () {
@@ -955,6 +1019,7 @@
         },
       );
       expect(message, equals(<String, dynamic>{ 'handled': false }));
+      message = null;
 
       // Set up a widget that will receive focused text events.
       final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart
index 5fa16ab..a2403ec 100644
--- a/packages/flutter/test/widgets/app_test.dart
+++ b/packages/flutter/test/widgets/app_test.dart
@@ -148,7 +148,7 @@
     await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonA);
     await tester.pumpAndSettle();
     expect(checked, isTrue);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   group('error control test', () {
     Future<void> expectFlutterError({
diff --git a/packages/flutter/test/widgets/default_text_editing_actions_test.dart b/packages/flutter/test/widgets/default_text_editing_actions_test.dart
index 91ca5a8..8718d71 100644
--- a/packages/flutter/test/widgets/default_text_editing_actions_test.dart
+++ b/packages/flutter/test/widgets/default_text_editing_actions_test.dart
@@ -114,5 +114,5 @@
     await tester.pump();
     expect(leftCalled, isFalse);
     expect(rightCalled, isTrue);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 }
diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart
index b069b4e..e7e44f8 100644
--- a/packages/flutter/test/widgets/editable_text_cursor_test.dart
+++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart
@@ -328,6 +328,8 @@
   });
 
   testWidgets('Cursor animation restarts when it is moved using keys on desktop', (WidgetTester tester) async {
+    debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
+
     const String testText = 'Some text long enough to move the cursor around';
     final TextEditingController controller = TextEditingController(text: testText);
     final Widget widget = MaterialApp(
@@ -400,7 +402,9 @@
     await tester.pump(const Duration(milliseconds: 1));
     expect(renderEditable.cursorColor!.alpha, 0);
     expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
-  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }));
+
+    debugDefaultTargetPlatformOverride = null;
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Cursor does not show when showCursor set to false', (WidgetTester tester) async {
     const Widget widget = MaterialApp(
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 0bce261..63b55d7 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -4864,8 +4864,23 @@
     expect(controller.text, isEmpty, reason: 'on $platform');
   }
 
-  testWidgets('keyboard text selection works', (WidgetTester tester) async {
+  testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async {
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
+
     await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
+
+    debugKeyEventSimulatorTransitModeOverride = null;
+
+    // On web, using keyboard for selection is handled by the browser.
+  }, skip: kIsWeb, variant: TargetPlatformVariant.all());
+
+  testWidgets('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async {
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+    await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
+
+    debugKeyEventSimulatorTransitModeOverride = null;
+
     // On web, using keyboard for selection is handled by the browser.
   }, skip: kIsWeb, variant: TargetPlatformVariant.all());
 
@@ -7675,7 +7690,7 @@
       expect(controller.selection.isCollapsed, true);
       expect(controller.selection.baseOffset, 1);
     }
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('the toolbar is disposed when selection changes and there is no selectionControls', (WidgetTester tester) async {
     late StateSetter setState;
diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart
index 881c203..949cd23 100644
--- a/packages/flutter/test/widgets/focus_manager_test.dart
+++ b/packages/flutter/test/widgets/focus_manager_test.dart
@@ -165,7 +165,101 @@
         'hasPrimaryFocus: false',
       ]);
     });
+
+    testWidgets('onKeyEvent and onKey correctly cooperate', (WidgetTester tester) async {
+      final FocusNode focusNode = FocusNode(debugLabel: 'Test Node 3');
+      List<List<KeyEventResult>> results = <List<KeyEventResult>>[
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+      ];
+      final List<int> logs = <int>[];
+
+      await tester.pumpWidget(
+        Focus(
+          focusNode: FocusNode(debugLabel: 'Test Node 1'),
+          onKeyEvent: (_, KeyEvent event) {
+            logs.add(0);
+            return results[0][0];
+          },
+          onKey: (_, RawKeyEvent event) {
+            logs.add(1);
+            return results[0][1];
+          },
+          child: Focus(
+            focusNode: FocusNode(debugLabel: 'Test Node 2'),
+            onKeyEvent: (_, KeyEvent event) {
+              logs.add(10);
+              return results[1][0];
+            },
+            onKey: (_, RawKeyEvent event) {
+              logs.add(11);
+              return results[1][1];
+            },
+            child: Focus(
+              focusNode: focusNode,
+              onKeyEvent: (_, KeyEvent event) {
+                logs.add(20);
+                return results[2][0];
+              },
+              onKey: (_, RawKeyEvent event) {
+                logs.add(21);
+                return results[2][1];
+              },
+              child: const SizedBox(width: 200, height: 100),
+            ),
+          ),
+        ),
+      );
+      focusNode.requestFocus();
+      await tester.pump();
+
+      // All ignored.
+      results = <List<KeyEventResult>>[
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+      ];
+      expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1),
+          false);
+      expect(logs, <int>[20, 21, 10, 11, 0, 1]);
+      logs.clear();
+
+      // The onKeyEvent should be able to stop propagation.
+      results = <List<KeyEventResult>>[
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.handled, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+      ];
+      expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1),
+          true);
+      expect(logs, <int>[20, 21, 10, 11]);
+      logs.clear();
+
+      // The onKey should be able to stop propagation.
+      results = <List<KeyEventResult>>[
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.handled],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+      ];
+      expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1),
+          true);
+      expect(logs, <int>[20, 21, 10, 11]);
+      logs.clear();
+
+      // KeyEventResult.skipRemainingHandlers works.
+      results = <List<KeyEventResult>>[
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.skipRemainingHandlers, KeyEventResult.ignored],
+        <KeyEventResult>[KeyEventResult.ignored, KeyEventResult.ignored],
+      ];
+      expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1),
+          false);
+      expect(logs, <int>[20, 21, 10, 11]);
+      logs.clear();
+    }, variant: KeySimulatorTransitModeVariant.all());
   });
+
   group(FocusScopeNode, () {
 
     testWidgets('Can setFirstFocus on a scope with no manager.', (WidgetTester tester) async {
@@ -935,7 +1029,7 @@
       // Since none of the focused nodes handle this event, nothing should
       // receive it.
       expect(receivedAnEvent, isEmpty);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Initial highlight mode guesses correctly.', (WidgetTester tester) async {
       FocusManager.instance.highlightStrategy = FocusHighlightStrategy.automatic;
diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart
index d514039..8fc023d 100644
--- a/packages/flutter/test/widgets/focus_traversal_test.dart
+++ b/packages/flutter/test/widgets/focus_traversal_test.dart
@@ -1678,7 +1678,7 @@
       expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue);
       await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
       expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue);
-    }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+    }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
 
     testWidgets('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async {
       final List<int> items = List<int>.generate(11, (int index) => index).toList();
@@ -1776,7 +1776,7 @@
       await tester.pump();
       expect(topNode.hasPrimaryFocus, isTrue);
       expect(controller.offset, equals(0.0));
-    }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+    }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
 
     testWidgets('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async {
       final List<int> items = List<int>.generate(11, (int index) => index).toList();
@@ -1874,7 +1874,7 @@
       await tester.pump();
       expect(leftNode.hasPrimaryFocus, isTrue);
       expect(controller.offset, equals(0.0));
-    }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347
+    }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347
 
     testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async {
       final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
@@ -1997,10 +1997,10 @@
       expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
       await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
       expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async {
-      final List<RawKeyEvent> events = <RawKeyEvent>[];
+      final List<Object> events = <Object>[];
 
       await tester.pumpWidget(MaterialApp(home: Container()));
 
@@ -2013,7 +2013,7 @@
       await tester.idle();
 
       expect(events.length, 2);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async {
       await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title')))));
@@ -2047,7 +2047,7 @@
       await tester.idle();
 
       expect(events.length, 2);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
   });
   group(FocusTraversalGroup, () {
     testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async {
diff --git a/packages/flutter/test/widgets/keyboard_listener_test.dart b/packages/flutter/test/widgets/keyboard_listener_test.dart
new file mode 100644
index 0000000..7164077
--- /dev/null
+++ b/packages/flutter/test/widgets/keyboard_listener_test.dart
@@ -0,0 +1,106 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('Can dispose without keyboard', (WidgetTester tester) async {
+    final FocusNode focusNode = FocusNode();
+    await tester.pumpWidget(KeyboardListener(focusNode: focusNode, onKeyEvent: null, child: Container()));
+    await tester.pumpWidget(KeyboardListener(focusNode: focusNode, onKeyEvent: null, child: Container()));
+    await tester.pumpWidget(Container());
+  });
+
+  testWidgets('Fuchsia key event', (WidgetTester tester) async {
+    final List<KeyEvent> events = <KeyEvent>[];
+
+    final FocusNode focusNode = FocusNode();
+
+    await tester.pumpWidget(
+      KeyboardListener(
+        focusNode: focusNode,
+        onKeyEvent: events.add,
+        child: Container(),
+      ),
+    );
+
+    focusNode.requestFocus();
+    await tester.idle();
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
+    await tester.idle();
+
+    expect(events.length, 2);
+    expect(events[0], isA<KeyDownEvent>());
+    expect(events[0].physicalKey, PhysicalKeyboardKey.metaLeft);
+    expect(events[0].logicalKey, LogicalKeyboardKey.metaLeft);
+
+    await tester.pumpWidget(Container());
+    focusNode.dispose();
+  }, skip: isBrowser); // This is a Fuchsia-specific test.
+
+  testWidgets('Web key event', (WidgetTester tester) async {
+    final List<KeyEvent> events = <KeyEvent>[];
+
+    final FocusNode focusNode = FocusNode();
+
+    await tester.pumpWidget(
+      KeyboardListener(
+        focusNode: focusNode,
+        onKeyEvent: events.add,
+        child: Container(),
+      ),
+    );
+
+    focusNode.requestFocus();
+    await tester.idle();
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'web');
+    await tester.idle();
+
+    expect(events.length, 2);
+    expect(events[0], isA<KeyDownEvent>());
+    expect(events[0].physicalKey, PhysicalKeyboardKey.metaLeft);
+    expect(events[0].logicalKey, LogicalKeyboardKey.metaLeft);
+
+    await tester.pumpWidget(Container());
+    focusNode.dispose();
+  });
+
+  testWidgets('Defunct listeners do not receive events', (WidgetTester tester) async {
+    final List<KeyEvent> events = <KeyEvent>[];
+
+    final FocusNode focusNode = FocusNode();
+
+    await tester.pumpWidget(
+      KeyboardListener(
+        focusNode: focusNode,
+        onKeyEvent: events.add,
+        child: Container(),
+      ),
+    );
+
+    focusNode.requestFocus();
+    await tester.idle();
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
+    await tester.idle();
+
+    expect(events.length, 2);
+    events.clear();
+
+    await tester.pumpWidget(Container());
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
+
+    await tester.idle();
+
+    expect(events.length, 0);
+
+    await tester.pumpWidget(Container());
+    focusNode.dispose();
+  });
+}
diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart
index 9df01fc..082fe70 100644
--- a/packages/flutter/test/widgets/scrollable_test.dart
+++ b/packages/flutter/test/widgets/scrollable_test.dart
@@ -520,7 +520,7 @@
     await tester.pumpAndSettle();
     expect(controller.position.pixels, equals(0.0));
     expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
     final ScrollController controller = ScrollController();
@@ -571,7 +571,7 @@
     await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
     await tester.pumpAndSettle();
     expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
     final ScrollController controller = ScrollController();
@@ -617,7 +617,7 @@
       await tester.sendKeyUpEvent(modifierKey);
     await tester.pumpAndSettle();
     expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 50.0, 600.0)));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Horizontal scrollables are scrolled the correct direction in RTL locales.', (WidgetTester tester) async {
     final ScrollController controller = ScrollController();
@@ -666,7 +666,7 @@
       await tester.sendKeyUpEvent(modifierKey);
     await tester.pumpAndSettle();
     expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(750.0, 0.0, 800.0, 600.0)));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Reversed vertical scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
     final ScrollController controller = ScrollController();
@@ -720,7 +720,7 @@
     await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
     await tester.pumpAndSettle();
     expect(tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 550.0, 800.0, 600.0)));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Reversed horizontal scrollables are scrolled when activated via keyboard.', (WidgetTester tester) async {
     final ScrollController controller = ScrollController();
@@ -768,7 +768,7 @@
     if (!kIsWeb)
       await tester.sendKeyUpEvent(modifierKey);
     await tester.pumpAndSettle();
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Custom scrollables with a center sliver are scrolled when activated via keyboard.', (WidgetTester tester) async {
     final ScrollController controller = ScrollController();
@@ -828,7 +828,7 @@
     // Goes up two past "center" where it started, so negative.
     expect(controller.position.pixels, equals(-100.0));
     expect(tester.getRect(find.byKey(const ValueKey<String>('Item 10'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 100.0, 800.0, 200.0)));
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Can recommendDeferredLoadingForContext - animation', (WidgetTester tester) async {
     final List<String> widgetTracker = <String>[];
diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart
index e58f680..f039c68 100644
--- a/packages/flutter/test/widgets/selectable_text_test.dart
+++ b/packages/flutter/test/widgets/selectable_text_test.dart
@@ -1570,7 +1570,7 @@
       await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
       await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
       expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Shift test 2', (WidgetTester tester) async {
       await setupWidget(tester, 'abcdefghi');
@@ -1582,7 +1582,7 @@
       await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
       await tester.pumpAndSettle();
       expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Control Shift test', (WidgetTester tester) async {
       await setupWidget(tester, 'their big house');
@@ -1594,7 +1594,7 @@
       await tester.pumpAndSettle();
 
       expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Down and up test', (WidgetTester tester) async {
       await setupWidget(tester, 'a big house');
@@ -1612,7 +1612,7 @@
       await tester.pumpAndSettle();
 
       expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('Down and up test 2', (WidgetTester tester) async {
       await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay');
@@ -1663,7 +1663,7 @@
       await tester.pumpAndSettle();
 
       expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
   });
 
   testWidgets('Copy test', (WidgetTester tester) async {
@@ -1722,7 +1722,7 @@
 
     await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
     await tester.pumpAndSettle();
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Select all test', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -1757,7 +1757,7 @@
 
     expect(controller.selection.baseOffset, 0);
     expect(controller.selection.extentOffset, 31);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -1804,7 +1804,7 @@
       expect(newSelection!.extentOffset, i + 1);
       newSelection = null;
     }
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Changing positions of selectable text', (WidgetTester tester) async {
     final FocusNode focusNode = FocusNode();
@@ -1894,7 +1894,7 @@
     c1 = editableTextWidget.controller;
 
     expect(c1.selection.extentOffset - c1.selection.baseOffset, -6);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
 
   testWidgets('Changing focus test', (WidgetTester tester) async {
@@ -1964,7 +1964,7 @@
 
     expect(c1.selection.extentOffset - c1.selection.baseOffset, 0);
     expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
-  });
+  }, variant: KeySimulatorTransitModeVariant.all());
 
   testWidgets('Caret works when maxLines is null', (WidgetTester tester) async {
     await tester.pumpWidget(
diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart
index f52c03b..42c3c2d 100644
--- a/packages/flutter/test/widgets/shortcuts_test.dart
+++ b/packages/flutter/test/widgets/shortcuts_test.dart
@@ -435,7 +435,33 @@
       invoked = 0;
 
       expect(RawKeyboard.instance.keysPressed, isEmpty);
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
+
+    testWidgets('handles repeated events', (WidgetTester tester) async {
+      int invoked = 0;
+      await tester.pumpWidget(activatorTester(
+        const SingleActivator(
+          LogicalKeyboardKey.keyC,
+          control: true,
+        ),
+        (Intent intent) { invoked += 1; },
+      ));
+      await tester.pump();
+
+      // LCtrl -> KeyC: Accept
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+      expect(invoked, 0);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
+      expect(invoked, 1);
+      await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC);
+      expect(invoked, 2);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+      expect(invoked, 2);
+      invoked = 0;
+
+      expect(RawKeyboard.instance.keysPressed, isEmpty);
+    }, variant: KeySimulatorTransitModeVariant.all());
 
     testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async {
       int invoked = 0;
@@ -1075,7 +1101,27 @@
       await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
       expect(invoked, 1);
       invoked = 0;
-    });
+    }, variant: KeySimulatorTransitModeVariant.all());
+
+    testWidgets('handles repeated events', (WidgetTester tester) async {
+      int invoked = 0;
+      await tester.pumpWidget(activatorTester(
+        const CharacterActivator('?'),
+        (Intent intent) { invoked += 1; },
+      ));
+      await tester.pump();
+
+      // Press KeyC: Accepted by DumbLogicalActivator
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
+      expect(invoked, 1);
+      await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?');
+      expect(invoked, 2);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
+      expect(invoked, 2);
+      invoked = 0;
+    }, variant: KeySimulatorTransitModeVariant.all());
   });
 
   group('CallbackShortcuts', () {
diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart
index 8c34a61..16f2440 100644
--- a/packages/flutter_test/lib/src/binding.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -828,6 +828,9 @@
     assert(debugAssertAllSchedulerVarsUnset(
       'The value of a scheduler debug variable was changed by the test.',
     ));
+    assert(debugAssertAllServicesVarsUnset(
+      'The value of a services debug variable was changed by the test.',
+    ));
   }
 
   void _verifyAutoUpdateGoldensUnset(bool valueBeforeTest) {
@@ -894,6 +897,10 @@
     // tests.
     // ignore: invalid_use_of_visible_for_testing_member
     RawKeyboard.instance.clearKeysPressed();
+    // ignore: invalid_use_of_visible_for_testing_member
+    HardwareKeyboard.instance.clearState();
+    // ignore: invalid_use_of_visible_for_testing_member
+    keyEventManager.clearState();
     assert(!RendererBinding.instance!.mouseTracker.mouseIsConnected,
         'The MouseTracker thinks that there is still a mouse connected, which indicates that a '
         'test has not removed the mouse pointer which it added. Call removePointer on the '
diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart
index 65d702b..a6466a3 100644
--- a/packages/flutter_test/lib/src/controller.dart
+++ b/packages/flutter_test/lib/src/controller.dart
@@ -973,7 +973,7 @@
     return box.size;
   }
 
-  /// Simulates sending physical key down and up events through the system channel.
+  /// Simulates sending physical key down and up events.
   ///
   /// This only simulates key events coming from a physical keyboard, not from a
   /// soft keyboard.
@@ -984,6 +984,9 @@
   /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
   /// supported.
   ///
+  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
+  ///
   /// Keys that are down when the test completes are cleared after each test.
   ///
   /// This method sends both the key down and the key up events, to simulate a
@@ -1004,7 +1007,7 @@
     return handled;
   }
 
-  /// Simulates sending a physical key down event through the system channel.
+  /// Simulates sending a physical key down event.
   ///
   /// This only simulates key down events coming from a physical keyboard, not
   /// from a soft keyboard.
@@ -1015,13 +1018,17 @@
   /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
   /// supported.
   ///
+  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
+  ///
   /// Keys that are down when the test completes are cleared after each test.
   ///
   /// Returns true if the key event was handled by the framework.
   ///
   /// See also:
   ///
-  ///  - [sendKeyUpEvent] to simulate the corresponding key up event.
+  ///  - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
+  ///    key up and repeat event.
   ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
   Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
     assert(platform != null);
@@ -1039,11 +1046,15 @@
   /// that type of system. Defaults to "web" on web, and "android" everywhere
   /// else. May not be null.
   ///
+  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
+  ///
   /// Returns true if the key event was handled by the framework.
   ///
   /// See also:
   ///
-  ///  - [sendKeyDownEvent] to simulate the corresponding key down event.
+  ///  - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
+  ///    corresponding key down and repeat event.
   ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
   Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
     assert(platform != null);
@@ -1051,6 +1062,35 @@
     return simulateKeyUpEvent(key, platform: platform);
   }
 
+  /// Simulates sending a physical key repeat event.
+  ///
+  /// This only simulates key repeat events coming from a physical keyboard, not
+  /// from a soft keyboard.
+  ///
+  /// Specify `platform` as one of the platforms allowed in
+  /// [platform.Platform.operatingSystem] to make the event appear to be from that type
+  /// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
+  /// null. Some platforms (e.g. Windows, iOS) are not yet supported.
+  ///
+  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
+  /// controlled by [debugKeyEventSimulatorTransitModeOverride]. If through [RawKeyEvent],
+  /// this method is equivalent to [sendKeyDownEvent].
+  ///
+  /// Keys that are down when the test completes are cleared after each test.
+  ///
+  /// Returns true if the key event was handled by the framework.
+  ///
+  /// See also:
+  ///
+  ///  - [sendKeyDownEvent] and [sendKeyUpEvent] to simulate the corresponding
+  ///    key down and up event.
+  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
+  Future<bool> sendKeyRepeatEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
+    assert(platform != null);
+    // Internally wrapped in async guard.
+    return simulateKeyRepeatEvent(key, character: character, platform: platform);
+  }
+
   /// Returns the rect of the given widget. This is only valid once
   /// the widget's render object has been laid out at least once.
   Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder);
diff --git a/packages/flutter_test/lib/src/event_simulation.dart b/packages/flutter_test/lib/src/event_simulation.dart
index 4b897e8..32f81dc 100644
--- a/packages/flutter_test/lib/src/event_simulation.dart
+++ b/packages/flutter_test/lib/src/event_simulation.dart
@@ -4,9 +4,12 @@
 
 import 'dart:async';
 import 'dart:io';
+import 'dart:ui' as ui;
 
-import 'package:flutter/foundation.dart' show kIsWeb;
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
 
 import 'binding.dart';
 import 'test_async_utils.dart';
@@ -16,13 +19,14 @@
 // https://github.com/flutter/flutter/issues/33521
 // This code can only simulate keys which appear in the key maps.
 
-String _keyLabel(LogicalKeyboardKey key) {
+String? _keyLabel(LogicalKeyboardKey key) {
   final String keyLabel = key.keyLabel;
   if (keyLabel.length == 1)
     return keyLabel.toLowerCase();
-  return '';
+  return null;
 }
 
+// ignore: avoid_classes_with_only_static_members
 /// A class that serves as a namespace for a bunch of keyboard-key generation
 /// utilities.
 class KeyEventSimulator {
@@ -153,7 +157,7 @@
     return result!;
   }
 
-  static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key, String platform) {
+  static PhysicalKeyboardKey _findPhysicalKeyByPlatform(LogicalKeyboardKey key, String platform) {
     assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation');
     late Map<dynamic, PhysicalKeyboardKey> map;
     if (kIsWeb) {
@@ -208,7 +212,7 @@
     key = _getKeySynonym(key);
 
     // Find a suitable physical key if none was supplied.
-    physicalKey ??= _findPhysicalKey(key, platform);
+    physicalKey ??= _findPhysicalKeyByPlatform(key, platform);
 
     assert(key.debugName != null);
     final int keyCode = _getKeyCode(key, platform);
@@ -219,7 +223,7 @@
       'keymap': platform,
     };
 
-    final String resultCharacter = character ?? _keyLabel(key);
+    final String resultCharacter = character ?? _keyLabel(key) ?? '';
     void assignWeb() {
       result['code'] = _getWebKeyCode(key);
       result['key'] = resultCharacter;
@@ -625,7 +629,64 @@
     return result;
   }
 
-  /// Simulates sending a hardware key down event through the system channel.
+  static Future<bool> _simulateKeyEventByRawEvent(ValueGetter<Map<String, dynamic>> buildKeyData) async {
+    return TestAsyncUtils.guard<bool>(() async {
+      final Completer<bool> result = Completer<bool>();
+      await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
+        SystemChannels.keyEvent.name,
+        SystemChannels.keyEvent.codec.encodeMessage(buildKeyData()),
+        (ByteData? data) {
+          if (data == null) {
+            result.complete(false);
+            return;
+          }
+          final Map<String, dynamic> decoded = SystemChannels.keyEvent.codec.decodeMessage(data) as Map<String, dynamic>;
+          result.complete(decoded['handled'] as bool);
+        }
+      );
+      return result.future;
+    });
+  }
+
+  static late final Map<String, PhysicalKeyboardKey> _debugNameToPhysicalKey = (() {
+    final Map<String, PhysicalKeyboardKey> result = <String, PhysicalKeyboardKey>{};
+    for (final PhysicalKeyboardKey key in PhysicalKeyboardKey.knownPhysicalKeys) {
+      final String? debugName = key.debugName;
+      if (debugName != null)
+        result[debugName] = key;
+    }
+    return result;
+  })();
+  static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key) {
+    final PhysicalKeyboardKey? result = _debugNameToPhysicalKey[key.debugName];
+    assert(result != null, 'Physical key for $key not found in known physical keys');
+    return result!;
+  }
+
+  static const KeyDataTransitMode _defaultTransitMode = KeyDataTransitMode.rawKeyData;
+
+  // The simulation transit mode for [simulateKeyDownEvent], [simulateKeyUpEvent],
+  // and [simulateKeyRepeatEvent].
+  //
+  // Simulation transit mode is the mode that simulated key events are constructed
+  // and delivered. For detailed introduction, see [KeyDataTransitMode] and
+  // its values.
+  //
+  // The `_transitMode` defaults to [KeyDataTransitMode.rawKeyEvent], and can be
+  // overridden with [debugKeyEventSimulatorTransitModeOverride].  In widget tests, it
+  // is often set with [KeySimulationModeVariant].
+  static KeyDataTransitMode get _transitMode {
+    KeyDataTransitMode? result;
+    assert(() {
+      result = debugKeyEventSimulatorTransitModeOverride;
+      return true;
+    }());
+    return result ?? _defaultTransitMode;
+  }
+
+  static String get _defaultPlatform => kIsWeb ? 'web' : Platform.operatingSystem;
+
+  /// Simulates sending a hardware key down event.
   ///
   /// This only simulates key presses coming from a physical keyboard, not from a
   /// soft keyboard.
@@ -641,29 +702,36 @@
   ///
   /// See also:
   ///
-  ///  - [simulateKeyUpEvent] to simulate the corresponding key up event.
-  static Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) async {
-    return TestAsyncUtils.guard<bool>(() async {
-      platform ??= Platform.operatingSystem;
-      assert(_osIsSupported(platform!), 'Platform $platform not supported for key simulation');
-
-
-      final Map<String, dynamic> data = getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
-      final Completer<bool> result = Completer<bool>();
-      await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
-        SystemChannels.keyEvent.name,
-        SystemChannels.keyEvent.codec.encodeMessage(data),
-        (ByteData? data) {
-          if (data == null) {
-            result.complete(false);
-            return;
-          }
-          final Map<String, dynamic> decoded = SystemChannels.keyEvent.codec.decodeMessage(data) as Map<String, dynamic>;
-          result.complete(decoded['handled'] as bool);
-        }
-      );
-      return result.future;
-    });
+  ///  * [simulateKeyUpEvent] to simulate the corresponding key up event.
+  static Future<bool> simulateKeyDownEvent(
+    LogicalKeyboardKey key, {
+    String? platform,
+    PhysicalKeyboardKey? physicalKey,
+    String? character,
+  }) async {
+    Future<bool> _simulateByRawEvent() {
+      return _simulateKeyEventByRawEvent(() {
+        platform ??= _defaultPlatform;
+        return getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
+      });
+    }
+    switch (_transitMode) {
+      case KeyDataTransitMode.rawKeyData:
+        return _simulateByRawEvent();
+      case KeyDataTransitMode.keyDataThenRawKeyData:
+        final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
+        final bool resultByKeyEvent = ServicesBinding.instance!.keyEventManager.handleKeyData(
+          ui.KeyData(
+            type: ui.KeyEventType.down,
+            physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
+            logical: logicalKey.keyId,
+            timeStamp: Duration.zero,
+            character: character ?? _keyLabel(key),
+            synthesized: false,
+          ),
+        );
+        return (await _simulateByRawEvent()) || resultByKeyEvent;
+    }
   }
 
   /// Simulates sending a hardware key up event through the system channel.
@@ -680,29 +748,81 @@
   ///
   /// See also:
   ///
-  ///  - [simulateKeyDownEvent] to simulate the corresponding key down event.
-  static Future<bool> simulateKeyUpEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) async {
-    return TestAsyncUtils.guard<bool>(() async {
-      platform ??= Platform.operatingSystem;
-      assert(_osIsSupported(platform!), 'Platform $platform not supported for key simulation');
+  ///  * [simulateKeyDownEvent] to simulate the corresponding key down event.
+  static Future<bool> simulateKeyUpEvent(
+    LogicalKeyboardKey key, {
+    String? platform,
+    PhysicalKeyboardKey? physicalKey,
+  }) async {
+    Future<bool> _simulateByRawEvent() {
+      return _simulateKeyEventByRawEvent(() {
+        platform ??= _defaultPlatform;
+        return getKeyData(key, platform: platform!, isDown: false, physicalKey: physicalKey);
+      });
+    }
+    switch (_transitMode) {
+      case KeyDataTransitMode.rawKeyData:
+        return _simulateByRawEvent();
+      case KeyDataTransitMode.keyDataThenRawKeyData:
+        final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
+        final bool resultByKeyEvent = ServicesBinding.instance!.keyEventManager.handleKeyData(
+          ui.KeyData(
+            type: ui.KeyEventType.up,
+            physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
+            logical: logicalKey.keyId,
+            timeStamp: Duration.zero,
+            character: null,
+            synthesized: false,
+          ),
+        );
+        return (await _simulateByRawEvent()) || resultByKeyEvent;
+    }
+  }
 
-      final Map<String, dynamic> data = getKeyData(key, platform: platform!, isDown: false, physicalKey: physicalKey);
-      bool result = false;
-      await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
-        SystemChannels.keyEvent.name,
-        SystemChannels.keyEvent.codec.encodeMessage(data),
-        (ByteData? data) {
-          if (data == null) {
-            return;
-          }
-          final Map<String, dynamic> decoded = SystemChannels.keyEvent.codec.decodeMessage(data) as Map<String, dynamic>;
-          if (decoded['handled'] as bool) {
-            result = true;
-          }
-        }
-      );
-      return result;
-    });
+  /// Simulates sending a hardware key repeat event through the system channel.
+  ///
+  /// This only simulates key presses coming from a physical keyboard, not from a
+  /// soft keyboard.
+  ///
+  /// Specify `platform` as one of the platforms allowed in
+  /// [Platform.operatingSystem] to make the event appear to be from that type of
+  /// system. Defaults to the operating system that the test is running on. Some
+  /// platforms (e.g. Windows, iOS) are not yet supported.
+  ///
+  /// Returns true if the key event was handled by the framework.
+  ///
+  /// See also:
+  ///
+  ///  * [simulateKeyDownEvent] to simulate the corresponding key down event.
+  static Future<bool> simulateKeyRepeatEvent(
+    LogicalKeyboardKey key, {
+    String? platform,
+    PhysicalKeyboardKey? physicalKey,
+    String? character,
+  }) async {
+    Future<bool> _simulateByRawEvent() {
+      return _simulateKeyEventByRawEvent(() {
+        platform ??= _defaultPlatform;
+        return getKeyData(key, platform: platform!, isDown: true, physicalKey: physicalKey, character: character);
+      });
+    }
+    switch (_transitMode) {
+      case KeyDataTransitMode.rawKeyData:
+        return _simulateByRawEvent();
+      case KeyDataTransitMode.keyDataThenRawKeyData:
+        final LogicalKeyboardKey logicalKey = _getKeySynonym(key);
+        final bool resultByKeyEvent = ServicesBinding.instance!.keyEventManager.handleKeyData(
+          ui.KeyData(
+            type: ui.KeyEventType.repeat,
+            physical: (physicalKey ?? _findPhysicalKey(logicalKey)).usbHidUsage,
+            logical: logicalKey.keyId,
+            timeStamp: Duration.zero,
+            character: character ?? _keyLabel(key),
+            synthesized: false,
+          ),
+        );
+        return (await _simulateByRawEvent()) || resultByKeyEvent;
+    }
   }
 }
 
@@ -725,8 +845,14 @@
 ///
 /// See also:
 ///
-///  - [simulateKeyUpEvent] to simulate the corresponding key up event.
-Future<bool> simulateKeyDownEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey, String? character}) {
+///  * [simulateKeyUpEvent] and [simulateKeyRepeatEvent] to simulate the
+///    corresponding key up and repeat event.
+Future<bool> simulateKeyDownEvent(
+  LogicalKeyboardKey key, {
+  String? platform,
+  PhysicalKeyboardKey? physicalKey,
+  String? character,
+}) {
   return KeyEventSimulator.simulateKeyDownEvent(key, platform: platform, physicalKey: physicalKey, character: character);
 }
 
@@ -747,7 +873,85 @@
 ///
 /// See also:
 ///
-///  - [simulateKeyDownEvent] to simulate the corresponding key down event.
-Future<bool> simulateKeyUpEvent(LogicalKeyboardKey key, {String? platform, PhysicalKeyboardKey? physicalKey}) {
+///  * [simulateKeyDownEvent] and [simulateKeyRepeatEvent] to simulate the
+///    corresponding key down and repeat event.
+Future<bool> simulateKeyUpEvent(
+  LogicalKeyboardKey key, {
+  String? platform,
+  PhysicalKeyboardKey? physicalKey,
+}) {
   return KeyEventSimulator.simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
 }
+
+/// Simulates sending a hardware key repeat event through the system channel.
+///
+/// This only simulates key presses coming from a physical keyboard, not from a
+/// soft keyboard.
+///
+/// Specify `platform` as one of the platforms allowed in
+/// [Platform.operatingSystem] to make the event appear to be from that type of
+/// system. Defaults to the operating system that the test is running on. Some
+/// platforms (e.g. Windows, iOS) are not yet supported.
+///
+/// Returns true if the key event was handled by the framework.
+///
+/// See also:
+///
+///  - [simulateKeyDownEvent] and [simulateKeyUpEvent] to simulate the
+///    corresponding key down and up event.
+Future<bool> simulateKeyRepeatEvent(
+  LogicalKeyboardKey key, {
+  String? platform,
+  PhysicalKeyboardKey? physicalKey,
+  String? character,
+}) {
+  return KeyEventSimulator.simulateKeyRepeatEvent(key, platform: platform, physicalKey: physicalKey, character: character);
+}
+
+/// A [TestVariant] that runs tests with transit modes set to different values
+/// of [KeyDataTransitMode].
+class KeySimulatorTransitModeVariant extends TestVariant<KeyDataTransitMode> {
+  /// Creates a [KeySimulatorTransitModeVariant] that tests the given [values].
+  const KeySimulatorTransitModeVariant(this.values);
+
+  /// Creates a [KeySimulatorTransitModeVariant] for each value option of
+  /// [KeyDataTransitMode].
+  KeySimulatorTransitModeVariant.all()
+    : this(KeyDataTransitMode.values.toSet());
+
+  /// Creates a [KeySimulatorTransitModeVariant] that only contains
+  /// [KeyDataTransitMode.keyDataThenRawKeyData].
+  KeySimulatorTransitModeVariant.keyDataThenRawKeyData()
+    : this(<KeyDataTransitMode>{KeyDataTransitMode.keyDataThenRawKeyData});
+
+  @override
+  final Set<KeyDataTransitMode> values;
+
+  @override
+  String describeValue(KeyDataTransitMode value) {
+    switch (value) {
+      case KeyDataTransitMode.rawKeyData:
+        return 'RawKeyEvent';
+      case KeyDataTransitMode.keyDataThenRawKeyData:
+        return 'ui.KeyData then RawKeyEvent';
+    }
+  }
+
+  @override
+  Future<KeyDataTransitMode?> setUp(KeyDataTransitMode value) async {
+    final KeyDataTransitMode? previousSetting = debugKeyEventSimulatorTransitModeOverride;
+    debugKeyEventSimulatorTransitModeOverride = value;
+    return previousSetting;
+  }
+
+  @override
+  Future<void> tearDown(KeyDataTransitMode value, KeyDataTransitMode? memento) async {
+    // ignore: invalid_use_of_visible_for_testing_member
+    RawKeyboard.instance.clearKeysPressed();
+    // ignore: invalid_use_of_visible_for_testing_member
+    HardwareKeyboard.instance.clearState();
+    // ignore: invalid_use_of_visible_for_testing_member
+    ServicesBinding.instance!.keyEventManager.clearState();
+    debugKeyEventSimulatorTransitModeOverride = memento;
+  }
+}
diff --git a/packages/flutter_test/lib/src/test_pointer.dart b/packages/flutter_test/lib/src/test_pointer.dart
index 77bd8bd..7ded5c0 100644
--- a/packages/flutter_test/lib/src/test_pointer.dart
+++ b/packages/flutter_test/lib/src/test_pointer.dart
@@ -230,6 +230,7 @@
       timeStamp: timeStamp,
       kind: kind,
       device: _device,
+      pointer: pointer,
       position: _location ?? Offset.zero,
     );
   }
@@ -255,6 +256,7 @@
       timeStamp: timeStamp,
       kind: kind,
       device: _device,
+      pointer: pointer,
       position: newLocation,
       delta: delta,
     );
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 9651fda..eb86947 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -510,7 +510,7 @@
 /// Class that programmatically interacts with widgets and the test environment.
 ///
 /// For convenience, instances of this class (such as the one provided by
-/// `testWidget`) can be used as the `vsync` for `AnimationController` objects.
+/// `testWidgets`) can be used as the `vsync` for `AnimationController` objects.
 class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider {
   WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
     if (binding is LiveTestWidgetsFlutterBinding)
diff --git a/packages/flutter_test/test/event_simulation_test.dart b/packages/flutter_test/test/event_simulation_test.dart
index 38cdd8f..0dfc774 100644
--- a/packages/flutter_test/test/event_simulation_test.dart
+++ b/packages/flutter_test/test/event_simulation_test.dart
@@ -2,14 +2,44 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 const List<String> platforms = <String>['linux', 'macos', 'android', 'fuchsia'];
 
+void _verifyKeyEvent<T extends KeyEvent>(KeyEvent event, PhysicalKeyboardKey physical, LogicalKeyboardKey logical, String? character) {
+  expect(event, isA<T>());
+  expect(event.physicalKey, physical);
+  expect(event.logicalKey, logical);
+  expect(event.character, character);
+  expect(event.synthesized, false);
+}
+
+void _verifyRawKeyEvent<T extends RawKeyEvent>(RawKeyEvent event, PhysicalKeyboardKey physical, LogicalKeyboardKey logical, String? character) {
+  expect(event, isA<T>());
+  expect(event.physicalKey, physical);
+  expect(event.logicalKey, logical);
+  expect(event.character, character);
+}
+
+Future<void> _shouldThrow<T extends Error>(AsyncValueGetter<void> func) async {
+  bool hasError = false;
+  try {
+    await func();
+  } catch (e) {
+    expect(e, isA<T>());
+    hasError = true;
+  } finally {
+    expect(hasError, true);
+  }
+}
+
 void main() {
-  testWidgets('simulates keyboard events', (WidgetTester tester) async {
+  testWidgets('simulates keyboard events (RawEvent)', (WidgetTester tester) async {
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
+
     final List<RawKeyEvent> events = <RawKeyEvent>[];
 
     final FocusNode focusNode = FocusNode();
@@ -51,5 +81,247 @@
 
     await tester.pumpWidget(Container());
     focusNode.dispose();
+
+    debugKeyEventSimulatorTransitModeOverride = null;
+  });
+
+  testWidgets('simulates keyboard events (KeyData then RawKeyEvent)', (WidgetTester tester) async {
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+    final List<KeyEvent> events = <KeyEvent>[];
+
+    final FocusNode focusNode = FocusNode();
+
+    await tester.pumpWidget(
+      KeyboardListener(
+        focusNode: focusNode,
+        onKeyEvent: events.add,
+        child: Container(),
+      ),
+    );
+
+    focusNode.requestFocus();
+    await tester.idle();
+
+    // Key press shiftLeft
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
+    expect(events.length, 1);
+    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.shiftLeft}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.shiftLeft);
+    expect(events.length, 1);
+    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.shiftLeft}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
+    expect(events.length, 1);
+    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    // Key press keyA
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
+    expect(events.length, 1);
+    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyA}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
+    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyA}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
+    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    // Key press numpad1
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad1);
+    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numpad1);
+    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad1);
+    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    // Key press numLock (1st time)
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.numLock);
+    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+    events.clear();
+
+    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numLock);
+    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+    events.clear();
+
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.numLock);
+    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
+    events.clear();
+
+    // Key press numLock (2nd time)
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.numLock);
+    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numLock);
+    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.numLock);
+    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
+    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
+    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
+    events.clear();
+
+    await tester.idle();
+
+    await tester.pumpWidget(Container());
+    focusNode.dispose();
+
+    debugKeyEventSimulatorTransitModeOverride = null;
+  });
+
+  testWidgets('simulates using the correct transit mode: rawKeyData', (WidgetTester tester) async {
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
+
+    final List<Object> events = <Object>[];
+
+    final FocusNode focusNode = FocusNode();
+    await tester.pumpWidget(
+      Focus(
+        focusNode: focusNode,
+        onKey: (FocusNode node, RawKeyEvent event) {
+          events.add(event);
+          return KeyEventResult.ignored;
+        },
+        onKeyEvent: (FocusNode node, KeyEvent event) {
+          events.add(event);
+          return KeyEventResult.ignored;
+        },
+        child: Container(),
+      ),
+    );
+
+    focusNode.requestFocus();
+    await tester.idle();
+
+    // A (physical keyA, logical keyA) is pressed.
+    await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
+    expect(events.length, 2);
+    expect(events[0], isA<KeyEvent>());
+    _verifyKeyEvent<KeyDownEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+    expect(events[1], isA<RawKeyEvent>());
+    _verifyRawKeyEvent<RawKeyDownEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+    events.clear();
+
+    // A (physical keyA, logical keyB) is released.
+    //
+    // Since this event was synthesized and regularized before being sent to
+    // HardwareKeyboard, this event will be accepted.
+    await simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA);
+    expect(events.length, 2);
+    expect(events[0], isA<KeyEvent>());
+    _verifyKeyEvent<KeyUpEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, null);
+    expect(events[1], isA<RawKeyEvent>());
+    _verifyRawKeyEvent<RawKeyUpEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, null);
+    events.clear();
+
+    // Manually switch the transit mode to `keyDataThenRawKeyData`. This will
+    // never happen in real applications so the assertion error can verify that
+    // the transit mode is correctly applied.
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+    await _shouldThrow<AssertionError>(() =>
+      simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA));
+
+    debugKeyEventSimulatorTransitModeOverride = null;
+  });
+
+  testWidgets('simulates using the correct transit mode: keyDataThenRawKeyData', (WidgetTester tester) async {
+    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
+
+    final List<Object> events = <Object>[];
+
+    final FocusNode focusNode = FocusNode();
+    await tester.pumpWidget(
+      Focus(
+        focusNode: focusNode,
+        onKey: (FocusNode node, RawKeyEvent event) {
+          events.add(event);
+          return KeyEventResult.ignored;
+        },
+        onKeyEvent: (FocusNode node, KeyEvent event) {
+          events.add(event);
+          return KeyEventResult.ignored;
+        },
+        child: Container(),
+      ),
+    );
+
+    focusNode.requestFocus();
+    await tester.idle();
+
+    // A (physical keyA, logical keyA) is pressed.
+    await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
+    expect(events.length, 2);
+    expect(events[0], isA<KeyEvent>());
+    _verifyKeyEvent<KeyDownEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+    expect(events[1], isA<RawKeyEvent>());
+    _verifyRawKeyEvent<RawKeyDownEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
+    events.clear();
+
+    // A (physical keyA, logical keyB) is released.
+    //
+    // Since this event is transmitted to HardwareKeyboard as-is, it will be rejected due to
+    // inconsistent logical key. This does not indicate behaviral difference,
+    // since KeyData is will never send malformed data sequence in real applications.
+    await _shouldThrow<AssertionError>(() =>
+      simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA));
+
+    debugKeyEventSimulatorTransitModeOverride = null;
   });
 }