From c2ff848fb48723d1a494eb7719f750bef8a2baee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 14 Jul 2026 13:49:17 +0200 Subject: [PATCH] fix: clear the accessibility cache before each Android snapshot capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../SnapshotInstrumentation.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java b/android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java index c9e0d626b..fa55ccfbf 100644 --- a/android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java +++ b/android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java @@ -341,6 +341,7 @@ private CaptureResult captureXml( // Busy or animated apps can still expose a usable root; capture whatever is available. } } + clearAccessibilityCache(automation); CaptureStats stats = new CaptureStats(); StringBuilder xml = new StringBuilder(); @@ -367,6 +368,38 @@ private CaptureResult captureXml( xml.toString(), windowCount > 0, captureMode, windowCount, stats.nodeCount, stats.truncated); } + private static void clearAccessibilityCache(UiAutomation automation) { + // Navigation 3 and any in-place content swap that keeps the same window/Activity render every + // destination inside one AndroidComposeView. A persistent helper session reuses a single + // UiAutomation connection across captures, and its per-connection accessibility node cache is + // not always invalidated by such a swap, so getWindows()/getRootInActiveWindow() can keep + // returning the previous screen. Drop the cache before each traversal so every capture re-reads + // the live tree, the way a fresh `uiautomator dump` (new connection, empty cache) does. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + try { + automation.clearCache(); + return; + } catch (RuntimeException ignored) { + // Fall back to the setServiceInfo() flush below if the platform rejects the public API. + } + } + clearAccessibilityCacheViaServiceInfo(automation); + } + + private static void clearAccessibilityCacheViaServiceInfo(UiAutomation automation) { + // Platforms before API 34 lack UiAutomation.clearCache(). Re-applying the current service info + // is the supported public-API way to drop the cache there: UiAutomation.setServiceInfo() calls + // AccessibilityInteractionClient.clearCache() before forwarding the config (verified in AOSP + // from API 24 through 33). Reflecting the internal clearCache() directly is not an option — it + // is a blocklisted non-SDK member, so hidden-API enforcement makes it unreachable from the + // helper process. Best-effort: if the flush fails the capture proceeds with the current tree. + try { + automation.setServiceInfo(automation.getServiceInfo()); + } catch (RuntimeException ignored) { + // No reliable cache reset on this platform; leave capture behavior unchanged. + } + } + private UiAutomation getConnectedUiAutomation(long timeoutMs) throws TimeoutException { long deadlineMs = System.currentTimeMillis() + Math.max(1, timeoutMs); UiAutomation automation = getUiAutomation();