Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 64 additions & 12 deletions maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -264,14 +264,27 @@ class AndroidDriver(

override fun tap(point: Point) {
metrics.measured("operation", mapOf("command" to "tap")) {
runDeviceCall("tap") {
blockingStubWithTimeout.tap(
tapRequest {
x = point.x
y = point.y
}
) ?: throw IllegalStateException("Response can't be null")
}
// Dispatch the tap via `adb shell input tap` rather than the
// on-device gRPC path that calls UiAutomator's
// `InteractionController.clickNoSync(x, y)`. clickNoSync injects
// events via `UiAutomation.injectInputEvent(event, sync=true)`,
// which internally calls the 3-arg overload with
// `waitForAnimations=true`. That flag makes UiAutomation block on
// `waitForAllWindowsDrawn(...)` before delivery. On Android 16
// (API 36) with certain stuck animation states (e.g. the dev-client
// splash dismissal that never signals completion), this wait
// routinely hits 10+ s per tap, surfacing as
// `WindowManager: Timed out waiting for animations to complete`
// warnings every 5 s. See
// https://github.com/mobile-dev-inc/maestro/issues/2718.
//
// `adb shell input` bypasses UiAutomation entirely and injects via
// InputManager directly with no animation wrapper — same
// `INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH` mode on the
// InputManager side, but without the `waitForAllWindowsDrawn` poll
// on top. `longPress`, `swipe`, and `pressKey` in this file already
// take the same approach.
dadb.shell("input tap ${point.x} ${point.y}")
}
}

Expand Down Expand Up @@ -564,10 +577,49 @@ class AndroidDriver(

override fun inputText(text: String) {
metrics.measured("operation", mapOf("command" to "inputText")) {
runDeviceCall("inputText") {
blockingStubWithTimeout.inputText(inputTextRequest {
this.text = text
}) ?: throw IllegalStateException("Input Response can't be null")
// Dispatch text input via `adb shell input text` for the same
// reason `tap` (above) does — the on-device gRPC path injects each
// keystroke through UiAutomation, which waits on
// `waitForAllWindowsDrawn` before delivery and hangs ~10 s/char on
// Android 16 dev clients with stuck window animation state. The
// shell `input text` command bypasses UiAutomation so the per-char
// cost drops from seconds to milliseconds.
//
// One char per `input text` invocation rather than all at once:
// some apps drive multi-field auto-focus inputs (e.g. OTP / PIN
// codes split across one EditText per character) where each input
// event must complete a React onChange → state-update → re-render
// → next field's `requestFocus()` cycle before the next event is
// dispatched, or characters end up in the wrong field. Bundling
// chars into one `input text` invocation injects them faster than
// the JS bridge can re-focus and produces e.g. "CCOC" from typing
// "COMH". Per-char + small inter-char delay keeps OTP-style fields
// happy without losing the orders-of-magnitude speedup over the
// gRPC path (still ~50 ms/char vs the broken ~10 s/char).
//
// Shell escaping caveats:
// - Spaces must be encoded as %s (an `input` quirk)
// - Inner double-quotes, backticks and dollar signs must be
// backslash-escaped so the surrounding "..." doesn't terminate
// - Backslashes themselves need doubling first
// - Newlines and carriage returns are silently dropped to match
// the on-device gRPC path's pre-existing behavior (the device
// `setText` handler had no KEYCODE_ENTER case and silently
// no-op'd them). If a flow needs ENTER, it must call
// `pressKey: ENTER` explicitly — same contract as before.
// - The full ASCII printable range works; arbitrary Unicode may
// fail on the device's `input` binary on some OEMs
text.forEach { ch ->
val escaped = when (ch) {
'\n', '\r' -> return@forEach
'\\' -> "\\\\"
'"' -> "\\\""
'`' -> "\\`"
'$' -> "\\$"
' ' -> "%s"
else -> ch.toString()
}
dadb.shell("input text \"$escaped\"")
}
}
}
Expand Down
Loading