Skip to content

Commit c2ff848

Browse files
committed
fix: clear the accessibility cache before each Android snapshot capture
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
1 parent 95c1e20 commit c2ff848

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

‎android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ private CaptureResult captureXml(
341341
// Busy or animated apps can still expose a usable root; capture whatever is available.
342342
}
343343
}
344+
clearAccessibilityCache(automation);
344345

345346
CaptureStats stats = new CaptureStats();
346347
StringBuilder xml = new StringBuilder();
@@ -367,6 +368,38 @@ private CaptureResult captureXml(
367368
xml.toString(), windowCount > 0, captureMode, windowCount, stats.nodeCount, stats.truncated);
368369
}
369370

371+
private static void clearAccessibilityCache(UiAutomation automation) {
372+
// Navigation 3 and any in-place content swap that keeps the same window/Activity render every
373+
// destination inside one AndroidComposeView. A persistent helper session reuses a single
374+
// UiAutomation connection across captures, and its per-connection accessibility node cache is
375+
// not always invalidated by such a swap, so getWindows()/getRootInActiveWindow() can keep
376+
// returning the previous screen. Drop the cache before each traversal so every capture re-reads
377+
// the live tree, the way a fresh `uiautomator dump` (new connection, empty cache) does.
378+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
379+
try {
380+
automation.clearCache();
381+
return;
382+
} catch (RuntimeException ignored) {
383+
// Fall back to the setServiceInfo() flush below if the platform rejects the public API.
384+
}
385+
}
386+
clearAccessibilityCacheViaServiceInfo(automation);
387+
}
388+
389+
private static void clearAccessibilityCacheViaServiceInfo(UiAutomation automation) {
390+
// Platforms before API 34 lack UiAutomation.clearCache(). Re-applying the current service info
391+
// is the supported public-API way to drop the cache there: UiAutomation.setServiceInfo() calls
392+
// AccessibilityInteractionClient.clearCache() before forwarding the config (verified in AOSP
393+
// from API 24 through 33). Reflecting the internal clearCache() directly is not an option — it
394+
// is a blocklisted non-SDK member, so hidden-API enforcement makes it unreachable from the
395+
// helper process. Best-effort: if the flush fails the capture proceeds with the current tree.
396+
try {
397+
automation.setServiceInfo(automation.getServiceInfo());
398+
} catch (RuntimeException ignored) {
399+
// No reliable cache reset on this platform; leave capture behavior unchanged.
400+
}
401+
}
402+
370403
private UiAutomation getConnectedUiAutomation(long timeoutMs) throws TimeoutException {
371404
long deadlineMs = System.currentTimeMillis() + Math.max(1, timeoutMs);
372405
UiAutomation automation = getUiAutomation();

0 commit comments

Comments
 (0)