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();