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