package com.kuhy.a11ydump;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Activity;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import java.util.List;
/**
* Dumps one display's accessibility tree as uiautomator-style XML.
*
*
Run: {@code am instrument -w -e display com.kuhy.a11ydump/.A11yDump}. The
* XML comes back on one line as the {@code xml} result key, in the same shape
* {@code uiautomator dump} writes, so the Python side parses both with one
* parser. Coordinates are the display's own, which is what {@code input -d}
* expects.
*/
public final class A11yDump extends Instrumentation {
private static final int ATTEMPTS = 15;
private static final long RETRY_MS = 200;
private Bundle arguments;
@Override
public void onCreate(Bundle args) {
arguments = args;
start();
}
@Override
public void onStart() {
Bundle out = new Bundle();
try {
int display = Integer.parseInt(arguments.getString("display", "0"));
UiAutomation automation = getUiAutomation(
UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
AccessibilityServiceInfo info = automation.getServiceInfo();
info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
| AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
| AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
automation.setServiceInfo(info);
out.putString("xml", dump(automation, display));
finish(Activity.RESULT_OK, out);
} catch (Throwable t) {
out.putString("error", String.valueOf(t));
finish(Activity.RESULT_CANCELED, out);
}
}
private static String dump(UiAutomation automation, int display) {
// Windows appear a beat after the service info changes; an empty
// list on the first read means "not yet", not "nothing there".
List windows = null;
for (int i = 0; i < ATTEMPTS; i++) {
SparseArray> all =
automation.getWindowsOnAllDisplays();
windows = all.get(display);
if (windows != null && !windows.isEmpty()) break;
SystemClock.sleep(RETRY_MS);
}
StringBuilder xml = new StringBuilder(
""
+ "");
if (windows != null) {
// Bottom layer first, like uiautomator, so a dialog's nodes come
// after (and so win over) the screen it covers.
for (int w = windows.size() - 1; w >= 0; w--) {
AccessibilityNodeInfo root = windows.get(w).getRoot();
if (root != null) node(root, 0, xml);
}
}
return xml.append("").toString();
}
private static void node(AccessibilityNodeInfo n, int index, StringBuilder xml) {
Rect r = new Rect();
n.getBoundsInScreen(r);
xml.append("");
for (int i = 0; i < n.getChildCount(); i++) {
AccessibilityNodeInfo child = n.getChild(i);
if (child != null) node(child, i, xml);
}
xml.append("");
}
private static void flag(StringBuilder xml, String name, boolean value) {
xml.append(' ').append(name).append("=\"").append(value).append('"');
}
private static void attr(StringBuilder xml, String name, CharSequence value) {
xml.append(' ').append(name).append("=\"");
if (value != null) {
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '&': xml.append("&"); break;
case '<': xml.append("<"); break;
case '>': xml.append(">"); break;
case '"': xml.append("""); break;
// Newlines would split the one-line result `am` prints.
case '\n': xml.append("
"); break;
case '\r': xml.append("
"); break;
default: xml.append(c);
}
}
}
xml.append('"');
}
}