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 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 @@ + +