Skip to content

Commit 0344a6f

Browse files
Add accessibility screenshot
1 parent 1da7ef2 commit 0344a6f

2 files changed

Lines changed: 114 additions & 31 deletions

File tree

app/src/main/java/com/termux/api/apis/AccessibilityAPI.java

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929

3030
import android.graphics.Rect;
3131

32-
import android.content.ContentResolver;
33-
3432
import android.provider.Settings;
3533

3634
import android.view.accessibility.AccessibilityManager;
@@ -40,6 +38,16 @@
4038
import android.accessibilityservice.AccessibilityService;
4139
import android.os.Bundle;
4240

41+
import android.view.Display;
42+
import android.accessibilityservice.AccessibilityService.TakeScreenshotCallback;
43+
import android.accessibilityservice.AccessibilityService.ScreenshotResult;
44+
import android.hardware.HardwareBuffer;
45+
import android.graphics.ColorSpace;
46+
import android.graphics.Bitmap;
47+
import java.io.OutputStream;
48+
import java.io.IOException;
49+
import java.lang.reflect.Field;
50+
4351
public class AccessibilityAPI {
4452

4553
private static final String LOG_TAG = "AccessibilityAPI";
@@ -54,20 +62,27 @@ public static void onReceive(TermuxApiReceiver apiReceiver, final Context contex
5462
context.startActivity(accessibilityIntent);
5563
}
5664

57-
ResultReturner.returnData(apiReceiver, intent, out -> {
58-
final ContentResolver contentResolver = context.getContentResolver();
59-
if (intent.hasExtra("dump")) {
60-
out.print(dump());
61-
} else if (intent.hasExtra("click")) {
62-
click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0), intent.getIntExtra("duration", 1));
63-
} else if (intent.hasExtra("type")) {
64-
type(intent.getStringExtra("type"));
65-
} else if (intent.hasExtra("global-action")) {
66-
performGlobalAction(intent.getStringExtra("global-action"));
67-
}
68-
});
65+
if (intent.hasExtra("dump")) {
66+
dump(apiReceiver, intent);
67+
} else if (intent.hasExtra("click")) {
68+
click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0), intent.getIntExtra("duration", 1));
69+
returnEmptyString(apiReceiver, intent);
70+
} else if (intent.hasExtra("type")) {
71+
type(intent.getStringExtra("type"));
72+
returnEmptyString(apiReceiver, intent);
73+
} else if (intent.hasExtra("global-action")) {
74+
performGlobalAction(intent.getStringExtra("global-action"));
75+
returnEmptyString(apiReceiver, intent);
76+
} else if (intent.hasExtra("screenshot")) {
77+
screenshot(apiReceiver, context, intent);
78+
}
6979
}
7080

