fix: Android soft keyboard input corruption - every key becomes space or 1#14495
fix: Android soft keyboard input corruption - every key becomes space or 1#14495almullay wants to merge 1 commit intorustdesk:masterfrom
Conversation
…desk#9789, rustdesk#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#13737, rustdesk#9789, rustdesk#11073
📝 WalkthroughWalkthroughThis PR improves Android soft keyboard input reliability by tracking keyboard visibility state and handling Backspace/Enter keys through a dedicated logical keycode path instead of unreliable physical keycodes from the soft keyboard. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@flutter/lib/models/input_model.dart`:
- Around line 696-718: The current logic short-circuits key events when
androidSoftKeyboardActive is true, which can mistakenly suppress hardware
keyboard input if a user connects a Bluetooth keyboard while the soft keyboard
remains visible; test the scenario (soft keyboard visible + Bluetooth/hardware
keyboard attached) and, if reproducible, modify detection to re-evaluate actual
hardware presence before returning KeyEventResult.handled (e.g., refresh or
debounce the androidSoftKeyboardActive flag after _disableAndroidSoftKeyboard()
calls or when input device changes), ensure backspace/enter handling via
inputKey('VK_BACK' / 'VK_RETURN') still occurs, and verify TextFormField
onChanged continues receiving input through the platform text input channel.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1be2d599-7985-4bc3-b3a2-57f6ccbab700
📒 Files selected for processing (2)
flutter/lib/mobile/pages/remote_page.dartflutter/lib/models/input_model.dart
| // 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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Flutter Android KeyEvent soft keyboard vs hardware keyboard: is there a supported way to distinguish IME-generated key events from Bluetooth/USB hardware keyboard events, and do parent Focus.onKeyEvent handlers still receive those events while a child TextField has focus?
💡 Result:
1) IME (soft keyboard) vs hardware keyboard: can Flutter reliably distinguish?
No—there isn’t a supported, reliable way in Flutter to distinguish “IME-generated keystrokes” from hardware keyboard events, mainly because Flutter’s key event pipeline is not intended to represent IME/soft-keyboard text entry. Flutter explicitly notes that Focus.onKeyEvent is not the way to get text input and “doesn’t support soft keyboards in general” (i.e., IME goes through the text-input/editing channel instead). [1]
What you can access in Flutter key events is limited:
KeyEvent.deviceTypeexists, but it’s only a coarse “source device type” and “not all platforms supply an accurate type,” defaulting tokeyboard. [2]- Older Android-only raw APIs exposed more platform source info (e.g.,
RawKeyEventDataAndroid.eventSource), but that was migrated into the coarseKeyEvent.deviceType, not into a “virtual/IME vs physical” indicator. [3]
If you truly need this distinction on Android, it generally requires native handling (platform code) using Android’s input APIs. For example, Android can represent a virtual input device and expose that via InputDevice.isVirtual() (e.g., the virtual keyboard). [4] Flutter’s KeyEvent API does not expose enough Android InputDevice identity/details to do that purely in Dart.
Also, at the Android level, there’s no guarantee that soft keyboard actions even produce KeyEvents—IMEs commonly commit text via the input connection rather than dispatching key events, and Flutter’s Android embedding treats key events and text input as separate stages. [5]
2) Do parent Focus.onKeyEvent handlers still receive events while a child TextField has focus?
Yes, for hardware key events, but only if the focused child doesn’t handle them.
Key-event propagation works like this:
- The event is first delivered to the node with primary focus (e.g., the
TextField’s focus node). [1] - If that handler returns
KeyEventResult.ignored, the event bubbles up to ancestor focus nodes in order. [1] - If a focused descendant returns
KeyEventResult.handled, ancestors won’t receive it. TheFocusdocs state ancestoronKeyEventruns only “unless a focused descendant’s … callback returnedKeyEventResult.handled”. [6]
So in practice: with a focused TextField, many keys (typing, editing/navigation keys) are typically handled by the text field/editable text system, so your parent handler often won’t see them—unless the text field ignores that particular key.
Sources: Flutter focus/key propagation docs [1], KeyEvent.deviceType docs [2], Flutter key-event migration note about Android eventSource → deviceType [3], Android InputDevice.isVirtual() [4], Flutter Android embedding keyboard/text-input staging [5], Focus widget doc re descendant handling [6].
🏁 Script executed:
# Check the code at lines 696-718 and surrounding context
cd flutter && cat -n lib/models/input_model.dart | sed -n '690,725p'Repository: rustdesk/rustdesk
Length of output: 1845
🏁 Script executed:
# Check lines 370-374 where androidSoftKeyboardActive is defined
cd flutter && cat -n lib/models/input_model.dart | sed -n '365,380p'Repository: rustdesk/rustdesk
Length of output: 710
🏁 Script executed:
# Check how androidSoftKeyboardActive is being set in remote_page.dart
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '1,50p'Repository: rustdesk/rustdesk
Length of output: 2361
🏁 Script executed:
# Look for the specific section mentioned in the review scratchpad (203-239)
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '200,245p'Repository: rustdesk/rustdesk
Length of output: 2127
🏁 Script executed:
# Check the remote_input.dart routing mentioned (16-52)
cd flutter && cat -n lib/common/widgets/remote_input.dart | sed -n '10,55p'Repository: rustdesk/rustdesk
Length of output: 1914
🏁 Script executed:
cd flutter && grep -n "TextFormField" lib/mobile/pages/remote_page.dart | head -20Repository: rustdesk/rustdesk
Length of output: 99
🏁 Script executed:
cd flutter && grep -n "_disableAndroidSoftKeyboard" lib/mobile/pages/remote_page.dartRepository: rustdesk/rustdesk
Length of output: 292
🏁 Script executed:
cd flutter && grep -n "enableSoftKeyboard\|enable_soft_keyboard" lib/mobile/pages/remote_page.dartRepository: rustdesk/rustdesk
Length of output: 490
🏁 Script executed:
cd flutter && grep -rn "hidden.*TextFormField\|TextFormField.*hidden" lib/mobile/pages/ --include="*.dart"Repository: rustdesk/rustdesk
Length of output: 43
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '610,630p'Repository: rustdesk/rustdesk
Length of output: 1337
🏁 Script executed:
cd flutter && grep -B5 -A10 "TextFormField" lib/mobile/pages/remote_page.dart | head -30Repository: rustdesk/rustdesk
Length of output: 793
🏁 Script executed:
cd flutter && grep -n "onChanged" lib/mobile/pages/remote_page.dart | grep -i "textform\|_value"Repository: rustdesk/rustdesk
Length of output: 43
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '625,655p'Repository: rustdesk/rustdesk
Length of output: 1657
🏁 Script executed:
cd flutter && grep -n "onChanged.*_handleIOSSoftKeyboardInput\|_handleIOSSoftKeyboardInput" lib/mobile/pages/remote_page.dartRepository: rustdesk/rustdesk
Length of output: 167
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '240,270p'Repository: rustdesk/rustdesk
Length of output: 1373
🏁 Script executed:
cd flutter && grep -n "handleSoftKeyboardInput" lib/mobile/pages/remote_page.dartRepository: rustdesk/rustdesk
Length of output: 176
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '330,345p'Repository: rustdesk/rustdesk
Length of output: 495
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '120,130p'Repository: rustdesk/rustdesk
Length of output: 466
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '1360,1365p'Repository: rustdesk/rustdesk
Length of output: 264
🏁 Script executed:
cd flutter && cat -n lib/mobile/pages/remote_page.dart | sed -n '1380,1385p'Repository: rustdesk/rustdesk
Length of output: 241
Consider testing whether hardware keyboards work while the soft keyboard is visible.
While androidSoftKeyboardActive does cause key events to return KeyEventResult.handled (suppressing normal input pipeline), the codebase already calls _disableAndroidSoftKeyboard() at startup and after dialogs close to prevent the soft keyboard from being active when it shouldn't be. However, if a user opens the soft keyboard and then plugs in a Bluetooth keyboard, the androidSoftKeyboardActive flag may not reflect actual keyboard presence.
Unfortunately, Flutter provides no supported way in Dart to distinguish IME-generated key events from hardware keyboard events—that would require native Android code with InputDevice.isVirtual(). The TextFormField's onChanged handler should still receive input through the platform text input channel, but key-based input may be affected. If this is a real issue in practice, consider:
- Testing the scenario (soft keyboard visible + hardware keyboard active)
- Improving keyboard visibility detection if the timing is off
- Using native Android platform code if you need reliable hardware/virtual keyboard distinction
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@flutter/lib/models/input_model.dart` around lines 696 - 718, The current
logic short-circuits key events when androidSoftKeyboardActive is true, which
can mistakenly suppress hardware keyboard input if a user connects a Bluetooth
keyboard while the soft keyboard remains visible; test the scenario (soft
keyboard visible + Bluetooth/hardware keyboard attached) and, if reproducible,
modify detection to re-evaluate actual hardware presence before returning
KeyEventResult.handled (e.g., refresh or debounce the androidSoftKeyboardActive
flag after _disableAndroidSoftKeyboard() calls or when input device changes),
ensure backspace/enter handling via inputKey('VK_BACK' / 'VK_RETURN') still
occurs, and verify TextFormField onChanged continues receiving input through the
platform text input channel.
Bug Description
When connecting from Android to any remote host (Windows, Linux, etc.), the Android soft keyboard can enter a broken state where every keypress sends the same character — typically space or the number
1, regardless of which key is actually pressed.Reliable reproduction (from issue #13737)
The workaround is to close and reopen the keyboard, but the bug returns easily.
Affected versions
Reported since v1.3.2 (worked in v1.3.1). Still present in v1.4.5.
Related issues
Root Cause
On Android, the soft keyboard's
physicalKey.usbHidUsagedata is unreliable for certain IME-generated key events (see Flutter issue #157771). The existing code comments ininput_model.dartalready document this for Backspace and Enter specifically.The problem:
RawKeyFocusScopewraps the entire remote page, soInputModel.handleKeyEvent()processes all key events — including ones from the soft keyboard with garbled physical key data. When the soft keyboard is active, this corrupts the hiddenTextFormField's text buffer state, causing the diff logic inhandleSoftKeyboardInput()to desynchronise. Once desynced, every subsequent keypress repeats whatever stale scancode was last processed.Fix
Two changes in
InputModel.handleKeyEvent()andremote_page.dart:1.
InputModel— skip normal key processing when Android soft keyboard is activeAdded an
androidSoftKeyboardActiveflag. When set,handleKeyEvent()returnsKeyEventResult.handledbut skips the normal input pipeline (no garbled scancodes sent). Backspace and Enter are sent directly using the reliablelogicalKeydata, since some IMEs (ko/zh/ja) require this.Returns
handled(notignored) so Android keeps sending key-repeat events for held keys.2.
remote_page.dart— wire up the flagonSoftKeyboardChanged()sets/clearsinputModel.androidSoftKeyboardActivewhen the keyboard opens/closes.What this preserves
TextFormFieldtext-diff approach for soft keyboard input (handleSoftKeyboardInput) continues to handle all character inputinitTextsentinel buffer trick still worksTested on
Files changed
flutter/lib/models/input_model.dart—androidSoftKeyboardActiveflag + early return inhandleKeyEvent()flutter/lib/mobile/pages/remote_page.dart— set/clear flag inonSoftKeyboardChanged()Summary by CodeRabbit