X Tutup
Skip to content

fix: Android soft keyboard input corruption - every key becomes space or 1#14495

Open
almullay wants to merge 1 commit intorustdesk:masterfrom
almullay:fix/android-keyboard-ime
Open

fix: Android soft keyboard input corruption - every key becomes space or 1#14495
almullay wants to merge 1 commit intorustdesk:masterfrom
almullay:fix/android-keyboard-ime

Conversation

@almullay
Copy link

@almullay almullay commented Mar 9, 2026

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)

  1. Connect from Android to a remote PC
  2. Open the soft keyboard
  3. Press Backspace/Delete immediately before typing anything
  4. Every subsequent keypress now produces a single repeated character

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.usbHidUsage data is unreliable for certain IME-generated key events (see Flutter issue #157771). The existing code comments in input_model.dart already document this for Backspace and Enter specifically.

The problem: RawKeyFocusScope wraps the entire remote page, so InputModel.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 hidden TextFormField's text buffer state, causing the diff logic in handleSoftKeyboardInput() to desynchronise. Once desynced, every subsequent keypress repeats whatever stale scancode was last processed.

Fix

Two changes in InputModel.handleKeyEvent() and remote_page.dart:

1. InputModel — skip normal key processing when Android soft keyboard is active

Added an androidSoftKeyboardActive flag. When set, handleKeyEvent() returns KeyEventResult.handled but skips the normal input pipeline (no garbled scancodes sent). Backspace and Enter are sent directly using the reliable logicalKey data, since some IMEs (ko/zh/ja) require this.

Returns handled (not ignored) so Android keeps sending key-repeat events for held keys.

2. remote_page.dart — wire up the flag

onSoftKeyboardChanged() sets/clears inputModel.androidSoftKeyboardActive when the keyboard opens/closes.

What this preserves

  • Physical keyboard input still works normally (flag is only set when soft keyboard is visible)
  • The existing TextFormField text-diff approach for soft keyboard input (handleSoftKeyboardInput) continues to handle all character input
  • The initText sentinel buffer trick still works
  • Korean/Chinese/Japanese IME composing behavior is unaffected
  • iOS behavior is completely untouched

Tested on

  • Samsung Galaxy (One UI, Samsung Keyboard + Gboard)
  • Android → Windows remote connection
  • Verified: original bug no longer reproduces, normal typing works, held-delete works at controlled rate

Files changed

  • flutter/lib/models/input_model.dartandroidSoftKeyboardActive flag + early return in handleKeyEvent()
  • flutter/lib/mobile/pages/remote_page.dart — set/clear flag in onSoftKeyboardChanged()

Summary by CodeRabbit

  • Bug Fixes
    • Improved Android soft keyboard input reliability, ensuring backspace and enter keys are processed consistently without duplicate inputs
    • Enhanced soft keyboard visibility tracking on Android devices for more accurate input state management

…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
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Android Soft Keyboard Input Handling
flutter/lib/mobile/pages/remote_page.dart, flutter/lib/models/input_model.dart
Adds tracking of Android soft keyboard visibility via new androidSoftKeyboardActive field in InputModel. Modifies key event handling to intercept Backspace and Enter keys when soft keyboard is active, sending reliable VK_BACK/VK_RETURN logical keycodes instead of relying on physical keycodes. Updates RemotePage to set the visibility flag via onSoftKeyboardChanged callback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A keyboard soft now tracked with care,
The Android keys find their true pair,
No more confusion, backspace rings clear,
With logical codes, the input draws near,
Flutter's flutter becomes precise cheer!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: addressing Android soft keyboard input corruption where keypresses become corrupted characters. This directly matches the primary objective of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between db3f5fe and 7b04665.

📒 Files selected for processing (2)
  • flutter/lib/mobile/pages/remote_page.dart
  • flutter/lib/models/input_model.dart

Comment on lines +696 to +718
// 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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.deviceType exists, but it’s only a coarse “source device type” and “not all platforms supply an accurate type,” defaulting to keyboard. [2]
  • Older Android-only raw APIs exposed more platform source info (e.g., RawKeyEventDataAndroid.eventSource), but that was migrated into the coarse KeyEvent.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. The Focus docs state ancestor onKeyEvent runs only “unless a focused descendant’s … callback returned KeyEventResult.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 eventSourcedeviceType [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 -20

Repository: rustdesk/rustdesk

Length of output: 99


🏁 Script executed:

cd flutter && grep -n "_disableAndroidSoftKeyboard" lib/mobile/pages/remote_page.dart

Repository: rustdesk/rustdesk

Length of output: 292


🏁 Script executed:

cd flutter && grep -n "enableSoftKeyboard\|enable_soft_keyboard" lib/mobile/pages/remote_page.dart

Repository: 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 -30

Repository: 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.dart

Repository: 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.dart

Repository: 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:

  1. Testing the scenario (soft keyboard visible + hardware keyboard active)
  2. Improving keyboard visibility detection if the timing is off
  3. 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.

@rustdesk rustdesk requested a review from fufesou March 9, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

X Tutup