Skip to content

Commit b3418f6

Browse files
author
Benjamin Loison
committed
Fix #828: Add accessibility API
1 parent 760c177 commit b3418f6

5 files changed

Lines changed: 195 additions & 0 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
3939
<uses-permission android:name="android.permission.DUMP" tools:ignore="ProtectedPermissions" />
4040
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
41+
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"/>
4142

4243
<!-- This permission is not used, but a permission is needed on the sharedfiles contentprovider,
4344
which will always use FLAG_GRANT_READ_URI_PERMISSION. -->
@@ -176,6 +177,19 @@
176177
android:enabled="true"
177178
android:exported="false" />
178179

180+
<service
181+
android:name=".TermuxAccessibilityService"
182+
android:enabled="true"
183+
android:exported="false"
184+
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
185+
<intent-filter>
186+
<action android:name="android.accessibilityservice.AccessibilityService"/>
187+
</intent-filter>
188+
<meta-data
189+
android:name="android.accessibilityservice"
190+
android:resource="@xml/accessibility_service_config"/>
191+
</service>
192+
179193
<service
180194
android:name=".apis.JobSchedulerAPI$JobSchedulerService"
181195
android:permission="android.permission.BIND_JOB_SERVICE"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.termux.api;
2+
3+
import com.termux.shared.logger.Logger;
4+
5+
import android.accessibilityservice.AccessibilityService;
6+
import android.view.accessibility.AccessibilityEvent;
7+
8+
public class TermuxAccessibilityService extends AccessibilityService {
9+
10+
private static final String LOG_TAG = "AccessibilityService";
11+
12+
public static TermuxAccessibilityService instance;
13+
14+
@Override
15+
protected void onServiceConnected() {
16+
super.onServiceConnected();
17+
Logger.logDebug(LOG_TAG, "onServiceConnected");
18+
instance = this;
19+
}
20+
21+
@Override
22+
public void onAccessibilityEvent(AccessibilityEvent event) {}
23+
24+
@Override
25+
public void onInterrupt() {}
26+
}

app/src/main/java/com/termux/api/TermuxApiReceiver.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.provider.Settings;
99
import android.widget.Toast;
1010

11+
import com.termux.api.apis.AccessibilityAPI;
1112
import com.termux.api.apis.AudioAPI;
1213
import com.termux.api.apis.BatteryStatusAPI;
1314
import com.termux.api.apis.BrightnessAPI;
@@ -84,6 +85,9 @@ private void doWork(Context context, Intent intent) {
8485
}
8586

