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), intent.getIntExtra("duration", 1)); + } 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, 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 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(); + // 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); + + // 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"); + // 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); + 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())); + Logger.logInfo(LOG_TAG, 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 globalAction) throws NoSuchFieldException, IllegalAccessException { + TermuxAccessibilityService.instance.performGlobalAction((int)AccessibilityService.class.getDeclaredField("GLOBAL_ACTION_" + globalAction.toUpperCase()).get(null)); + } +} diff --git a/app/src/main/java/com/termux/api/apis/NotificationListAPI.java b/app/src/main/java/com/termux/api/apis/NotificationListAPI.java index 388e3520a..4428efab4 100644 --- a/app/src/main/java/com/termux/api/apis/NotificationListAPI.java +++ b/app/src/main/java/com/termux/api/apis/NotificationListAPI.java @@ -15,6 +15,12 @@ import com.termux.api.util.ResultReturner.ResultJsonWriter; import com.termux.shared.logger.Logger; +import android.media.session.MediaSessionManager; +import android.media.MediaMetadata; +import android.media.session.PlaybackState; +import android.media.session.MediaController; +import java.util.List; +import android.content.ComponentName; public class NotificationListAPI { @@ -26,7 +32,11 @@ public static void onReceive(TermuxApiReceiver apiReceiver, final Context contex ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { @Override public void writeJson(JsonWriter out) throws Exception { - listNotifications(context, out); + if (!intent.hasExtra("media")) { + listNotifications(context, out); + } else { + listMedias(context, out); + } } }); } @@ -93,7 +103,65 @@ static void listNotifications(Context context, JsonWriter out) throws Exception out.endArray(); } + static void listMedias(Context context, JsonWriter out) throws Exception { + MediaSessionManager mediaSessionManager = (MediaSessionManager)context.getSystemService(Context.MEDIA_SESSION_SERVICE); + + ComponentName listenerComponent = new ComponentName(NotificationService.get(), NotificationService.class); + + List controllers = mediaSessionManager.getActiveSessions(listenerComponent); + + out.beginArray(); + for (MediaController controller : controllers) { + MediaMetadata metadata = controller.getMetadata(); + PlaybackState state = controller.getPlaybackState(); + + if (metadata != null) { + out.beginObject() + .name("packageName").value(controller.getPackageName()) + .name("state").value(getStateString(state.getState())) + .name("title").value(metadata.getString(MediaMetadata.METADATA_KEY_TITLE)) + .name("artist").value(metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)) + .name("duration").value(metadata.getLong(MediaMetadata.METADATA_KEY_DURATION)) + .name("bufferedPosition").value(state.getBufferedPosition()) + .name("lastPositionUpdateTime").value(state.getLastPositionUpdateTime()) + .name("playbackSpeed").value(state.getPlaybackSpeed()) + .name("position").value(state.getPosition()); + out.endObject(); + } + } + out.endArray(); + } + static String getStateString(int state) { + switch(state) { + case PlaybackState.STATE_BUFFERING: + return "buffering"; + case PlaybackState.STATE_CONNECTING: + return "connecting"; + case PlaybackState.STATE_ERROR: + return "error"; + case PlaybackState.STATE_FAST_FORWARDING: + return "fast_forwarding"; + case PlaybackState.STATE_NONE: + return "none"; + case PlaybackState.STATE_PAUSED: + return "paused"; + case PlaybackState.STATE_PLAYING: + return "playing"; + case PlaybackState.STATE_REWINDING: + return "rewinding"; + case PlaybackState.STATE_SKIPPING_TO_NEXT: + return "skipping_to_next"; + case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: + return "skipping_to_previous"; + case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM: + return "skipping_to_queue_item"; + case PlaybackState.STATE_STOPPED: + return "stopped"; + default: + return "unknown"; + } + } public static class NotificationService extends NotificationListenerService { static NotificationService _this; diff --git a/app/src/main/java/com/termux/api/apis/SpeechToTextAPI.java b/app/src/main/java/com/termux/api/apis/SpeechToTextAPI.java index c33d7a31a..71576eef5 100644 --- a/app/src/main/java/com/termux/api/apis/SpeechToTextAPI.java +++ b/app/src/main/java/com/termux/api/apis/SpeechToTextAPI.java @@ -140,14 +140,6 @@ public void onBeginningOfSpeech() { }).setNegativeButton("Cancel", null) // cancel button .create().show(); } - - Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); - recognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Enter shell command"); - recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); - recognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 10); - recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en-US"); - recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); - mSpeechRecognizer.startListening(recognizerIntent); } @Override @@ -158,6 +150,22 @@ public void onDestroy() { mSpeechRecognizer.destroy(); } + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Logger.logDebug(LOG_TAG, "onStartCommand"); + + Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + recognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Enter shell command"); + recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + recognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 10); + String language = intent.hasExtra("language") ? intent.getStringExtra("language") : "en-US"; + recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); + recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); + mSpeechRecognizer.startListening(recognizerIntent); + + return super.onStartCommand(intent, flags, startId); + } + @Override protected void onHandleIntent(final Intent intent) { Logger.logDebug(LOG_TAG, "onHandleIntent:\n" + IntentUtils.getIntentString(intent)); @@ -171,6 +179,7 @@ public void writeResult(PrintWriter out) throws Exception { return; } else { out.println(s); + out.flush(); } } } diff --git a/app/src/main/java/com/termux/api/apis/TelephonyAPI.java b/app/src/main/java/com/termux/api/apis/TelephonyAPI.java index 6a751e6c8..e8df2a0df 100644 --- a/app/src/main/java/com/termux/api/apis/TelephonyAPI.java +++ b/app/src/main/java/com/termux/api/apis/TelephonyAPI.java @@ -198,7 +198,7 @@ public void writeJson(JsonWriter out) throws Exception { { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - out.name("data_enabled").value(Boolean.toString(manager.isDataEnabled())); + out.name("data_enabled").value(manager.isDataEnabled()); } int dataActivity = manager.getDataActivity(); @@ -383,6 +383,18 @@ public void writeJson(JsonWriter out) throws Exception { break; case TelephonyManager.SIM_STATE_UNKNOWN: simStateString = "unknown"; + break; + case TelephonyManager.SIM_STATE_NOT_READY: + simStateString = "not_ready"; + break; + case TelephonyManager.SIM_STATE_PERM_DISABLED: + simStateString = "perm_disabled"; + break; + case TelephonyManager.SIM_STATE_CARD_IO_ERROR: + simStateString = "card_io_error"; + break; + case TelephonyManager.SIM_STATE_CARD_RESTRICTED: + simStateString = "card_restricted"; break; default: simStateString = Integer.toString(simState); diff --git a/app/src/main/java/com/termux/api/apis/TextToSpeechAPI.java b/app/src/main/java/com/termux/api/apis/TextToSpeechAPI.java index 2dae2dd1c..653fa56e2 100644 --- a/app/src/main/java/com/termux/api/apis/TextToSpeechAPI.java +++ b/app/src/main/java/com/termux/api/apis/TextToSpeechAPI.java @@ -188,15 +188,16 @@ public void onDone(String utteranceId) { if (!line.isEmpty()) { submittedUtterances++; mTts.speak(line, TextToSpeech.QUEUE_ADD, params, utteranceId); + synchronized (ttsDoneUtterancesCount) { + while (ttsDoneUtterancesCount.get() != submittedUtterances) { + ttsDoneUtterancesCount.wait(); + } + } + out.println("spoken"); + out.flush(); } } } - - synchronized (ttsDoneUtterancesCount) { - while (ttsDoneUtterancesCount.get() != submittedUtterances) { - ttsDoneUtterancesCount.wait(); - } - } } catch (Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "TTS error", e); } 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 @@ + +