Skip to content

fix(android): bypass UiAutomation for tap and inputText to fix Android 16 animation-wait hang#3334

Open
jesper-irukadx wants to merge 1 commit into
mobile-dev-inc:mainfrom
jesper-irukadx:fix/2718-android-tap-inputtext-uiautomation-bypass
Open

fix(android): bypass UiAutomation for tap and inputText to fix Android 16 animation-wait hang#3334
jesper-irukadx wants to merge 1 commit into
mobile-dev-inc:mainfrom
jesper-irukadx:fix/2718-android-tap-inputtext-uiautomation-bypass

Conversation

@jesper-irukadx

Copy link
Copy Markdown

Proposed changes

Fixes #2718inputText (and tap) hang ~10 s per character on Android 16 with certain dev-client splash states.

Root cause: tap and inputText go through the gRPC path → UiAutomator's InteractionControllerUiAutomation.injectInputEvent(event, sync=true). The 2-arg overload internally calls the 3-arg overload with waitForAnimations=true, which makes UiAutomation block on waitForAllWindowsDrawn(...) before delivery. On Android 16 (API 36), Expo / RN dev clients (and probably others) leave a starting_reveal window animation that never reaches a completion callback. WindowManager logs Timed out waiting for animations to complete every 5 s, and every tap/keystroke eats one of those waits.

This PR replaces the gRPC paths for tap and inputText in AndroidDriver.kt with dadb.shell("input tap …") / dadb.shell("input text …") — the same approach this file already uses for longPress, swipe, and pressKey. adb shell input bypasses UiAutomation entirely and injects via InputManager directly with no animation wrapper, while keeping the same INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH mode on the InputManager side.

Implementation notes

  • inputText is per-char, not all-at-once. Multi-field auto-focus inputs (OTP/PIN codes split across one EditText per character) need each input event to complete a full onChange → state-update → re-render → next field's requestFocus cycle before the next event. Bundling chars into one input text invocation can outrun the JS bridge and produce wrong-field characters (e.g. CCOC from COMH). Per-char keeps OTP fields happy without losing the orders-of-magnitude speedup (still ~50 ms/char vs the broken ~10 s/char).
  • Newline handling preserved. The previous on-device MaestroDriverService.setText had no KEYCODE_ENTER case in its when and silently no-op'd \n / \r. The patched per-char loop matches that exactly. Flows that needed an ENTER keystroke already had to call pressKey: ENTER.
  • Shell escaping handles \, ", `, $, and spaces (%s, an input quirk). Other shell metacharacters are safely contained by the surrounding "...".

Behavioral diff

Aspect Before After
Event delivery UiAutomation.injectInputEvent(sync=true)waitForAnimations=true wrapper → InputManager (WAIT_FOR_FINISH) adb shell inputInputManager (WAIT_FOR_FINISH) directly
Pre-delivery animation wait waitForAllWindowsDrawn(...) (the hang) None
runDeviceCall retry on UNAVAILABLE Yes No (matches longPress / swipe / pressKey)
Tracer.trace(x, y) instrumentation Called Skipped (matches longPress / swipe / pressKey)

Alternatives considered

  • Device-side patch using the 3-arg overload (UiAutomation.injectInputEvent(event, sync, waitForAnimations=false), @hide @TestApi) — architecturally cleaner; preserves all gRPC error semantics and Tracer. More invasive (requires re-bundling maestro-server.apk). Happy to switch this PR to that approach if you prefer.
  • API-36-conditional or env-var-gated rollout — easy to add if you want a narrower change for the first release.

Testing

Measurements

Affected device — Pixel 7a, Android 16 (API 36), Expo dev-client:

Operation Before After
driver.tap ~10,000 ms ~150 ms
driver.inputText (10 chars) DEADLINE (cut at 120 s) ~500 ms
Full 8-screen sign-up flow with OTP and assertions DEADLINE 144 s, all 9 asserts pass
WindowManager: Timed out waiting for animations logs per flow 22+ 0

Healthy device — API 34 emulator, single run (3× coordinate tap, includes runner startup): vanilla 22.5 s vs patched 19.2 s. Not enough samples to characterize a delta; takeaway is no measurable regression.

Suites run locally

  • ./gradlew test — 754 tests across 9 modules, 0 failures, 0 errors
  • ./gradlew :maestro-test:test — integration suite, BUILD SUCCESSFUL
  • ./gradlew detekt — BUILD SUCCESSFUL
  • Manual: full real-device sign-up + onboarding + OTP + assertion flow with ./gradlew :maestro-cli:installDist build

Not done by me (would appreciate maintainer-side coverage):

  • Full CI matrix on API 32 / 35 / 36 emulators
  • Maestro Cloud volume validation of the lost runDeviceCall retry semantics for tap / inputText (the other shell-based methods in this file already lack this retry)

e2e demo tests

The existing e2e/demo_app/ flows exercise tap and inputText heavily, so existing demo coverage validates the change. I haven't added a new demo screen because the change is at the driver level and behavior-preserving on healthy devices; let me know if you'd like a targeted regression flow added.

Issues fixed

Fixes #2718

…d 16 animation-wait hang

UiAutomator's clickNoSync (and the equivalent path used by inputText)
injects events via UiAutomation.injectInputEvent(event, sync=true). The
2-arg overload internally calls the 3-arg overload with
waitForAnimations=true, which makes UiAutomation block on
waitForAllWindowsDrawn(...) before delivery.

On Android 16 (API 36) dev clients (Expo dev-client confirmed; likely
any RN/Hermes splash flow) the system splash-dismissal queues a
starting_reveal animation that never reaches a completion callback.
WindowManager logs "Timed out waiting for animations to complete" every
5 s, and each clickNoSync / inputText keystroke blocks on one of those
waits. ~10 s per tap, ~10 s per character — enough to trip the host-side
120 s gRPC deadline on a typical form.

Replace the gRPC paths for tap and inputText with dadb.shell("input
tap ...") / dadb.shell("input text ..."). This is the same approach
this file already uses for longPress, swipe, and pressKey. adb shell
input bypasses UiAutomation entirely and injects via InputManager
directly with no animation wrapper, while keeping the same
INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH mode on the InputManager side.

inputText sends one char per shell invocation rather than all at once,
because multi-field auto-focus inputs (OTP / PIN codes split across one
EditText per character) need each input event to complete an onChange →
state-update → re-render → next field's requestFocus cycle before the
next event. Bundling chars produces wrong-field characters (e.g. CCOC
from typing COMH). Newlines and carriage returns are silently skipped to
match the on-device gRPC path's pre-existing behavior (MaestroDriverService.setText
had no KEYCODE_ENTER case).

Measurements (Pixel 7a, Android 16):
- tap: 10,000 ms → 150 ms
- inputText (10 chars): DEADLINE → 500 ms
- Full sign-up flow with 8 form fields + OTP: DEADLINE → 144 s, all asserts pass

No regression observed on API 34 emulator.

Fixes mobile-dev-inc#2718
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.

inputText extremely slow on Android 16

1 participant