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
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes
Fixes #2718 —
inputText(andtap) hang ~10 s per character on Android 16 with certain dev-client splash states.Root cause:
tapandinputTextgo through the gRPC path → UiAutomator'sInteractionController→UiAutomation.injectInputEvent(event, sync=true). The 2-arg overload internally calls the 3-arg overload withwaitForAnimations=true, which makes UiAutomation block onwaitForAllWindowsDrawn(...)before delivery. On Android 16 (API 36), Expo / RN dev clients (and probably others) leave astarting_revealwindow animation that never reaches a completion callback. WindowManager logsTimed out waiting for animations to completeevery 5 s, and every tap/keystroke eats one of those waits.This PR replaces the gRPC paths for
tapandinputTextinAndroidDriver.ktwithdadb.shell("input tap …")/dadb.shell("input text …")— the same approach this file already uses forlongPress,swipe, andpressKey.adb shell inputbypasses UiAutomation entirely and injects viaInputManagerdirectly with no animation wrapper, while keeping the sameINJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISHmode on the InputManager side.Implementation notes
inputTextis 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 fullonChange → state-update → re-render → next field's requestFocuscycle before the next event. Bundling chars into oneinput textinvocation can outrun the JS bridge and produce wrong-field characters (e.g.CCOCfromCOMH). Per-char keeps OTP fields happy without losing the orders-of-magnitude speedup (still ~50 ms/char vs the broken ~10 s/char).MaestroDriverService.setTexthad noKEYCODE_ENTERcase in itswhenand silently no-op'd\n/\r. The patched per-char loop matches that exactly. Flows that needed an ENTER keystroke already had to callpressKey: ENTER.\,",`,$, and spaces (%s, aninputquirk). Other shell metacharacters are safely contained by the surrounding"...".Behavioral diff
UiAutomation.injectInputEvent(sync=true)→waitForAnimations=truewrapper →InputManager(WAIT_FOR_FINISH)adb shell input→InputManager(WAIT_FOR_FINISH) directlywaitForAllWindowsDrawn(...)(the hang)runDeviceCallretry onUNAVAILABLElongPress/swipe/pressKey)Tracer.trace(x, y)instrumentationlongPress/swipe/pressKey)Alternatives considered
UiAutomation.injectInputEvent(event, sync, waitForAnimations=false),@hide @TestApi) — architecturally cleaner; preserves all gRPC error semantics andTracer. More invasive (requires re-bundlingmaestro-server.apk). Happy to switch this PR to that approach if you prefer.Testing
Measurements
Affected device — Pixel 7a, Android 16 (API 36), Expo dev-client:
driver.tapdriver.inputText(10 chars)WindowManager: Timed out waiting for animationslogs per flowHealthy 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./gradlew :maestro-cli:installDistbuildNot done by me (would appreciate maintainer-side coverage):
runDeviceCallretry semantics fortap/inputText(the other shell-based methods in this file already lack this retry)e2e demo tests
The existing
e2e/demo_app/flows exercisetapandinputTextheavily, 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