From e435b1ae262413894fa5e73c34da0ca717ddda4c Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Tue, 2 May 2023 17:15:07 +0200 Subject: [PATCH] Add debugPrintKeyboardEvents flag (#125629) ## Description This PR adds a new debug flag named `debugPrintKeyboardEvents` to help debugging keyboard issues. Keyboard code has some useful asserts but sometimes an assertion failure is related to the handling of previous key events. This debug flag will help understanding the flow of key events which leads to an assertion failure. ## Related Issue Fixes https://github.com/flutter/flutter/issues/125627 ## Tests Adds 1 test. --- packages/flutter/lib/src/services/debug.dart | 10 +++- .../lib/src/services/hardware_keyboard.dart | 50 +++++++++++++++++++ .../test/services/hardware_keyboard_test.dart | 22 +++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/services/debug.dart b/packages/flutter/lib/src/services/debug.dart index b3581119f8..518c29c535 100644 --- a/packages/flutter/lib/src/services/debug.dart +++ b/packages/flutter/lib/src/services/debug.dart @@ -27,6 +27,14 @@ KeyDataTransitMode? debugKeyEventSimulatorTransitModeOverride; /// Flutter to the host platform, "down" is the host platform to flutter. bool debugProfilePlatformChannels = false; +/// Setting to true will cause extensive logging to occur when key events are +/// received. +/// +/// Can be used to debug keyboard issues: each time a key event is received on +/// the framework side, the event details and the current pressed state will +/// be printed. +bool debugPrintKeyboardEvents = false; + /// 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 @@ -38,7 +46,7 @@ bool debugAssertAllServicesVarsUnset(String reason) { if (debugKeyEventSimulatorTransitModeOverride != null) { throw FlutterError(reason); } - if (debugProfilePlatformChannels) { + if (debugProfilePlatformChannels || debugPrintKeyboardEvents) { throw FlutterError(reason); } return true; diff --git a/packages/flutter/lib/src/services/hardware_keyboard.dart b/packages/flutter/lib/src/services/hardware_keyboard.dart index 414b9c8c21..9f12f723c6 100644 --- a/packages/flutter/lib/src/services/hardware_keyboard.dart +++ b/packages/flutter/lib/src/services/hardware_keyboard.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'binding.dart'; +import 'debug.dart'; import 'raw_keyboard.dart'; import 'system_channels.dart'; @@ -17,6 +18,41 @@ export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder; export 'keyboard_key.g.dart' show LogicalKeyboardKey, PhysicalKeyboardKey; export 'raw_keyboard.dart' show RawKeyEvent, RawKeyboard; +// When using _keyboardDebug, always call it like so: +// +// assert(_keyboardDebug(() => 'Blah $foo')); +// +// It needs to be inside the assert in order to be removed in release mode, and +// it needs to use a closure to generate the string in order to avoid string +// interpolation when debugPrintKeyboardEvents is false. +// +// It will throw a StateError if you try to call it when the app is in release +// mode. +bool _keyboardDebug( + String Function() messageFunc, [ + Iterable Function()? detailsFunc, +]) { + if (kReleaseMode) { + throw StateError( + '_keyboardDebug was called in Release mode, which means they are called ' + 'without being wrapped in an assert. Always call _keyboardDebug like so:\n' + r" assert(_keyboardDebug(() => 'Blah $foo'));" + ); + } + if (!debugPrintKeyboardEvents) { + return true; + } + debugPrint('KEYBOARD: ${messageFunc()}'); + final Iterable details = detailsFunc?.call() ?? const []; + if (details.isNotEmpty) { + for (final Object detail in details) { + debugPrint(' $detail'); + } + } + // Return true so that it can be used inside of an assert. + return true; +} + /// 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, @@ -546,9 +582,22 @@ class HardwareKeyboard { return handled; } + List _debugPressedKeysDetails() { + if (_pressedKeys.isEmpty) { + return ['Empty']; + } + final List details = []; + for (final PhysicalKeyboardKey physicalKey in _pressedKeys.keys) { + details.add('$physicalKey: ${_pressedKeys[physicalKey]}'); + } + return details; + } + /// Process a new [KeyEvent] by recording the state changes and dispatching /// to handlers. bool handleKeyEvent(KeyEvent event) { + assert(_keyboardDebug(() => 'Key event received: $event')); + assert(_keyboardDebug(() => 'Pressed state before processing the event:', _debugPressedKeysDetails)); _assertEventIsRegular(event); final PhysicalKeyboardKey physicalKey = event.physicalKey; final LogicalKeyboardKey logicalKey = event.logicalKey; @@ -568,6 +617,7 @@ class HardwareKeyboard { // Empty } + assert(_keyboardDebug(() => 'Pressed state after processing the event:', _debugPressedKeysDetails)); return _dispatchKeyEvent(event); } diff --git a/packages/flutter/test/services/hardware_keyboard_test.dart b/packages/flutter/test/services/hardware_keyboard_test.dart index 6c6b13f081..b9522292a7 100644 --- a/packages/flutter/test/services/hardware_keyboard_test.dart +++ b/packages/flutter/test/services/hardware_keyboard_test.dart @@ -465,10 +465,28 @@ void main() { // trigger assertions. expect(record, isNull); }, variant: KeySimulatorTransitModeVariant.all()); + + testWidgets('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async { + final bool oldDebugPrintKeyboardEvents = debugPrintKeyboardEvents; + final DebugPrintCallback oldDebugPrint = debugPrint; + final StringBuffer messages = StringBuffer(); + debugPrint = (String? message, {int? wrapWidth}) { + messages.writeln(message ?? ''); + }; + debugPrintKeyboardEvents = true; + try { + await simulateKeyDownEvent(LogicalKeyboardKey.keyA); + } finally { + debugPrintKeyboardEvents = oldDebugPrintKeyboardEvents; + debugPrint = oldDebugPrint; + } + final String messagesStr = messages.toString(); + expect(messagesStr, contains('KEYBOARD: Key event received: ')); + expect(messagesStr, contains('KEYBOARD: Pressed state before processing the event:')); + expect(messagesStr, contains('KEYBOARD: Pressed state after processing the event:')); + }); } - - Future _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async { final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError; FlutterError.onError = onError;