8687
switch (apiMethod) {
88+
case "Accessibility":
89+
AccessibilityAPI.onReceive(this, context, intent);
90+
break;
8791
case "AudioInfo":
8892
AudioAPI.onReceive(this, context, intent);
8993
break;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.termux.api.apis;
2+
3+
import android.content.Context;
4+
import android.content.Intent;
5+
6+
import com.termux.api.TermuxApiReceiver;
7+
import com.termux.api.util.ResultReturner;
8+
import com.termux.shared.logger.Logger;
9+
10+
import java.io.PrintWriter;
11+
12+
import com.termux.api.TermuxAccessibilityService;
13+
import android.view.accessibility.AccessibilityNodeInfo;
14+
import android.accessibilityservice.GestureDescription;
15+
import android.graphics.Path;
16+
17+
import javax.xml.parsers.DocumentBuilder;
18+
import javax.xml.parsers.ParserConfigurationException;
19+
import javax.xml.parsers.DocumentBuilderFactory;
20+
import javax.xml.transform.Transformer;
21+
import javax.xml.transform.TransformerFactory;
22+
import javax.xml.transform.TransformerException;
23+
import javax.xml.transform.OutputKeys;
24+
import javax.xml.transform.dom.DOMSource;
25+
import javax.xml.transform.stream.StreamResult;
26+
import org.w3c.dom.Document;
27+
import org.w3c.dom.Element;
28+
import java.io.StringWriter;
29+
30+
import android.graphics.Rect;
31+
32+
import android.content.ContentResolver;
33+
34+
public class AccessibilityAPI {
35+
36+
private static final String LOG_TAG = "AccessibilityAPI";
37+
38+
public static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) {
39+
Logger.logDebug(LOG_TAG, "onReceive");
40+
41+
ResultReturner.returnData(apiReceiver, intent, out -> {
42+
final ContentResolver contentResolver = context.getContentResolver();
43+
if (intent.hasExtra("dump")) {
44+
out.print(dump());
45+
}
46+
else if (intent.hasExtra("x") && intent.hasExtra("y")) {
47+
click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0));
48+
}
49+
});
50+
}
51+
52+
private static void click(int x, int y) {
53+
Path swipePath = new Path();
54+
swipePath.moveTo(x, y);
55+
GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
56+
gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 1));
57+
TermuxAccessibilityService.instance.dispatchGesture(gestureBuilder.build(), null, null);
58+
}
59+
60+
// The aim of this function is to give a compatible output with `adb` `uiautomator dump`.
61+
private static String dump() throws TransformerException, ParserConfigurationException {
62+
// Create a DocumentBuilder
63+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
64+
DocumentBuilder builder = factory.newDocumentBuilder();
65+
66+
// Create a new Document
67+
Document document = builder.newDocument();
68+
69+
// Create root element
70+
Element root = document.createElement("hierarchy");
71+
document.appendChild(root);
72+
73+
AccessibilityNodeInfo root = TermuxAccessibilityService.instance.getRootInActiveWindow();
74+
75+
dumpNodeAuxiliary(document, root, node);
76+
77+
// Write as XML
78+
TransformerFactory transformerFactory = TransformerFactory.newInstance();
79+
Transformer transformer = transformerFactory.newTransformer();
80+
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
81+
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
82+
DOMSource source = new DOMSource(document);
83+
84+
StringWriter sw = new StringWriter();
85+
StreamResult result = new StreamResult(sw);
86+
transformer.transform(source, result);
87+
88+
return sw.toString();
89+
}
90+
91+
private static void dumpNodeAuxiliary(Document document, Element element, AccessibilityNodeInfo node) {
92+
for (int i = 0; i < node.getChildCount(); i++) {
93+
AccessibilityNodeInfo nodeChild = node.getChild(i);
94+
Element elementChild = document.createElement("node");
95+
96+
elementChild.setAttribute("index", String.valueOf(i));
97+
98+
elementChild.setAttribute("text", getCharSequenceAsString(nodeChild.getText()));
99+
100+
String nodeChildViewIdResourceName = nodeChild.getViewIdResourceName();
101+
elementChild.setAttribute("resource-id", nodeChildViewIdResourceName != null ? nodeChildViewIdResourceName : "");
102+
103+
elementChild.setAttribute("class", nodeChild.getClassName().toString());
104+
105+
elementChild.setAttribute("package", nodeChild.getPackageName().toString());
106+
107+
elementChild.setAttribute("content-desc", getCharSequenceAsString(nodeChild.getContentDescription()));
108+
109+
elementChild.setAttribute("checkable", String.valueOf(nodeChild.isCheckable()));
110+
111+
elementChild.setAttribute("checked", String.valueOf(nodeChild.isChecked()));
112+
113+
elementChild.setAttribute("clickable", String.valueOf(nodeChild.isClickable()));
114+
115+
elementChild.setAttribute("enabled", String.valueOf(nodeChild.isEnabled()));
116+
117+
elementChild.setAttribute("focusable", String.valueOf(nodeChild.isFocusable()));
118+
119+
elementChild.setAttribute("focused", String.valueOf(nodeChild.isFocused()));
120+
121+
elementChild.setAttribute("scrollable", String.valueOf(nodeChild.isScrollable()));
122+
123+
elementChild.setAttribute("long-clickable", String.valueOf(nodeChild.isLongClickable()));
124+
125+
elementChild.setAttribute("password", String.valueOf(nodeChild.isPassword()));
126+
127+
elementChild.setAttribute("selected", String.valueOf(nodeChild.isSelected()));
128+
129+
Rect nodeChildBounds = new Rect();
130+
nodeChild.getBoundsInScreen(nodeChildBounds);
131+
elementChild.setAttribute("bounds", nodeChildBounds.toShortString());
132+
133+
elementChild.setAttribute("drawing-order", String.valueOf(nodeChild.getDrawingOrder()));
134+
135+
elementChild.setAttribute("hint", getCharSequenceAsString(nodeChild.getHintText()));
136+
137+
element.appendChild(elementChild);
138+
dumpNodeAuxiliary(document, elementChild, nodeChild);
139+
}
140+
}
141+
142+
private static String getCharSequenceAsString(CharSequence charSequence) {
143+
return charSequence != null ? charSequence.toString() : "";
144+
}
145+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<accessibility-service
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:canRetrieveWindowContent="true"
5+
android:accessibilityFlags="flagReportViewIds"
6+
android:canPerformGestures="true"/>

0 commit comments

Comments
 (0)