Intermittently, on the CI emulator the launched activity window has has-window-focus=false for the whole instrumented run, so Espresso's RootViewPicker (used by onView(...).check(), Intents.intended(), Espresso.pressBack(), and focus-dependent clipboard reads) waits 10s for a focused root and times out with RootViewWithoutFocusException. When it happens it fails every window-focus-dependent test at once while the ~280 pure-Compose semantics tests (which don't need window focus) pass.
Evidence from a failing E2E (35) leg (PR #470, run 28985259521):
Across the entire captured logcat, has-window-focus=true appears ZERO times — the app window never gains focus for the session.
Both the first attempt (test pid 4393) and the once-retry (pid 6278) fail identically → a persistent environmental state, not a per-test transient.
The failures are test-type-correlated, not time- or class-correlated: within each affected class the non-RootViewPicker tests pass (e.g. AccountPickerScreenTest 4 tests / 1 failure), and failures span the whole run (accountsetup early → reporting late).
The stack traces confirm the mechanism — even Intents.intended() funnels through ViewInteraction.check() → RootViewPicker.
The prior mitigation was a single fire-and-forget adb shell input keyevent 82 (MENU) right after sys.boot_completed=1. On modern Android (API 30+) MENU no longer dismisses the keyguard, and when delivered before SystemUI/keyguard finishes coming up it is simply dropped ("no focused window"). The insecure keyguard / non-interactive display then persists and no app window ever takes focus — hence the intermittent, whole-leg flake. (The [EmulatorConsole]: Failed to start Emulator console for 5554 line is the unrelated telnet-5554 auth-console warning, not an input/window-focus issue.)
Fix
A single shared helper — .github/scripts/emulator_focus_gate.py — invoked identically by both E2E jobs (the e2e API 29–36 matrix ande2e-preview API 37) and by the local preflight runners (local_instrumented.py / api37_e2e.py), so it cannot drift between them. After boot it:
Wakes the display (KEYCODE_WAKEUP — never toggles it off the way POWER would).
Dismisses + disables the keyguard (wm dismiss-keyguard, locksettings set-disabled true).
Keeps it awake (svc power stayon true + max screen_off_timeout).
Disables animations (parity with the matrix's old inline lines — and now applied to e2e-preview too, uniformly).
Gates: polls dumpsys power/dumpsys window until the device is interactive (mWakefulness=Awake) and a real window holds input focus (mCurrentFocus non-null) — the exact precondition RootViewPicker needs — re-nudging each iteration so a lost race self-heals.
The gate is soft (bounded wait, then proceeds with a ::warning:: + final device state so a genuine environmental failure is diagnosable from the step log) and non-fatal (|| true), preserving #454's guarantee that the unlock never aborts the boot. It leaves #454's manual boot, #460's path-filter, and #464's wedge-capture untouched.
Coverage
e2e matrix (API 29–36): replaces the inline keyevent 82 + animation-disable with the gate call.
e2e-preview (API 37, both shards): replaces the lone keyevent 82 with the gate call (also gains animation-disable).
Local preflight (local_instrumented.py, api37_e2e.py): route their keyguard step through the same helper, so local E2E exercises the identical fix.
Tests / validation
test_emulator_focus_gate.py unit-tests the pure readiness parser (19 tests, incl. the exact "awake but mCurrentFocus=null" flake signature → not ready), run by the existing traffic-control-tests job.
YAML validated (PyYAML parse + actionlint exit 0). Python compiles; SPDX headers on new files.
Determinism is self-validated by this PR's own matrix run — the previously-flaky Espresso legs must now pass. Recommend re-running the affected leg(s) 2–3× to confirm the intermittency is gone.
## Root cause
Intermittently, on the CI emulator the launched activity window has `has-window-focus=false` for the **whole** instrumented run, so Espresso's `RootViewPicker` (used by `onView(...).check()`, `Intents.intended()`, `Espresso.pressBack()`, and focus-dependent clipboard reads) waits 10s for a focused root and times out with `RootViewWithoutFocusException`. When it happens it fails **every** window-focus-dependent test at once while the ~280 pure-Compose semantics tests (which don't need window focus) pass.
Evidence from a failing `E2E (35)` leg (PR #470, run `28985259521`):
- Across the entire captured logcat, **`has-window-focus=true` appears ZERO times** — the app window never gains focus for the session.
- **Both** the first attempt (test pid 4393) **and** the once-retry (pid 6278) fail identically → a persistent environmental state, not a per-test transient.
- The failures are **test-type-correlated, not time- or class-correlated**: within each affected class the non-RootViewPicker tests pass (e.g. `AccountPickerScreenTest` 4 tests / 1 failure), and failures span the whole run (accountsetup early → reporting late).
- The stack traces confirm the mechanism — even `Intents.intended()` funnels through `ViewInteraction.check()` → `RootViewPicker`.
The prior mitigation was a single fire-and-forget `adb shell input keyevent 82` (MENU) right after `sys.boot_completed=1`. On modern Android (API 30+) MENU no longer dismisses the keyguard, and when delivered before SystemUI/keyguard finishes coming up it is simply dropped ("no focused window"). The insecure keyguard / non-interactive display then persists and **no app window ever takes focus** — hence the intermittent, whole-leg flake. (The `[EmulatorConsole]: Failed to start Emulator console for 5554` line is the unrelated telnet-5554 auth-console warning, not an input/window-focus issue.)
## Fix
A single shared helper — **`.github/scripts/emulator_focus_gate.py`** — invoked **identically** by both E2E jobs (the `e2e` API 29–36 matrix **and** `e2e-preview` API 37) and by the local preflight runners (`local_instrumented.py` / `api37_e2e.py`), so it cannot drift between them. After boot it:
1. **Wakes** the display (`KEYCODE_WAKEUP` — never toggles it off the way POWER would).
2. **Dismisses + disables** the keyguard (`wm dismiss-keyguard`, `locksettings set-disabled true`).
3. **Keeps it awake** (`svc power stayon true` + max `screen_off_timeout`).
4. **Disables animations** (parity with the matrix's old inline lines — and now applied to `e2e-preview` too, uniformly).
5. **Gates**: polls `dumpsys power`/`dumpsys window` until the device is interactive (`mWakefulness=Awake`) **and** a real window holds input focus (`mCurrentFocus` non-null) — the exact precondition `RootViewPicker` needs — re-nudging each iteration so a lost race self-heals.
The gate is **soft** (bounded wait, then proceeds with a `::warning::` + final device state so a genuine environmental failure is diagnosable from the step log) and **non-fatal** (`|| true`), preserving #454's guarantee that the unlock never aborts the boot. It leaves #454's manual boot, #460's path-filter, and #464's wedge-capture untouched.
## Coverage
- `e2e` matrix (API 29–36): replaces the inline `keyevent 82` + animation-disable with the gate call.
- `e2e-preview` (API 37, both shards): replaces the lone `keyevent 82` with the gate call (also gains animation-disable).
- Local preflight (`local_instrumented.py`, `api37_e2e.py`): route their keyguard step through the same helper, so local E2E exercises the identical fix.
## Tests / validation
- `test_emulator_focus_gate.py` unit-tests the pure readiness parser (19 tests, incl. the exact "awake but `mCurrentFocus=null`" flake signature → not ready), run by the existing `traffic-control-tests` job.
- YAML validated (PyYAML parse + `actionlint` exit 0). Python compiles; SPDX headers on new files.
- **Determinism** is self-validated by this PR's own matrix run — the previously-flaky Espresso legs must now pass. Recommend re-running the affected leg(s) 2–3× to confirm the intermittency is gone.
Closes #468
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Root cause
Intermittently, on the CI emulator the launched activity window has
has-window-focus=falsefor the whole instrumented run, so Espresso'sRootViewPicker(used byonView(...).check(),Intents.intended(),Espresso.pressBack(), and focus-dependent clipboard reads) waits 10s for a focused root and times out withRootViewWithoutFocusException. When it happens it fails every window-focus-dependent test at once while the ~280 pure-Compose semantics tests (which don't need window focus) pass.Evidence from a failing
E2E (35)leg (PR #470, run28985259521):has-window-focus=trueappears ZERO times — the app window never gains focus for the session.AccountPickerScreenTest4 tests / 1 failure), and failures span the whole run (accountsetup early → reporting late).Intents.intended()funnels throughViewInteraction.check()→RootViewPicker.The prior mitigation was a single fire-and-forget
adb shell input keyevent 82(MENU) right aftersys.boot_completed=1. On modern Android (API 30+) MENU no longer dismisses the keyguard, and when delivered before SystemUI/keyguard finishes coming up it is simply dropped ("no focused window"). The insecure keyguard / non-interactive display then persists and no app window ever takes focus — hence the intermittent, whole-leg flake. (The[EmulatorConsole]: Failed to start Emulator console for 5554line is the unrelated telnet-5554 auth-console warning, not an input/window-focus issue.)Fix
A single shared helper —
.github/scripts/emulator_focus_gate.py— invoked identically by both E2E jobs (thee2eAPI 29–36 matrix ande2e-previewAPI 37) and by the local preflight runners (local_instrumented.py/api37_e2e.py), so it cannot drift between them. After boot it:KEYCODE_WAKEUP— never toggles it off the way POWER would).wm dismiss-keyguard,locksettings set-disabled true).svc power stayon true+ maxscreen_off_timeout).e2e-previewtoo, uniformly).dumpsys power/dumpsys windowuntil the device is interactive (mWakefulness=Awake) and a real window holds input focus (mCurrentFocusnon-null) — the exact preconditionRootViewPickerneeds — re-nudging each iteration so a lost race self-heals.The gate is soft (bounded wait, then proceeds with a
::warning::+ final device state so a genuine environmental failure is diagnosable from the step log) and non-fatal (|| true), preserving #454's guarantee that the unlock never aborts the boot. It leaves #454's manual boot, #460's path-filter, and #464's wedge-capture untouched.Coverage
e2ematrix (API 29–36): replaces the inlinekeyevent 82+ animation-disable with the gate call.e2e-preview(API 37, both shards): replaces the lonekeyevent 82with the gate call (also gains animation-disable).local_instrumented.py,api37_e2e.py): route their keyguard step through the same helper, so local E2E exercises the identical fix.Tests / validation
test_emulator_focus_gate.pyunit-tests the pure readiness parser (19 tests, incl. the exact "awake butmCurrentFocus=null" flake signature → not ready), run by the existingtraffic-control-testsjob.actionlintexit 0). Python compiles; SPDX headers on new files.Closes #468
Merge Queue Status
2026-07-09 01:54 UTC· Rule:default· triggered by merge protections2026-07-09 02:19 UTC· at58447d7d12f17f2bec660394c93da53b9c7d1b8e· mergeThis pull request spent 25 minutes 11 seconds in the queue, including 24 minutes 46 seconds running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed