From 17a32f13a6a3a55443923ea4462c237ad60361b6 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Fri, 27 Feb 2026 16:52:49 +0100 Subject: [PATCH 01/12] Fix #828: Add accessibility API --- app/src/main/AndroidManifest.xml | 13 ++ .../api/TermuxAccessibilityService.java | 26 ++++ .../com/termux/api/TermuxApiReceiver.java | 4 + .../com/termux/api/apis/AccessibilityAPI.java | 145 ++++++++++++++++++ .../res/xml/accessibility_service_config.xml | 6 + 5 files changed, 194 insertions(+) create mode 100644 app/src/main/java/com/termux/api/TermuxAccessibilityService.java create mode 100644 app/src/main/java/com/termux/api/apis/AccessibilityAPI.java create mode 100644 app/src/main/res/xml/accessibility_service_config.xml 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" /> + + + + + + + { + final ContentResolver contentResolver = context.getContentResolver(); + if (intent.hasExtra("dump")) { + out.print(dump()); + } + else if (intent.hasExtra("click")) { + click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0)); + } + }); + } + + private static void click(int x, int y) { + Path swipePath = new Path(); + swipePath.moveTo(x, y); + GestureDescription.Builder gestureBuilder = new GestureDescription.Builder(); + gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 1)); + TermuxAccessibilityService.instance.dispatchGesture(gestureBuilder.build(), null, null); + } + + // The aim of this function is to give a compatible output with `adb` `uiautomator dump`. + private static String dump() throws TransformerException, ParserConfigurationException { + // Create a DocumentBuilder + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // Create a new Document + Document document = builder.newDocument(); + + // Create root element + Element root = document.createElement("hierarchy"); + document.appendChild(root); + + AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow(); + + dumpNodeAuxiliary(document, root, node); + + // Write as XML + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + DOMSource source = new DOMSource(document); + + StringWriter sw = new StringWriter(); + StreamResult result = new StreamResult(sw); + transformer.transform(source, result); + + 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); + 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() : ""; + } +} 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..b9b2e0de8 --- /dev/null +++ b/app/src/main/res/xml/accessibility_service_config.xml @@ -0,0 +1,6 @@ + + From d948b26e227e5e215c25efc5b6d8fe2a17c48106 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Fri, 27 Feb 2026 20:55:52 +0100 Subject: [PATCH 02/12] Prompt the user to enable accessibility to Termux API if not already done --- .../com/termux/api/apis/AccessibilityAPI.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 616175137..74685f409 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -31,6 +31,14 @@ import android.content.ContentResolver; +import android.provider.Settings; + +import android.view.accessibility.AccessibilityManager; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ServiceInfo; +import java.util.List; +import android.accessibilityservice.AccessibilityService; + public class AccessibilityAPI { private static final String LOG_TAG = "AccessibilityAPI"; @@ -38,6 +46,13 @@ public class AccessibilityAPI { public static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { Logger.logDebug(LOG_TAG, "onReceive"); + boolean isAccessibilityEnabled = isAccessibilityServiceEnabled(context, TermuxAccessibilityService.class); + if (!isAccessibilityEnabled) { + Intent accessibilityIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + accessibilityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(accessibilityIntent); + } + ResultReturner.returnData(apiReceiver, intent, out -> { final ContentResolver contentResolver = context.getContentResolver(); if (intent.hasExtra("dump")) { @@ -49,6 +64,20 @@ else if (intent.hasExtra("click")) { }); } + // [The Stack Overflow answer 14923144](https://stackoverflow.com/a/14923144) + public static boolean isAccessibilityServiceEnabled(Context context, Class 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) { Path swipePath = new Path(); swipePath.moveTo(x, y); From f20f6af9f1603a3a220392be01f105d3e9883a99 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Fri, 27 Feb 2026 21:21:53 +0100 Subject: [PATCH 03/12] Add `type` ability --- .../java/com/termux/api/apis/AccessibilityAPI.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 74685f409..badd70efe 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -38,6 +38,7 @@ import android.content.pm.ServiceInfo; import java.util.List; import android.accessibilityservice.AccessibilityService; +import android.os.Bundle; public class AccessibilityAPI { @@ -57,9 +58,10 @@ public static void onReceive(TermuxApiReceiver apiReceiver, final Context contex final ContentResolver contentResolver = context.getContentResolver(); if (intent.hasExtra("dump")) { out.print(dump()); - } - else if (intent.hasExtra("click")) { + } else if (intent.hasExtra("click")) { click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0)); + } else if (intent.hasExtra("type")) { + type(intent.getStringExtra("type")); } }); } @@ -171,4 +173,11 @@ private static void dumpNodeAuxiliary(Document document, Element element, Access 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); + } } From 8dcd6890f38bb74cf38229c69b7c1973706ae5db Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Fri, 27 Feb 2026 22:30:53 +0100 Subject: [PATCH 04/12] Add `performGlobalAction` --- app/src/main/java/com/termux/api/apis/AccessibilityAPI.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index badd70efe..4aa1dd219 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -62,6 +62,8 @@ public static void onReceive(TermuxApiReceiver apiReceiver, final Context contex click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0)); } else if (intent.hasExtra("type")) { type(intent.getStringExtra("type")); + } else if (intent.hasExtra("global-action")) { + performGlobalAction(intent.getStringExtra("global-action")); } }); } @@ -180,4 +182,8 @@ private static void type(String toType) { arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, toType); focusedNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } + + private static void performGlobalAction(String globalAction) throws NoSuchFieldException, IllegalAccessException { + TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction).get(null)); + } } From c093de428facbf86e7d630342345c9935033d3ce Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sat, 28 Feb 2026 03:29:12 +0100 Subject: [PATCH 05/12] Prevent random `NullPointerException` --- app/src/main/java/com/termux/api/apis/AccessibilityAPI.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 4aa1dd219..75772fe7c 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -124,6 +124,11 @@ private static String dump() throws TransformerException, ParserConfigurationExc 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)); From 573f9982f6b8c8d86bf5170dace95bcfd0d906c4 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sat, 28 Feb 2026 03:49:59 +0100 Subject: [PATCH 06/12] Prevent random crash due to UI root being `null` --- .../com/termux/api/apis/AccessibilityAPI.java | 234 +++++++++--------- 1 file changed, 119 insertions(+), 115 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 75772fe7c..753274f4d 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -47,52 +47,52 @@ public class AccessibilityAPI { public static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { Logger.logDebug(LOG_TAG, "onReceive"); - boolean isAccessibilityEnabled = isAccessibilityServiceEnabled(context, TermuxAccessibilityService.class); - if (!isAccessibilityEnabled) { - Intent accessibilityIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); - accessibilityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(accessibilityIntent); - } - - ResultReturner.returnData(apiReceiver, intent, out -> { - final ContentResolver contentResolver = context.getContentResolver(); - if (intent.hasExtra("dump")) { - out.print(dump()); - } else if (intent.hasExtra("click")) { - click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0)); - } else if (intent.hasExtra("type")) { - type(intent.getStringExtra("type")); - } else if (intent.hasExtra("global-action")) { + boolean isAccessibilityEnabled = isAccessibilityServiceEnabled(context, TermuxAccessibilityService.class); + if (!isAccessibilityEnabled) { + Intent accessibilityIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + accessibilityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(accessibilityIntent); + } + + ResultReturner.returnData(apiReceiver, intent, out -> { + final ContentResolver contentResolver = context.getContentResolver(); + if (intent.hasExtra("dump")) { + out.print(dump()); + } else if (intent.hasExtra("click")) { + click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0)); + } else if (intent.hasExtra("type")) { + type(intent.getStringExtra("type")); + } else if (intent.hasExtra("global-action")) { performGlobalAction(intent.getStringExtra("global-action")); - } - }); - } - - // [The Stack Overflow answer 14923144](https://stackoverflow.com/a/14923144) - public static boolean isAccessibilityServiceEnabled(Context context, Class 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) { - Path swipePath = new Path(); - swipePath.moveTo(x, y); - GestureDescription.Builder gestureBuilder = new GestureDescription.Builder(); - gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 1)); - TermuxAccessibilityService.instance.dispatchGesture(gestureBuilder.build(), null, null); - } - - // The aim of this function is to give a compatible output with `adb` `uiautomator dump`. - private static String dump() throws TransformerException, ParserConfigurationException { - // Create a DocumentBuilder + } + }); + } + + // [The Stack Overflow answer 14923144](https://stackoverflow.com/a/14923144) + public static boolean isAccessibilityServiceEnabled(Context context, Class 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) { + Path swipePath = new Path(); + swipePath.moveTo(x, y); + GestureDescription.Builder gestureBuilder = new GestureDescription.Builder(); + gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 1)); + TermuxAccessibilityService.instance.dispatchGesture(gestureBuilder.build(), null, null); + } + + // The aim of this function is to give a compatible output with `adb` `uiautomator dump`. + private static String dump() throws TransformerException, ParserConfigurationException { + // Create a DocumentBuilder DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); @@ -103,92 +103,96 @@ private static String dump() throws TransformerException, ParserConfigurationExc Element root = document.createElement("hierarchy"); document.appendChild(root); - AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow(); + AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow(); + // Randomly faced [Benjamin_Loison/Voice_assistant/issues/84#issue-3661682](https://codeberg.org/Benjamin_Loison/Voice_assistant/issues/84#issue-3661682) + if (node == null) { + return ""; + } - dumpNodeAuxiliary(document, root, node); + dumpNodeAuxiliary(document, root, node); // Write as XML TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); DOMSource source = new DOMSource(document); - StringWriter sw = new StringWriter(); + StringWriter sw = new StringWriter(); StreamResult result = new StreamResult(sw); transformer.transform(source, result); - 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); + 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 String getCharSequenceAsString(CharSequence charSequence) { + return charSequence != null ? charSequence.toString() : ""; + } - private static void type(String toType) { - AccessibilityNodeInfo focusedNode = TermuxAccessibilityService.instance.getRootInActiveWindow().findFocus(AccessibilityNodeInfo.FOCUS_INPUT); + 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 globalAction) throws NoSuchFieldException, IllegalAccessException { - TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction).get(null)); - } + private static void performGlobalAction(String globalAction) throws NoSuchFieldException, IllegalAccessException { + TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction).get(null)); + } } From b9a4467da4456ce0183c78e61aa5b72f323be963 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sat, 28 Feb 2026 17:13:05 +0100 Subject: [PATCH 07/12] Dump UI as UTF-16 for emojis --- app/src/main/java/com/termux/api/apis/AccessibilityAPI.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 753274f4d..67688e95d 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -116,6 +116,8 @@ private static String dump() throws TransformerException, ParserConfigurationExc Transformer transformer = 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(); From f0a6b6b1fb4e3a49a1bc42895ceaee73c3e1bed0 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sat, 28 Feb 2026 17:13:19 +0100 Subject: [PATCH 08/12] Let the ability to use `global-action` with lowercases --- app/src/main/java/com/termux/api/apis/AccessibilityAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 67688e95d..b37638276 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -195,6 +195,6 @@ private static void type(String toType) { } private static void performGlobalAction(String globalAction) throws NoSuchFieldException, IllegalAccessException { - TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction).get(null)); + TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction.toUpperCase()).get(null)); } } From 6c210779fedda9aed55f9cf625a89132a8b858be Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sun, 1 Mar 2026 03:52:15 +0100 Subject: [PATCH 09/12] Add click duration --- app/src/main/java/com/termux/api/apis/AccessibilityAPI.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index b37638276..754711aeb 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -59,7 +59,7 @@ public static void onReceive(TermuxApiReceiver apiReceiver, final Context contex if (intent.hasExtra("dump")) { out.print(dump()); } else if (intent.hasExtra("click")) { - click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0)); + click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0), intent.getIntExtra("duration", 1)); } else if (intent.hasExtra("type")) { type(intent.getStringExtra("type")); } else if (intent.hasExtra("global-action")) { @@ -82,11 +82,11 @@ public static boolean isAccessibilityServiceEnabled(Context context, Class Date: Sat, 7 Mar 2026 17:24:56 +0100 Subject: [PATCH 10/12] Precise empty dump possible reason --- app/src/main/java/com/termux/api/apis/AccessibilityAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 754711aeb..6b39bcd62 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -104,7 +104,7 @@ private static String dump() throws TransformerException, ParserConfigurationExc document.appendChild(root); AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow(); - // Randomly faced [Benjamin_Loison/Voice_assistant/issues/84#issue-3661682](https://codeberg.org/Benjamin_Loison/Voice_assistant/issues/84#issue-3661682) + // On Signal *App permissions* for instance if (node == null) { return ""; } From 1da7ef2cea3d7f8045b9f782a310503192b6c3ab Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sat, 7 Mar 2026 19:22:07 +0100 Subject: [PATCH 11/12] Add `accessibilityFlag` `flagIncludeNotImportantViews` required for *Clock* *Timer* [Benjamin_Loison/Voice_assistant/issues/89#issuecomment-11348374](https://codeberg.org/Benjamin_Loison/Voice_assistant/issues/89#issuecomment-11348374) --- app/src/main/res/xml/accessibility_service_config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/xml/accessibility_service_config.xml b/app/src/main/res/xml/accessibility_service_config.xml index b9b2e0de8..cdeedf984 100644 --- a/app/src/main/res/xml/accessibility_service_config.xml +++ b/app/src/main/res/xml/accessibility_service_config.xml @@ -2,5 +2,5 @@ From 0344a6f5312e39e31e7a96eab5d0f765fea0d1f9 Mon Sep 17 00:00:00 2001 From: Benjamin Loison Date: Sun, 8 Mar 2026 02:31:36 +0100 Subject: [PATCH 12/12] Add accessibility screenshot --- .../com/termux/api/apis/AccessibilityAPI.java | 142 ++++++++++++++---- .../res/xml/accessibility_service_config.xml | 3 +- 2 files changed, 114 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java index 6b39bcd62..3b8238392 100644 --- a/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java +++ b/app/src/main/java/com/termux/api/apis/AccessibilityAPI.java @@ -29,8 +29,6 @@ import android.graphics.Rect; -import android.content.ContentResolver; - import android.provider.Settings; import android.view.accessibility.AccessibilityManager; @@ -40,6 +38,16 @@ import android.accessibilityservice.AccessibilityService; import android.os.Bundle; +import android.view.Display; +import android.accessibilityservice.AccessibilityService.TakeScreenshotCallback; +import android.accessibilityservice.AccessibilityService.ScreenshotResult; +import android.hardware.HardwareBuffer; +import android.graphics.ColorSpace; +import android.graphics.Bitmap; +import java.io.OutputStream; +import java.io.IOException; +import java.lang.reflect.Field; + public class AccessibilityAPI { private static final String LOG_TAG = "AccessibilityAPI"; @@ -54,20 +62,27 @@ public static void onReceive(TermuxApiReceiver apiReceiver, final Context contex context.startActivity(accessibilityIntent); } - ResultReturner.returnData(apiReceiver, intent, out -> { - final ContentResolver contentResolver = context.getContentResolver(); - if (intent.hasExtra("dump")) { - out.print(dump()); - } else if (intent.hasExtra("click")) { - click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0), intent.getIntExtra("duration", 1)); - } else if (intent.hasExtra("type")) { - type(intent.getStringExtra("type")); - } else if (intent.hasExtra("global-action")) { - performGlobalAction(intent.getStringExtra("global-action")); - } - }); + if (intent.hasExtra("dump")) { + dump(apiReceiver, intent); + } else if (intent.hasExtra("click")) { + click(intent.getIntExtra("x", 0), intent.getIntExtra("y", 0), intent.getIntExtra("duration", 1)); + returnEmptyString(apiReceiver, intent); + } else if (intent.hasExtra("type")) { + type(intent.getStringExtra("type")); + returnEmptyString(apiReceiver, intent); + } else if (intent.hasExtra("global-action")) { + performGlobalAction(intent.getStringExtra("global-action")); + returnEmptyString(apiReceiver, intent); + } else if (intent.hasExtra("screenshot")) { + screenshot(apiReceiver, context, intent); + } } + // Necessary for void functions not to hang. + private static void returnEmptyString(TermuxApiReceiver apiReceiver, Intent intent) { + ResultReturner.returnData(apiReceiver, intent, out -> {}); + } + // [The Stack Overflow answer 14923144](https://stackoverflow.com/a/14923144) public static boolean isAccessibilityServiceEnabled(Context context, Class service) { AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); @@ -91,10 +106,30 @@ private static void click(int x, int y, int millisecondsDuration) { } // The aim of this function is to give a compatible output with `adb` `uiautomator dump`. - private static String dump() throws TransformerException, ParserConfigurationException { - // Create a DocumentBuilder + 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 = factory.newDocumentBuilder(); + DocumentBuilder builder = null; + try { + builder = factory.newDocumentBuilder(); + } catch (ParserConfigurationException parserConfigurationException) { + Logger.logDebug(LOG_TAG, "ParserConfigurationException"); + } // Create a new Document Document document = builder.newDocument(); @@ -103,17 +138,16 @@ private static String dump() throws TransformerException, ParserConfigurationExc Element root = document.createElement("hierarchy"); document.appendChild(root); - AccessibilityNodeInfo node = TermuxAccessibilityService.instance.getRootInActiveWindow(); - // On Signal *App permissions* for instance - if (node == null) { - return ""; - } - dumpNodeAuxiliary(document, root, node); // Write as XML TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); + 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) @@ -122,10 +156,13 @@ private static String dump() throws TransformerException, ParserConfigurationExc StringWriter sw = new StringWriter(); StreamResult result = new StreamResult(sw); - transformer.transform(source, result); - - return sw.toString(); - } + 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++) { @@ -194,7 +231,52 @@ private static void type(String toType) { focusedNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } - private static void performGlobalAction(String globalAction) throws NoSuchFieldException, IllegalAccessException { - TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction.toUpperCase()).get(null)); + 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 index cdeedf984..aa7f2bc74 100644 --- a/app/src/main/res/xml/accessibility_service_config.xml +++ b/app/src/main/res/xml/accessibility_service_config.xml @@ -3,4 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:canRetrieveWindowContent="true" android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews" - android:canPerformGestures="true"/> + android:canPerformGestures="true" + android:canTakeScreenshot="true"/>