fix: refresh Android snapshots after in-place ComposeView content swaps (#1254)#1259
Conversation
Size Report
Startup median (7 runs, lower is better):
Top changed chunks: no changes in the largest emitted chunks. |
|
One compatibility blocker in the pre-34 fallback: Android 13 changed Please handle the API 33 connection-scoped signature (while retaining the older no-arg fallback), then verify at least the reflective path on API 33. The API 36 before/after evidence is otherwise strong, and the clear ordering before both traversal paths looks correct. AOSP references: https://android.googlesource.com/platform/frameworks/base/+/android-13.0.0_r1/core/java/android/view/accessibility/AccessibilityInteractionClient.java and https://android.googlesource.com/platform/frameworks/base/+/android-13.0.0_r1/core/java/android/app/UiAutomation.java |
The snapshot helper keeps one UiAutomation connection hot across captures in a persistent session. Its per-connection accessibility node cache is not always invalidated by an in-place content swap that keeps the same window/Activity — e.g. Jetpack Navigation 3, which renders every destination inside a single AndroidComposeView. After the second such navigation getWindows()/getRootInActiveWindow() keep serving the previous screen, so every selector on the new screen fails to match even though the app has moved on. `uiautomator dump` is unaffected because it uses a fresh connection with an empty cache each time. The #861 recovery only rescues empty/system-only/content-poor captures, so a stale-but-plausible app screen slips past it. Fix the root cause instead: drop the cache before every traversal. On API 34+ use the documented UiAutomation.clearCache(). On older platforms, which lack it, re-apply the current service info via UiAutomation.setServiceInfo() — a public API that internally calls AccessibilityInteractionClient.clearCache() (verified in AOSP from API 24 through 33). The internal cache API cannot be reflected directly: it is a blocklisted non-SDK member, so hidden-API enforcement makes it unreachable from the helper process. Fixes #1254
f0c398a to
c2ff848
Compare
|
Good catch — you're right that the no-arg reflection was a dead path on API 33. I went to implement the connection-scoped signature you described, but while verifying it on-device I hit a wall that changes the approach, so let me lay out what I found. The reflective path is blocked by hidden-API enforcementI built the Both Switched to a public API that does the same thingYou noted
So the fix is now: Verification (real device, API 36 — Nav3 repro, Home → Screen 2 → Screen 3)Both branches exercised by forcing each in turn: On the Also rebased onto latest |
Validation limitation (pre-34 real device)Recording this so it's explicit for review: I could not exercise the pre-34
What the fallback path does rest on:
Given the fallback is a public, long-stable SDK call whose cache-flush is visible directly in source across the whole supported range, and the mechanism is exercised on real hardware, the residual risk is low. Flagging it rather than blocking on hardware we don't have. |
|
Summary
Fixes #1254 — after the second Jetpack Navigation 3 navigation,
agent-device snapshotkeeps returning the previous screen. Every selector on the new screen then fails to match (click,fill,is,wait,find) even though the app has actually moved on.adb shell uiautomator dumpat the same instant shows the correct screen, so it's the helper's snapshot that is stale, not the app or device.Root cause
The snapshot helper keeps a single
UiAutomationconnection hot across captures in a persistent session (a deliberate "keep the runner hot" optimization). That connection's per-connection accessibility node cache is not reliably invalidated by an in-place content swap that keeps the same window/Activity — which is exactly what Navigation 3 does: every destination is rendered inside oneAndroidComposeView, with no new Activity or window (mCurrentFocusnever changes). SogetWindows()/getRootInActiveWindow()keep serving the cached subtree from the earlier navigation.uiautomator dumpis unaffected because each invocation uses a fresh connection with an empty cache.The #861 recovery only rescues empty / system-only / content-poor helper output and falls back to a stock
uiautomator dump. Here the stale tree is a rich, plausible foreground-app screen (16 nodes from the app package), so it slips past every heuristic.Fix
Clear the accessibility cache before every traversal so each capture re-reads the live tree — the same effect a fresh
uiautomator dumpgets for free:UiAutomation.clearCache()(public since Android 14).AccessibilityInteractionClient(all failures swallowed; capture proceeds unchanged if unavailable).The change is confined to the on-device helper (
SnapshotInstrumentation.java); the npm-bundled APK is rebuilt from source atprepack.Verification
Reproduced end-to-end with the reporter's minimal repro (
antFrancon/nav3-agent-device-snapshot-repro) on an Android emulator (API 36).Before (unpatched helper):
After (patched helper) —
Home -> Screen 2 -> Screen 3, snapshot correctly lands on Screen 3 (screen3-field, "Type here"):Home and the first navigation continued to capture correctly, and the Android content-recovery unit tests still pass.