81+
// Necessary for void functions not to hang.
82+
private static void returnEmptyString(TermuxApiReceiver apiReceiver, Intent intent) {
83+
ResultReturner.returnData(apiReceiver, intent, out -> {});
84+
}
85+
7186
// [The Stack Overflow answer 14923144](https://stackoverflow.com/a/14923144)
7287
public static boolean isAccessibilityServiceEnabled(Context context, Class<? extends AccessibilityService> service) {
7388
AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
@@ -91,10 +106,30 @@ private static void click(int x, int y, int millisecondsDuration) {
91106
}
92107

93108
// The aim of this function is to give a compatible output with `adb` `uiautomator dump`.
94-
private static String dump() throws TransformerException, ParserConfigurationException {
95-
// Create a DocumentBuilder
109+
private static void dump(TermuxApiReceiver apiReceiver, Intent intent) {
110+
AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow();
111+
// On Signal *App permissions* for instance
112+
if (node == null) {
113+
ResultReturner.returnData(apiReceiver, intent, out -> {});
114+
return;
115+
}
116+
117+
String swString = dumpAuxiliary(node);
118+
119+
ResultReturner.returnData(apiReceiver, intent, out -> {
120+
out.write(swString);
121+
});
122+
}
123+
124+
private static String dumpAuxiliary(AccessibilityNodeInfo node) {
125+
// Create a DocumentBuilder
96126
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
97-
DocumentBuilder builder = factory.newDocumentBuilder();
127+
DocumentBuilder builder = null;
128+
try {
129+
builder = factory.newDocumentBuilder();
130+
} catch (ParserConfigurationException parserConfigurationException) {
131+
Logger.logDebug(LOG_TAG, "ParserConfigurationException");
132+
}
98133

99134
// Create a new Document
100135
Document document = builder.newDocument();
@@ -103,17 +138,16 @@ private static String dump() throws TransformerException, ParserConfigurationExc
103138
Element root = document.createElement("hierarchy");
104139
document.appendChild(root);
105140

106-
AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow();
107-
// On Signal *App permissions* for instance
108-
if (node == null) {
109-
return "";
110-
}
111-
112141
dumpNodeAuxiliary(document, root, node);
113142

114143
// Write as XML
115144
TransformerFactory transformerFactory = TransformerFactory.newInstance();
116-
Transformer transformer = transformerFactory.newTransformer();
145+
Transformer transformer = null;
146+
try {
147+
transformer = transformerFactory.newTransformer();
148+
} catch (TransformerException transformerException) {
149+
Logger.logDebug(LOG_TAG, "TransformerException transformerFactory.newTransformer");
150+
}
117151
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
118152
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
119153
// Necessary to not have surrogate pairs for emojis, see [Benjamin_Loison/Voice_assistant/issues/83#issue-3661619](https://codeberg.org/Benjamin_Loison/Voice_assistant/issues/83#issue-3661619)
@@ -122,10 +156,13 @@ private static String dump() throws TransformerException, ParserConfigurationExc
122156

123157
StringWriter sw = new StringWriter();
124158
StreamResult result = new StreamResult(sw);
125-
transformer.transform(source, result);
126-
127-
return sw.toString();
128-
}
159+
try {
160+
transformer.transform(source, result);
161+
} catch (TransformerException transformerException) {
162+
Logger.logDebug(LOG_TAG, "TransformerException transformer.transform");
163+
}
164+
return sw.toString();
165+
}
129166

130167
private static void dumpNodeAuxiliary(Document document, Element element, AccessibilityNodeInfo node) {
131168
for (int i = 0; i < node.getChildCount(); i++) {
@@ -194,7 +231,52 @@ private static void type(String toType) {
194231
focusedNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
195232
}
196233

197-
private static void performGlobalAction(String globalAction) throws NoSuchFieldException, IllegalAccessException {
198-
TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction.toUpperCase()).get(null));
234+
private static void performGlobalAction(String globalActionString) {
235+
String fieldName = "GLOBAL_ACTION_" + globalActionString.toUpperCase();
236+
Field field = null;
237+
try {
238+
field = AccessibilityService.class.getDeclaredField(fieldName);
239+
} catch (NoSuchFieldException noSuchFieldException) {
240+
Logger.logDebug(LOG_TAG, "NoSuchFieldException");
241+
}
242+
Object globalActionObject = null;
243+
try {
244+
globalActionObject = field.get(null);
245+
} catch(IllegalAccessException illegalAccessException) {
246+
Logger.logDebug(LOG_TAG, "IllegalAccessException");
247+
}
248+
int globalActionInt = (int)globalActionObject;
249+
TermuxAccessibilityService.instance.performGlobalAction(globalActionInt);
199250
}
251+
252+
private static void screenshot(TermuxApiReceiver apiReceiver, final Context context, Intent intent) {
253+
TermuxAccessibilityService.instance.takeScreenshot(
254+
Display.DEFAULT_DISPLAY,
255+
context.getMainExecutor(),
256+
new TakeScreenshotCallback() {
257+
258+
@Override
259+
public void onSuccess(ScreenshotResult screenshotResult) {
260+
Logger.logDebug(LOG_TAG, "onSuccess");
261+
HardwareBuffer buffer = screenshotResult.getHardwareBuffer();
262+
ColorSpace colorSpace = screenshotResult.getColorSpace();
263+
264+
Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, colorSpace);
265+
266+
ResultReturner.returnData(apiReceiver, intent, new ResultReturner.BinaryOutput()
267+
{
268+
@Override
269+
public void writeResult(OutputStream out) throws IOException {
270+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
271+
}
272+
});
273+
}
274+
275+
@Override
276+
public void onFailure(int errorCode) {
277+
Logger.logDebug(LOG_TAG, "onFailure: " + errorCode);
278+
}
279+
}
280+
);
281+
}
200282
}

app/src/main/res/xml/accessibility_service_config.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
xmlns:android="http://schemas.android.com/apk/res/android"
44
android:canRetrieveWindowContent="true"
55
android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews"
6-
android:canPerformGestures="true"/>
6+
android:canPerformGestures="true"
7+
android:canTakeScreenshot="true"/>

0 commit comments

Comments
 (0)