The root cause: an ANR dialog nobody was looking at
Not SAF, not root discovery, not the picker. The launcher ANRs on a loaded runner
emulator, and the "Application Not Responding" dialog it leaves behind never goes
away. That dialog belongs to system_server, and it is opaque and fullscreen — so AccessibilityWindowManager drops every application window beneath it. The app is Displayed and unreadable at the same time, which is the contradiction that made
this look like a SAF bug across six PRs.
ANR in com.google.android.apps.nexuslauncher (com.google.android.apps.nexuslauncher/.NexusLauncherActivity)
Reason: Input dispatching timed out (Application does not have a focused window)
Window{4ed8414 u0 Application Not Responding: com.google.android.apps.nexuslauncher}
Nothing in the picker was ever wrong.
How it was found, since none of it was visible from the original failure
1. The failure was not what it said. On the API 34 leg of the run #93 cites
(32806342548), DocumentsUI's own ProvidersAccess: Matched roots names the fixture
root five times inside the sixty seconds the test spent failing, the provider process
started on cue, and the PickActivity was logged as Displayed. Meanwhile By.desc("Show roots") — the toolbar button, present whether the roots list is stale
or not — was equally invisible. A stale roots list loses rows; it does not lose the
toolbar.
2. UiAutomator never read anything at all:
Retrieving node with selector
Node not found with selector
failing API 34 leg
1095
1095
green API 34 leg (32800638011)
7
2
3. So the test was made to ask a question it could answer. requireAReadableScreen() asks whether this process can see the app's own window,
while the app is in front and before anything is tapped. Run 32811493607 (API 35 and
37) then failed in 12 s instead of 60 with UiAutomator cannot see this app's own window … This is not a SAF failure.
4. And then to say what it could see.describeWindows(), run 32812248131
(API 34) and 32812892103 (API 33), character for character the same on both:
What it could see: com.android.systemui[type=3], android[type=3]
type=3 is TYPE_SYSTEM. Not one TYPE_APPLICATION window. android is system_server — and grepping the same logcats for what it was holding produced the
ANR dialog above, present on both legs, minutes before the class ran.
The change
dismissASystemErrorDialog() — by resource id (aerr_wait, aerr_close, button1) rather than localised button text, aerr_wait first so the app under
the dialog is left alone. Called at the start of each pick attempt and inside the
back-out loop: an app-error dialog swallows key events, so a back press aimed at
the picker lands on the dialog and nothing moves. That second call site is not
speculative — run 32813885120 exhausted all four back presses with android in
front.
requireAReadableScreen() — the probe that made the diagnosis possible, kept,
and now the thing that stops a future occurrence being mistaken for SAF.
unlockTheDevice() and rebuildUiAutomation() — kept behind the dialog
dismissal and documented as measured non-causes, not fixes.
The whole pick is retried (dismiss, tap "Choose file" again). A smaller and
separate claim: it answers a picker whose lists were built before their data
arrived, not the occlusion above. The KDoc says so rather than letting one fix take
credit for both.
walkThePickerToTheFixture is a when over three things in order — picker,
root, file — returning whichever was missing, so those stop arriving as one message.
Back presses are counted against Activity.hasWindowFocus, not against UiAutomator:
the failure being worked around is a window UiAutomator cannot see, so a probe
through the same list would report "the picker is gone" about the window still in
front.
Your question: the asymmetric call sites
You are right that the file selector gets no ifAbsent, and that is deliberate —
now stated in the KDoc. openTheRootsDrawer exists because a root has a second
place it can be shown; a document in a directory listing has no second place, so an
in-picker action would have nothing to do. Its recovery is the outer loop: a fresh
pick re-walks from Recent into the root, rebuilding the directory listing as well as
the roots strip. So it is not a bare retry overall.
Variant B was not reproduced and not diagnosed. I am not claiming it is fixed. It
is covered because a fresh pick re-walks, and because the ANR dialog explains that
shape as readily as the other one — but that is an inference, not a measurement.
#80 was right, and it is now measured. A hierarchy dump on a cold API 34 emulator
during a passing run has the fixture root on the landing screen — text="LMC R38 fixtures" at android:id/title, under a BROWSE FILES IN OTHER APPS header — with
the drawer shut (Show roots present, Hide roots absent). The drawer path never
runs; it is kept as a widening for a device whose populated Recent pushes the strip
off screen. Now a line in the KDoc so the next person does not rediscover it.
The check that mattered most: a retry tolerating a missing root would have destroyed
it silently. ConverterScreen.kt:95, arrayOf("*/*") -> arrayOf("application/x-lmc-no-such-type"), on a cold local API 34 emulator, tests=2 failures=2:
java.lang.AssertionError: the system picker never showed BySelector [TEXT='\QLMC R38 fixtures\E'], in 3 separate pickers (the last one left org.libremediaconverter in front)
at org.libremediaconverter.saf.SafPickerRoundTripTest.pickTheFixture(SafPickerRoundTripTest.kt:268)
at org.libremediaconverter.saf.SafPickerRoundTripTest.thePickedInputSurvivesARealRotation(SafPickerRoundTripTest.kt:200)
and the same on pickingAFileThroughTheSystemPickerFillsInTheFileCard. The sentence #64 quotes survives verbatim. Cost: 126 s and 127 s for the two tests, against the
1200 s wrapper timeout in .github/scripts/e2e-run.sh.
Every new path was forced on and measured, not trusted
The reopen recovers, not just fails correctly: a temporary field made the first
walk of each test return a selector nothing matches. Both tests passed, with ActivityTaskManager logging fourOPEN_DOCUMENT starts between them — two
pickers each. That says pickInput.launch is not refused from the re-resumed
Activity, and that the second test's reopen (which lands in the last-accessed stack
rather than on Recent) still walks to the file.
The rebuild does not poison the connection: forced on unconditionally, suite
green, with logcat showing the connection really replaced (Init UiAutomation[id=2, flags=0], id=4, flags=1, id=6, flags=0). A rebuilt connection that came back
without FLAG_RETRIEVE_INTERACTIVE_WINDOWS would have caused the very emptiness it
is meant to cure.
The dialog dismissal does no harm: forced on with no dialog present, suite green
— ruling out a blind click() breaking a healthy run.
Evidence
Local, cold emulators (fresh AVD each time, swangle_indirect, 2 cores):
API 34, whole suite: 59/59
API 35, whole suite: 59/59
API 35, class only: 5/5, and green on every run after each subsequent change
API 34, class only: green on every run
CI on this branch — six runs, and the red ones are the valuable half:
commit
result
what it told us
606dfcb
5/5 green
nothing; ~1-in-4 by luck at the measured base rate
6cfe43b
red 35, 37
"cannot see this app's own window" — not SAF
9cc55bc
red 34
com.android.systemui[type=3], android[type=3]
8d475e9
red 33, 37
same string again — not one level's quirk
6832e28
red 34
back presses exhausted with android in front
25f1629
5/5 gating green
the shipping commit (run 32814569871)
The advisory API 37 job is red, as it is on every PR by design.
The fault never reproduced locally — not on six warm runs, not on the cold
full-suite runs, not under host load. This workstation is much faster than a runner
and the launcher does not ANR on it. Every diagnostic step above came from CI.
Not recommending the non-gating fallback. The cause is identified with a mechanism
and a remedy that matches it, and the gating legs are green. If it recurs, the failure
message names the window list and the dialog, so the next step is a measurement rather
than another theory.
Closes #93.
## The root cause: an ANR dialog nobody was looking at
Not SAF, not root discovery, not the picker. **The launcher ANRs on a loaded runner
emulator, and the "Application Not Responding" dialog it leaves behind never goes
away.** That dialog belongs to `system_server`, and it is opaque and fullscreen — so
`AccessibilityWindowManager` drops every application window beneath it. The app is
`Displayed` and unreadable at the same time, which is the contradiction that made
this look like a SAF bug across six PRs.
```
ANR in com.google.android.apps.nexuslauncher (com.google.android.apps.nexuslauncher/.NexusLauncherActivity)
Reason: Input dispatching timed out (Application does not have a focused window)
Window{4ed8414 u0 Application Not Responding: com.google.android.apps.nexuslauncher}
```
Nothing in the picker was ever wrong.
## How it was found, since none of it was visible from the original failure
**1. The failure was not what it said.** On the API 34 leg of the run #93 cites
(32806342548), DocumentsUI's own `ProvidersAccess: Matched roots` names the fixture
root five times inside the sixty seconds the test spent failing, the provider process
started on cue, and the `PickActivity` was logged as `Displayed`. Meanwhile
`By.desc("Show roots")` — the toolbar button, present whether the roots list is stale
or not — was equally invisible. A stale roots list loses rows; it does not lose the
toolbar.
**2. UiAutomator never read anything at all:**
| | `Retrieving node with selector` | `Node not found with selector` |
|---|---|---|
| failing API 34 leg | 1095 | **1095** |
| green API 34 leg (32800638011) | 7 | 2 |
**3. So the test was made to ask a question it could answer.**
`requireAReadableScreen()` asks whether this process can see the app's *own* window,
while the app is in front and before anything is tapped. Run 32811493607 (API 35 and
37) then failed in 12 s instead of 60 with `UiAutomator cannot see this app's own
window … This is not a SAF failure.`
**4. And then to say what it *could* see.** `describeWindows()`, run 32812248131
(API 34) and 32812892103 (API 33), character for character the same on both:
```
What it could see: com.android.systemui[type=3], android[type=3]
```
`type=3` is `TYPE_SYSTEM`. Not one `TYPE_APPLICATION` window. `android` is
`system_server` — and grepping the same logcats for what it was holding produced the
ANR dialog above, present on both legs, minutes before the class ran.
## The change
- **`dismissASystemErrorDialog()`** — by resource id (`aerr_wait`, `aerr_close`,
`button1`) rather than localised button text, `aerr_wait` first so the app under
the dialog is left alone. Called at the start of each pick attempt **and inside the
back-out loop**: an app-error dialog swallows key events, so a back press aimed at
the picker lands on the dialog and nothing moves. That second call site is not
speculative — run 32813885120 exhausted all four back presses with `android` in
front.
- **`requireAReadableScreen()`** — the probe that made the diagnosis possible, kept,
and now the thing that stops a future occurrence being mistaken for SAF.
- **`unlockTheDevice()` and `rebuildUiAutomation()`** — kept behind the dialog
dismissal and documented as **measured non-causes**, not fixes.
- **The whole pick is retried** (dismiss, tap "Choose file" again). A smaller and
separate claim: it answers a picker whose *lists* were built before their data
arrived, not the occlusion above. The KDoc says so rather than letting one fix take
credit for both.
- **`walkThePickerToTheFixture` is a `when` over three things in order** — picker,
root, file — returning whichever was missing, so those stop arriving as one message.
- Back presses are counted against `Activity.hasWindowFocus`, not against UiAutomator:
the failure being worked around is a window UiAutomator cannot see, so a probe
through the same list would report "the picker is gone" about the window still in
front.
## Your question: the asymmetric call sites
You are right that the file selector gets no `ifAbsent`, and **that is deliberate** —
now stated in the KDoc. `openTheRootsDrawer` exists because a root has a *second
place* it can be shown; a document in a directory listing has no second place, so an
in-picker action would have nothing to do. Its recovery is the outer loop: a fresh
pick re-walks from Recent into the root, rebuilding the directory listing as well as
the roots strip. So it is not a bare retry overall.
**Variant B was not reproduced and not diagnosed.** I am not claiming it is fixed. It
is covered because a fresh pick re-walks, and because the ANR dialog explains that
shape as readily as the other one — but that is an inference, not a measurement.
## On #80's "no drawer navigation needed"
#80 was right, and it is now measured. A hierarchy dump on a cold API 34 emulator
during a passing run has the fixture root on the landing screen — `text="LMC R38
fixtures"` at `android:id/title`, under a `BROWSE FILES IN OTHER APPS` header — with
the drawer shut (`Show roots` present, `Hide roots` absent). The drawer path never
runs; it is kept as a widening for a device whose populated Recent pushes the strip
off screen. Now a line in the KDoc so the next person does not rediscover it.
## #64's MIME mutation still goes red
The check that mattered most: a retry tolerating a missing root would have destroyed
it silently. `ConverterScreen.kt:95`, `arrayOf("*/*")` ->
`arrayOf("application/x-lmc-no-such-type")`, on a cold local API 34 emulator,
`tests=2 failures=2`:
```
java.lang.AssertionError: the system picker never showed BySelector [TEXT='\QLMC R38 fixtures\E'], in 3 separate pickers (the last one left org.libremediaconverter in front)
at org.libremediaconverter.saf.SafPickerRoundTripTest.pickTheFixture(SafPickerRoundTripTest.kt:268)
at org.libremediaconverter.saf.SafPickerRoundTripTest.thePickedInputSurvivesARealRotation(SafPickerRoundTripTest.kt:200)
```
and the same on `pickingAFileThroughTheSystemPickerFillsInTheFileCard`. The sentence
#64 quotes survives verbatim. Cost: 126 s and 127 s for the two tests, against the
1200 s wrapper timeout in `.github/scripts/e2e-run.sh`.
## Every new path was forced on and measured, not trusted
- **The reopen recovers**, not just fails correctly: a temporary field made the first
walk of each test return a selector nothing matches. Both tests passed, with
`ActivityTaskManager` logging **four** `OPEN_DOCUMENT` starts between them — two
pickers each. That says `pickInput.launch` is not refused from the re-resumed
Activity, and that the second test's reopen (which lands in the last-accessed stack
rather than on Recent) still walks to the file.
- **The rebuild does not poison the connection**: forced on unconditionally, suite
green, with logcat showing the connection really replaced (`Init UiAutomation[id=2,
flags=0]`, `id=4, flags=1`, `id=6, flags=0`). A rebuilt connection that came back
without `FLAG_RETRIEVE_INTERACTIVE_WINDOWS` would have caused the very emptiness it
is meant to cure.
- **The dialog dismissal does no harm**: forced on with no dialog present, suite green
— ruling out a blind `click()` breaking a healthy run.
## Evidence
**Local, cold emulators** (fresh AVD each time, `swangle_indirect`, 2 cores):
- API 34, whole suite: 59/59
- API 35, whole suite: 59/59
- API 35, class only: 5/5, and green on every run after each subsequent change
- API 34, class only: green on every run
**CI on this branch — six runs, and the red ones are the valuable half:**
| commit | result | what it told us |
|---|---|---|
| `606dfcb` | 5/5 green | nothing; ~1-in-4 by luck at the measured base rate |
| `6cfe43b` | red 35, 37 | "cannot see this app's own window" — not SAF |
| `9cc55bc` | red 34 | `com.android.systemui[type=3], android[type=3]` |
| `8d475e9` | red 33, 37 | same string again — not one level's quirk |
| `6832e28` | red 34 | back presses exhausted with `android` in front |
| **`25f1629`** | **5/5 gating green** | the shipping commit (run 32814569871) |
The advisory API 37 job is red, as it is on every PR by design.
**The fault never reproduced locally** — not on six warm runs, not on the cold
full-suite runs, not under host load. This workstation is much faster than a runner
and the launcher does not ANR on it. Every diagnostic step above came from CI.
**Not recommending the non-gating fallback.** The cause is identified with a mechanism
and a remedy that matches it, and the gating legs are green. If it recurs, the failure
message names the window list and the dialog, so the next step is a measurement rather
than another theory.
## Gate
`./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue` — BUILD SUCCESSFUL.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Closes #93.
The root cause: an ANR dialog nobody was looking at
Not SAF, not root discovery, not the picker. The launcher ANRs on a loaded runner
emulator, and the "Application Not Responding" dialog it leaves behind never goes
away. That dialog belongs to
system_server, and it is opaque and fullscreen — soAccessibilityWindowManagerdrops every application window beneath it. The app isDisplayedand unreadable at the same time, which is the contradiction that madethis look like a SAF bug across six PRs.
Nothing in the picker was ever wrong.
How it was found, since none of it was visible from the original failure
1. The failure was not what it said. On the API 34 leg of the run #93 cites
(32806342548), DocumentsUI's own
ProvidersAccess: Matched rootsnames the fixtureroot five times inside the sixty seconds the test spent failing, the provider process
started on cue, and the
PickActivitywas logged asDisplayed. MeanwhileBy.desc("Show roots")— the toolbar button, present whether the roots list is staleor not — was equally invisible. A stale roots list loses rows; it does not lose the
toolbar.
2. UiAutomator never read anything at all:
Retrieving node with selectorNode not found with selector3. So the test was made to ask a question it could answer.
requireAReadableScreen()asks whether this process can see the app's own window,while the app is in front and before anything is tapped. Run 32811493607 (API 35 and
37) then failed in 12 s instead of 60 with
UiAutomator cannot see this app's own window … This is not a SAF failure.4. And then to say what it could see.
describeWindows(), run 32812248131(API 34) and 32812892103 (API 33), character for character the same on both:
type=3isTYPE_SYSTEM. Not oneTYPE_APPLICATIONwindow.androidissystem_server— and grepping the same logcats for what it was holding produced theANR dialog above, present on both legs, minutes before the class ran.
The change
dismissASystemErrorDialog()— by resource id (aerr_wait,aerr_close,button1) rather than localised button text,aerr_waitfirst so the app underthe dialog is left alone. Called at the start of each pick attempt and inside the
back-out loop: an app-error dialog swallows key events, so a back press aimed at
the picker lands on the dialog and nothing moves. That second call site is not
speculative — run 32813885120 exhausted all four back presses with
androidinfront.
requireAReadableScreen()— the probe that made the diagnosis possible, kept,and now the thing that stops a future occurrence being mistaken for SAF.
unlockTheDevice()andrebuildUiAutomation()— kept behind the dialogdismissal and documented as measured non-causes, not fixes.
separate claim: it answers a picker whose lists were built before their data
arrived, not the occlusion above. The KDoc says so rather than letting one fix take
credit for both.
walkThePickerToTheFixtureis awhenover three things in order — picker,root, file — returning whichever was missing, so those stop arriving as one message.
Activity.hasWindowFocus, not against UiAutomator:the failure being worked around is a window UiAutomator cannot see, so a probe
through the same list would report "the picker is gone" about the window still in
front.
Your question: the asymmetric call sites
You are right that the file selector gets no
ifAbsent, and that is deliberate —now stated in the KDoc.
openTheRootsDrawerexists because a root has a secondplace it can be shown; a document in a directory listing has no second place, so an
in-picker action would have nothing to do. Its recovery is the outer loop: a fresh
pick re-walks from Recent into the root, rebuilding the directory listing as well as
the roots strip. So it is not a bare retry overall.
Variant B was not reproduced and not diagnosed. I am not claiming it is fixed. It
is covered because a fresh pick re-walks, and because the ANR dialog explains that
shape as readily as the other one — but that is an inference, not a measurement.
On #80's "no drawer navigation needed"
#80 was right, and it is now measured. A hierarchy dump on a cold API 34 emulator
during a passing run has the fixture root on the landing screen —
text="LMC R38 fixtures"atandroid:id/title, under aBROWSE FILES IN OTHER APPSheader — withthe drawer shut (
Show rootspresent,Hide rootsabsent). The drawer path neverruns; it is kept as a widening for a device whose populated Recent pushes the strip
off screen. Now a line in the KDoc so the next person does not rediscover it.
#64's MIME mutation still goes red
The check that mattered most: a retry tolerating a missing root would have destroyed
it silently.
ConverterScreen.kt:95,arrayOf("*/*")->arrayOf("application/x-lmc-no-such-type"), on a cold local API 34 emulator,tests=2 failures=2:and the same on
pickingAFileThroughTheSystemPickerFillsInTheFileCard. The sentence#64 quotes survives verbatim. Cost: 126 s and 127 s for the two tests, against the
1200 s wrapper timeout in
.github/scripts/e2e-run.sh.Every new path was forced on and measured, not trusted
walk of each test return a selector nothing matches. Both tests passed, with
ActivityTaskManagerlogging fourOPEN_DOCUMENTstarts between them — twopickers each. That says
pickInput.launchis not refused from the re-resumedActivity, and that the second test's reopen (which lands in the last-accessed stack
rather than on Recent) still walks to the file.
green, with logcat showing the connection really replaced (
Init UiAutomation[id=2, flags=0],id=4, flags=1,id=6, flags=0). A rebuilt connection that came backwithout
FLAG_RETRIEVE_INTERACTIVE_WINDOWSwould have caused the very emptiness itis meant to cure.
— ruling out a blind
click()breaking a healthy run.Evidence
Local, cold emulators (fresh AVD each time,
swangle_indirect, 2 cores):CI on this branch — six runs, and the red ones are the valuable half:
606dfcb6cfe43b9cc55bccom.android.systemui[type=3], android[type=3]8d475e96832e28androidin front25f1629The advisory API 37 job is red, as it is on every PR by design.
The fault never reproduced locally — not on six warm runs, not on the cold
full-suite runs, not under host load. This workstation is much faster than a runner
and the launcher does not ANR on it. Every diagnostic step above came from CI.
Not recommending the non-gating fallback. The cause is identified with a mechanism
and a remedy that matches it, and the gating legs are green. If it recurs, the failure
message names the window list and the dialog, so the next step is a measurement rather
than another theory.
Gate
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt :app:lintDebug --continue— BUILD SUCCESSFUL.🤖 Generated with Claude Code