diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9d371212..4de1ed59d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -176,6 +176,19 @@
android:enabled="true"
android:exported="false" />
+
+
+
+
+
+
+
{});
+ }
+
+ // [The Stack Overflow answer 14923144](https://stackoverflow.com/a/14923144)
+ public static boolean isAccessibilityServiceEnabled(Context context, Class extends AccessibilityService> service) {
+ AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ List enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+
+ for (AccessibilityServiceInfo enabledService : enabledServices) {
+ ServiceInfo enabledServiceInfo = enabledService.getResolveInfo().serviceInfo;
+ if (enabledServiceInfo.packageName.equals(context.getPackageName()) && enabledServiceInfo.name.equals(service.getName()))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void click(int x, int y, int millisecondsDuration) {
+ Path swipePath = new Path();
+ swipePath.moveTo(x, y);
+ GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
+ gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, millisecondsDuration));
+ TermuxAccessibilityService.instance.dispatchGesture(gestureBuilder.build(), null, null);
+ }
+
+ // The aim of this function is to give a compatible output with `adb` `uiautomator dump`.
+ private static void dump(TermuxApiReceiver apiReceiver, Intent intent) {
+ AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow();
+ // On Signal *App permissions* for instance
+ if (node == null) {
+ ResultReturner.returnData(apiReceiver, intent, out -> {});
+ return;
+ }
+
+ String swString = dumpAuxiliary(node);
+
+ ResultReturner.returnData(apiReceiver, intent, out -> {
+ out.write(swString);
+ });
+ }
+
+ private static String dumpAuxiliary(AccessibilityNodeInfo node) {
+ // Create a DocumentBuilder
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = null;
+ try {
+ builder = factory.newDocumentBuilder();
+ } catch (ParserConfigurationException parserConfigurationException) {
+ Logger.logDebug(LOG_TAG, "ParserConfigurationException");
+ }
+
+ // Create a new Document
+ Document document = builder.newDocument();
+
+ // Create root element
+ Element root = document.createElement("hierarchy");
+ document.appendChild(root);
+
+ dumpNodeAuxiliary(document, root, node);
+
+ // Write as XML
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ Transformer transformer = null;
+ try {
+ transformer = transformerFactory.newTransformer();
+ } catch (TransformerException transformerException) {
+ Logger.logDebug(LOG_TAG, "TransformerException transformerFactory.newTransformer");
+ }
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+ // 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)
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16");
+ DOMSource source = new DOMSource(document);
+
+ StringWriter sw = new StringWriter();
+ StreamResult result = new StreamResult(sw);
+ try {
+ transformer.transform(source, result);
+ } catch (TransformerException transformerException) {
+ Logger.logDebug(LOG_TAG, "TransformerException transformer.transform");
+ }
+ return sw.toString();
+ }
+
+ private static void dumpNodeAuxiliary(Document document, Element element, AccessibilityNodeInfo node) {
+ for (int i = 0; i < node.getChildCount(); i++) {
+ AccessibilityNodeInfo nodeChild = node.getChild(i);
+ // May be faced randomly, see [Benjamin-Loison/android/issues/28#issuecomment-3975714760](https://github.com/Benjamin-Loison/android/issues/28#issuecomment-3975714760)
+ if (nodeChild == null)
+ {
+ continue;
+ }
+ Element elementChild = document.createElement("node");
+
+ elementChild.setAttribute("index", String.valueOf(i));
+
+ elementChild.setAttribute("text", getCharSequenceAsString(nodeChild.getText()));
+
+ String nodeChildViewIdResourceName = nodeChild.getViewIdResourceName();
+ elementChild.setAttribute("resource-id", nodeChildViewIdResourceName != null ? nodeChildViewIdResourceName : "");
+
+ elementChild.setAttribute("class", nodeChild.getClassName().toString());
+
+ elementChild.setAttribute("package", nodeChild.getPackageName().toString());
+
+ elementChild.setAttribute("content-desc", getCharSequenceAsString(nodeChild.getContentDescription()));
+
+ elementChild.setAttribute("checkable", String.valueOf(nodeChild.isCheckable()));
+
+ elementChild.setAttribute("checked", String.valueOf(nodeChild.isChecked()));
+
+ elementChild.setAttribute("clickable", String.valueOf(nodeChild.isClickable()));
+
+ elementChild.setAttribute("enabled", String.valueOf(nodeChild.isEnabled()));
+
+ elementChild.setAttribute("focusable", String.valueOf(nodeChild.isFocusable()));
+
+ elementChild.setAttribute("focused", String.valueOf(nodeChild.isFocused()));
+
+ elementChild.setAttribute("scrollable", String.valueOf(nodeChild.isScrollable()));
+
+ elementChild.setAttribute("long-clickable", String.valueOf(nodeChild.isLongClickable()));
+
+ elementChild.setAttribute("password", String.valueOf(nodeChild.isPassword()));
+
+ elementChild.setAttribute("selected", String.valueOf(nodeChild.isSelected()));
+
+ Rect nodeChildBounds = new Rect();
+ nodeChild.getBoundsInScreen(nodeChildBounds);
+ elementChild.setAttribute("bounds", nodeChildBounds.toShortString());
+
+ elementChild.setAttribute("drawing-order", String.valueOf(nodeChild.getDrawingOrder()));
+
+ elementChild.setAttribute("hint", getCharSequenceAsString(nodeChild.getHintText()));
+
+ element.appendChild(elementChild);
+ dumpNodeAuxiliary(document, elementChild, nodeChild);
+ }
+ }
+
+ private static String getCharSequenceAsString(CharSequence charSequence) {
+ return charSequence != null ? charSequence.toString() : "";
+ }
+
+ private static void type(String toType) {
+ AccessibilityNodeInfo focusedNode = TermuxAccessibilityService.instance.getRootInActiveWindow().findFocus(AccessibilityNodeInfo.FOCUS_INPUT);
+ Bundle arguments = new Bundle();
+ arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, toType);
+ focusedNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
+ }
+
+ private static void performGlobalAction(String globalActionString) {
+ String fieldName = "GLOBAL_ACTION_" + globalActionString.toUpperCase();
+ Field field = null;
+ try {
+ field = AccessibilityService.class.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException noSuchFieldException) {
+ Logger.logDebug(LOG_TAG, "NoSuchFieldException");
+ }
+ Object globalActionObject = null;
+ try {
+ globalActionObject = field.get(null);
+ } catch(IllegalAccessException illegalAccessException) {
+ Logger.logDebug(LOG_TAG, "IllegalAccessException");
+ }
+ int globalActionInt = (int)globalActionObject;
+ TermuxAccessibilityService.instance.performGlobalAction(globalActionInt);
+ }
+
+ private static void screenshot(TermuxApiReceiver apiReceiver, final Context context, Intent intent) {
+ TermuxAccessibilityService.instance.takeScreenshot(
+ Display.DEFAULT_DISPLAY,
+ context.getMainExecutor(),
+ new TakeScreenshotCallback() {
+
+ @Override
+ public void onSuccess(ScreenshotResult screenshotResult) {
+ Logger.logDebug(LOG_TAG, "onSuccess");
+ HardwareBuffer buffer = screenshotResult.getHardwareBuffer();
+ ColorSpace colorSpace = screenshotResult.getColorSpace();
+
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, colorSpace);
+
+ ResultReturner.returnData(apiReceiver, intent, new ResultReturner.BinaryOutput()
+ {
+ @Override
+ public void writeResult(OutputStream out) throws IOException {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(int errorCode) {
+ Logger.logDebug(LOG_TAG, "onFailure: " + errorCode);
+ }
+ }
+ );
+ }
+}
diff --git a/app/src/main/res/xml/accessibility_service_config.xml b/app/src/main/res/xml/accessibility_service_config.xml
new file mode 100644
index 000000000..aa7f2bc74
--- /dev/null
+++ b/app/src/main/res/xml/accessibility_service_config.xml
@@ -0,0 +1,7 @@
+
+