Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.callstack.agentdevice.snapshothelper;

import android.os.Build;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;

/**
* Reads window roots and children from the app with uninterruptible descendant prefetch.
*
* <p>Every read that misses the connection cache is a round trip served on the app's main thread
* after the frame it is drawing, so on an app that never stops animating each miss costs about a
* frame. Of the prefetch strategies, uninterruptible depth-first traversed such an app fastest.
*/
final class AccessibilityNodeReads {
private static final int PREFETCH_STRATEGY =
AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST
| AccessibilityNodeInfo.FLAG_PREFETCH_UNINTERRUPTIBLE;

private AccessibilityNodeReads() {}

static AccessibilityNodeInfo root(AccessibilityWindowInfo window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return window.getRoot(PREFETCH_STRATEGY);
}
return window.getRoot();
}

static AccessibilityNodeInfo child(AccessibilityNodeInfo node, int index) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return node.getChild(index, PREFETCH_STRATEGY);
}
return node.getChild(index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

/** Captures and serializes the Android accessibility window tree. */
final class AccessibilityTreeCapture {
private static final long WINDOW_LIST_SETTLE_TIMEOUT_MS = 1_000;

private AccessibilityTreeCapture() {}

static Result capture(
Expand Down Expand Up @@ -98,23 +100,59 @@ private static void clearAccessibilityCacheViaServiceInfo(UiAutomation automatio
}
}

static void enableInteractiveWindowRetrieval(UiAutomation automation) {
/**
* Turns on interactive-window retrieval and, when this call turned it on, waits a bounded time for
* the window list to name an active window. The platform fills the list after the flag is set, so
* a read straight after it can list only the status bar and none of the app's windows.
*/
static void enableInteractiveWindowRetrieval(UiAutomation automation)
throws InterruptedException {
if (!turnOnInteractiveWindowRetrieval(automation)) {
return;
}
try {
AccessibilityCaptureStabilizer.capture(
() -> () -> listsActiveWindow(automation), WINDOW_LIST_SETTLE_TIMEOUT_MS);
} catch (AccessibilityCaptureStabilizer.IncompleteCaptureException ignored) {
// Capture what the platform lists; the host judges whether it holds the app.
}
}

private static boolean turnOnInteractiveWindowRetrieval(UiAutomation automation) {
AccessibilityServiceInfo serviceInfo;
try {
serviceInfo = automation.getServiceInfo();
} catch (RuntimeException error) {
return;
return false;
}
if (serviceInfo == null
|| (serviceInfo.flags & AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS) != 0) {
return;
return false;
}
serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
try {
automation.setServiceInfo(serviceInfo);
return true;
} catch (RuntimeException ignored) {
// Fall back to active-window capture if the platform rejects dynamic service flags.
return false;
}
}

@SuppressWarnings("deprecation")
private static boolean listsActiveWindow(UiAutomation automation) {
List<AccessibilityWindowInfo> windows;
try {
windows = automation.getWindows();
} catch (RuntimeException error) {
return false;
}
boolean active = false;
for (AccessibilityWindowInfo window : windows) {
active |= isActiveWindow(window);
window.recycle();
}
return active;
}

@SuppressWarnings("deprecation")
Expand Down Expand Up @@ -144,7 +182,7 @@ private static int appendInteractiveWindowRoots(
AccessibilityTreeXml.WindowMetadata windowMetadata = null;
try {
windowMetadata = AccessibilityTreeXml.readWindowMetadata(window, windowCount);
root = window.getRoot();
root = AccessibilityNodeReads.root(window);
if (root == null) {
missingRootWindowTypes.add(windowMetadata.type);
stats.activeWindowRootMissing |= activeWindow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ static void appendNode(
stats.truncated = true;
break;
}
AccessibilityNodeInfo child = node.getChild(index);
AccessibilityNodeInfo child = AccessibilityNodeReads.child(node, index);
if (child == null) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Rect toRect() {
}

@SuppressWarnings("deprecation")
static Reading readReading(UiAutomation automation) {
static Reading readReading(UiAutomation automation) throws InterruptedException {
try {
automation.waitForIdle(100, 2_000);
} catch (TimeoutException ignored) {
Expand Down Expand Up @@ -138,10 +138,6 @@ static WindowEdges struckInputMethod(List<WindowEdges> inputMethodWindows, Windo
return struck;
}

static Rect read(UiAutomation automation) {
return readReading(automation).application;
}

private static Rect resolveApplication(
UiAutomation automation, Rect activeBounds, Rect fallbackBounds) {
if (activeBounds != null) return activeBounds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private static void putCaptureMetadata(
}
}

private void runOneShotViewport(Bundle result) {
private void runOneShotViewport(Bundle result) throws InterruptedException {
TouchCommandHandler.populateViewport(result, getConnectedUiAutomationUnchecked());
finishSafely(0, result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
final class TouchCommandHandler {
private TouchCommandHandler() {}

static void populateViewport(Bundle result, UiAutomation automation) {
static void populateViewport(Bundle result, UiAutomation automation)
throws InterruptedException {
GestureViewportReader.Reading reading = GestureViewportReader.readReading(automation);
result.putString("ok", "true");
result.putString("kind", "viewport");
Expand Down
Loading