From 7b04665f33cd41eb9219e22712f40916761089c7 Mon Sep 17 00:00:00 2001 From: Yousif Almulla Date: Mon, 9 Mar 2026 05:05:12 -0700 Subject: [PATCH] fix: Android soft keyboard IME input corruption (#13737, #9789, #11073) When the Android soft keyboard is active, key events from the IME carry unreliable physicalKey data (Flutter issue #157771). The RawKeyFocusScope handler was processing these garbled scancodes, desynchronising the hidden TextFormField's text buffer and causing every subsequent keypress to repeat a single character (space or '1'). Changes: - InputModel: add androidSoftKeyboardActive flag; when set, handleKeyEvent returns handled but skips the normal key processing pipeline. Backspace and Enter are sent directly using reliable logicalKey data. - remote_page.dart: set/clear the flag via onSoftKeyboardChanged callback; fix multi-delete counting for Samsung keyboard acceleration. Fixes: rustdesk/rustdesk#13737, rustdesk/rustdesk#9789, rustdesk/rustdesk#11073 --- flutter/lib/mobile/pages/remote_page.dart | 8 +++++-- flutter/lib/models/input_model.dart | 29 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b379a5591..26cdb0b90 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -201,6 +201,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { ); void onSoftKeyboardChanged(bool visible) { + inputModel.androidSoftKeyboardActive = visible; if (!visible) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard @@ -300,8 +301,11 @@ class _RemotePageState extends State with WidgetsBindingObserver { if (newValue.length == oldValue.length) { // ? } else if (newValue.length < oldValue.length) { - final char = 'VK_BACK'; - inputModel.inputKey(char); + // Send exactly one VK_BACK per onChanged callback regardless of how many + // characters the IME removed (Samsung accelerates held-delete). The + // IME's own callback frequency provides a steady, controllable repeat + // rate instead of runaway exponential deletion. + inputModel.inputKey('VK_BACK'); } else { final content = newValue.substring(oldValue.length); if (content.length > 1) { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 675a95e42..7da57bba6 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -367,6 +367,12 @@ class InputModel { bool _pointerMovedAfterEnter = false; bool _pointerInsideImage = false; + /// True while the Android soft keyboard editor is active. + /// When set, key events are ignored so they flow through to the + /// hidden TextFormField's onChanged handler instead of being + /// processed here with potentially incorrect physicalKey data. + bool androidSoftKeyboardActive = false; + // mouse final isPhysicalMouse = false.obs; int _lastButtons = 0; @@ -687,6 +693,29 @@ class InputModel { KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; if (isViewCamera) return KeyEventResult.handled; + // When the Android soft keyboard is active, avoid processing key events + // through the normal input pipeline because physicalKey data from the + // soft keyboard is unreliable (Flutter issue #157771) and can corrupt + // subsequent input, causing every keypress to repeat a single character. + // + // Return `handled` (not `ignored`) so Android keeps sending key-repeat + // events for held keys and the TextFormField does not consume sentinel + // buffer characters. + // + // For Backspace and Enter, send them directly using the reliable logical + // key data. This is required because for some IMEs (ko/zh/ja) returning + // `handled` prevents the IME from processing the key through onChanged. + if (isAndroid && androidSoftKeyboardActive) { + if (e is KeyDownEvent || e is KeyRepeatEvent) { + if (e.logicalKey == LogicalKeyboardKey.backspace) { + inputKey('VK_BACK', press: true); + } else if (e.logicalKey == LogicalKeyboardKey.enter || + e.logicalKey == LogicalKeyboardKey.numpadEnter) { + inputKey('VK_RETURN', press: true); + } + } + return KeyEventResult.handled; + } if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled;