From 1f2e562ac783d8db092b6199f1280e6b96f4e680 Mon Sep 17 00:00:00 2001 From: Fredrik Fornwall Date: Sun, 26 Jul 2015 02:23:21 +0200 Subject: [PATCH] Initial push The project was just converted from Eclipse to Android Studio, so there may be some glitches. --- .gitignore | 39 ++++ .idea/gradle.xml | 18 ++ README.md | 19 +- app/build.gradle | 33 +++ app/src/main/AndroidManifest.xml | 44 ++++ .../java/com/termux/api/BatteryStatusAPI.java | 111 ++++++++++ .../java/com/termux/api/CameraInfoAPI.java | 156 +++++++++++++ .../java/com/termux/api/ClipboardAPI.java | 52 +++++ .../java/com/termux/api/ContactListAPI.java | 61 +++++ .../java/com/termux/api/DialogActivity.java | 77 +++++++ .../main/java/com/termux/api/DownloadAPI.java | 44 ++++ .../main/java/com/termux/api/LocationAPI.java | 155 +++++++++++++ .../java/com/termux/api/NotificationAPI.java | 43 ++++ .../java/com/termux/api/PhotoActivity.java | 190 ++++++++++++++++ .../main/java/com/termux/api/SmsInboxAPI.java | 105 +++++++++ .../main/java/com/termux/api/SmsSendAPI.java | 28 +++ .../java/com/termux/api/SpeechToTextAPI.java | 208 ++++++++++++++++++ .../com/termux/api/TermuxApiReceiver.java | 64 ++++++ .../java/com/termux/api/TextToSpeechAPI.java | 164 ++++++++++++++ .../main/java/com/termux/api/VibrateAPI.java | 18 ++ .../com/termux/api/util/ResultReturner.java | 138 ++++++++++++ .../com/termux/api/util/TermuxApiLogger.java | 21 ++ .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 1094 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 786 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 1315 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 1819 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 2594 bytes .../main/res/layout/dialog_textarea_input.xml | 39 ++++ app/src/main/res/values/attrs.xml | 14 ++ app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/styles.xml | 26 +++ build.gradle | 15 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++++++ gradlew.bat | 90 ++++++++ settings.gradle | 1 + 38 files changed, 2150 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .idea/gradle.xml create mode 100644 app/build.gradle create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/termux/api/BatteryStatusAPI.java create mode 100644 app/src/main/java/com/termux/api/CameraInfoAPI.java create mode 100644 app/src/main/java/com/termux/api/ClipboardAPI.java create mode 100644 app/src/main/java/com/termux/api/ContactListAPI.java create mode 100644 app/src/main/java/com/termux/api/DialogActivity.java create mode 100644 app/src/main/java/com/termux/api/DownloadAPI.java create mode 100644 app/src/main/java/com/termux/api/LocationAPI.java create mode 100644 app/src/main/java/com/termux/api/NotificationAPI.java create mode 100644 app/src/main/java/com/termux/api/PhotoActivity.java create mode 100644 app/src/main/java/com/termux/api/SmsInboxAPI.java create mode 100644 app/src/main/java/com/termux/api/SmsSendAPI.java create mode 100644 app/src/main/java/com/termux/api/SpeechToTextAPI.java create mode 100644 app/src/main/java/com/termux/api/TermuxApiReceiver.java create mode 100644 app/src/main/java/com/termux/api/TextToSpeechAPI.java create mode 100644 app/src/main/java/com/termux/api/VibrateAPI.java create mode 100644 app/src/main/java/com/termux/api/util/ResultReturner.java create mode 100644 app/src/main/java/com/termux/api/util/TermuxApiLogger.java create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/layout/dialog_textarea_input.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..95843591 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# .gitignore from https://gist.github.com/iainconnor/8605514: + +# Built application files +build/ + +# Crashlytics configuations +com_crashlytics_export_strings.xml + +# Local configuration file (sdk path, etc) +local.properties + +# Gradle generated files +.gradle/ + +# Signing files +.signing/ + +# User-specific configurations +.idea/libraries/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +*.iml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..8d2df476 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 2fcab8ea..aa54619f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# termux-api -Add-on app which exposes device functionality as API to command line programs. +Termux API +========== +This is an app exposing Android API to command line usage and scripts or programs. + +The termux-api client helper binary +=================================== +The client helper binary ([termux-api.c](https://github.com/termux/termux-packages/blob/master/packages/termux-api/termux-api.c)) +generates two linux anonymous namespace sockets, and passes their address as in: + - /system/bin/am broadcast ${SERVICE_CLASS} --es socket_input ${INPUT_SOCKET} --es socket_output ${OUTPUT_SOCKET} +where the sockets are used to transfer: + - input through stdin to the helper binary are forwarded to java code + - java code may output feedback which are forwarded to the stdout of the helper binary + +Client scripts +============== +Client scripts which processes command line arguments before calling the termux-api helper binary are available: + - [The termux-api package](https://github.com/termux/termux-packages/tree/master/packages/termux-api) diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..a07c9bf8 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "22.0.1" + + defaultConfig { + applicationId "com.termux.api" + minSdkVersion 21 + targetSdkVersion 22 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + } + + signingConfigs { + release { + storeFile new File(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + signingConfig signingConfigs.release + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ecf13db5 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/termux/api/BatteryStatusAPI.java b/app/src/main/java/com/termux/api/BatteryStatusAPI.java new file mode 100644 index 00000000..1fdf1136 --- /dev/null +++ b/app/src/main/java/com/termux/api/BatteryStatusAPI.java @@ -0,0 +1,111 @@ +package com.termux.api; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.util.JsonWriter; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.TermuxApiLogger; +import com.termux.api.util.ResultReturner.ResultJsonWriter; + +public class BatteryStatusAPI { + + public static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { + @Override + public void writeJson(JsonWriter out) throws Exception { + Intent batteryStatus = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + + int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + final int batteryPercentage = (level * 100) / scale; + + int health = batteryStatus.getIntExtra(BatteryManager.EXTRA_HEALTH, -1); + String batteryHealth; + switch (health) { + case BatteryManager.BATTERY_HEALTH_COLD: + batteryHealth = "COLD"; + break; + case BatteryManager.BATTERY_HEALTH_DEAD: + batteryHealth = "DEAD"; + break; + case BatteryManager.BATTERY_HEALTH_GOOD: + batteryHealth = "GOOD"; + break; + case BatteryManager.BATTERY_HEALTH_OVERHEAT: + batteryHealth = "OVERHEAD"; + break; + case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: + batteryHealth = "OVER_VOLTAGE"; + break; + case BatteryManager.BATTERY_HEALTH_UNKNOWN: + batteryHealth = "UNKNOWN"; + break; + case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: + batteryHealth = "UNSPECIFIED_FAILURE"; + break; + default: + batteryHealth = Integer.toString(health); + } + + // BatteryManager.EXTRA_PLUGGED: "Extra for ACTION_BATTERY_CHANGED: integer indicating whether the + // device is plugged in to a power source; 0 means it is on battery, other constants are different types + // of power sources." + int pluggedInt = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + String batteryPlugged; + switch (pluggedInt) { + case 0: + batteryPlugged = "UNPLUGGED"; + break; + case BatteryManager.BATTERY_PLUGGED_AC: + batteryPlugged = "PLUGGED_AC"; + break; + case BatteryManager.BATTERY_PLUGGED_USB: + batteryPlugged = "PLUGGED_USB"; + break; + case BatteryManager.BATTERY_PLUGGED_WIRELESS: + batteryPlugged = "PLUGGED_WIRELESS"; + break; + default: + batteryPlugged = "PLUGGED_" + pluggedInt; + } + + double batteryTemperature = batteryStatus.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1) / 10.f; + + String batteryStatusString; + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + switch (status) { + case BatteryManager.BATTERY_STATUS_CHARGING: + batteryStatusString = "CHARGING"; + break; + case BatteryManager.BATTERY_STATUS_DISCHARGING: + batteryStatusString = "DISCHARGING"; + break; + case BatteryManager.BATTERY_STATUS_FULL: + batteryStatusString = "FULL"; + break; + case BatteryManager.BATTERY_STATUS_NOT_CHARGING: + batteryStatusString = "NOT_CHARGING"; + break; + case BatteryManager.BATTERY_STATUS_UNKNOWN: + batteryStatusString = "UNKNOWN"; + break; + default: + TermuxApiLogger.error("Invalid BatteryManager.EXTRA_STATUS value: " + status); + batteryStatusString = "UNKNOWN"; + } + + out.beginObject(); + out.name("health").value(batteryHealth); + out.name("percentage").value(batteryPercentage); + out.name("plugged").value(batteryPlugged); + out.name("status").value(batteryStatusString); + out.name("temperature").value(batteryTemperature); + out.endObject(); + } + }); + + } +} diff --git a/app/src/main/java/com/termux/api/CameraInfoAPI.java b/app/src/main/java/com/termux/api/CameraInfoAPI.java new file mode 100644 index 00000000..2657bb74 --- /dev/null +++ b/app/src/main/java/com/termux/api/CameraInfoAPI.java @@ -0,0 +1,156 @@ +package com.termux.api; + +import android.content.Context; +import android.content.Intent; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.util.JsonWriter; +import android.util.SizeF; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.ResultReturner.ResultJsonWriter; + +public class CameraInfoAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { + @Override + public void writeJson(JsonWriter out) throws Exception { + final CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + + out.beginArray(); + for (String cameraId : manager.getCameraIdList()) { + out.beginObject(); + out.name("id").value(cameraId); + + CameraCharacteristics camera = manager.getCameraCharacteristics(cameraId); + + out.name("facing"); + int lensFacing = camera.get(CameraCharacteristics.LENS_FACING); + switch (lensFacing) { + case CameraMetadata.LENS_FACING_FRONT: + out.value("front"); + break; + case CameraMetadata.LENS_FACING_BACK: + out.value("back"); + break; + default: + out.value(lensFacing); + } + + out.name("focal_lengths").beginArray(); + for (float f : camera.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)) + out.value(f); + out.endArray(); + + out.name("auto_exposure_modes").beginArray(); + int[] flashModeValues = camera.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); + for (int flashMode : flashModeValues) { + switch (flashMode) { + case CameraMetadata.CONTROL_AE_MODE_OFF: + out.value("CONTROL_AE_MODE_OFF"); + break; + case CameraMetadata.CONTROL_AE_MODE_ON: + out.value("CONTROL_AE_MODE_ON"); + break; + case CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH: + out.value("CONTROL_AE_MODE_ON_ALWAYS_FLASH"); + break; + case CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH: + out.value("CONTROL_AE_MODE_ON_AUTO_FLASH"); + break; + case CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE: + out.value("CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE"); + break; + default: + out.value(flashMode); + } + } + out.endArray(); + + // out.write(" Focus modes: "); + // boolean first = true; + // // for (String mode : params.getSupportedFocusModes()) { + // // if (first) { + // // first = false; + // // } else { + // // out.write("/"); + // // } + // // out.write(mode); + // // } + // out.write("\n"); + + SizeF physicalSize = camera.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); + out.name("physical_size").beginObject().name("width").value(physicalSize.getWidth()).name("height") + .value(physicalSize.getHeight()).endObject(); + + out.name("capabilities").beginArray(); + for (int capability : camera.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)) { + switch (capability) { + case CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR: + out.value("manual_sensor"); + break; + case CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING: + out.value("manual_post_processing"); + break; + case CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE: + out.value("backward_compatible"); + break; + case CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_RAW: + out.value("raw"); + break; + default: + out.value(capability); + } + } + out.endArray(); + // out.write(" Picture formats: "); + // first = true; + // for (int i : params.getSupportedPictureFormats()) { + // if (first) { + // first = false; + // } else { + // out.write("/"); + // } + // switch (i) { + // case ImageFormat.JPEG: + // out.write("JPEG"); + // break; + // case ImageFormat.NV16: + // out.write("NV16"); + // break; + // case ImageFormat.NV21: + // out.write("NV21"); + // break; + // case ImageFormat.RGB_565: + // out.write("RGB_565"); + // break; + // case ImageFormat.YUV_420_888: + // out.write("YUV_420_888"); + // break; + // case ImageFormat.YUY2: + // out.write("YUY2"); + // break; + // case ImageFormat.YV12: + // out.write("YV12"); + // break; + // default: + // out.write(i + " (no matching ImageFormat constant)"); + // } + // } + // out.write("\n"); + + // out.write(" Sizes:\n"); + + // for (Size size : params.getSupportedPictureSizes()) { + // out.write(" [" + count + "]: " + size.width + "x" + size.height + "\n"); + // count++; + // } + out.endObject(); + } + out.endArray(); + } + }); + } +} diff --git a/app/src/main/java/com/termux/api/ClipboardAPI.java b/app/src/main/java/com/termux/api/ClipboardAPI.java new file mode 100644 index 00000000..d128efb8 --- /dev/null +++ b/app/src/main/java/com/termux/api/ClipboardAPI.java @@ -0,0 +1,52 @@ +package com.termux.api; + +import java.io.PrintWriter; + +import android.content.ClipData; +import android.content.ClipData.Item; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.ResultReturner.ResultWriter; + +public class ClipboardAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { + final ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + final ClipData clipData = clipboard.getPrimaryClip(); + final String newClipText = intent.getStringExtra("text"); + if (newClipText != null) { + // Set clip. + clipboard.setPrimaryClip(ClipData.newPlainText("", newClipText)); + } + + ResultReturner.returnData(apiReceiver, intent, new ResultWriter() { + @Override + public void writeResult(PrintWriter out) { + if (newClipText == null) { + // Get clip. + if (clipData == null) { + out.println(); + } else { + int itemCount = clipData.getItemCount(); + for (int i = 0; i < itemCount; i++) { + Item item = clipData.getItemAt(i); + CharSequence text = item.coerceToText(context); + if (text != null) { + out.print(text); + if (i + 1 != itemCount) { + out.println(); + } + } + } + } + } else { + // Set clip - already done in main thread. + } + } + }); + } + +} diff --git a/app/src/main/java/com/termux/api/ContactListAPI.java b/app/src/main/java/com/termux/api/ContactListAPI.java new file mode 100644 index 00000000..f614d627 --- /dev/null +++ b/app/src/main/java/com/termux/api/ContactListAPI.java @@ -0,0 +1,61 @@ +package com.termux.api; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.util.JsonWriter; +import android.util.SparseArray; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.ResultReturner.ResultJsonWriter; + +public class ContactListAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { + @Override + public void writeJson(JsonWriter out) throws Exception { + listContacts(context, out); + } + }); + } + + static void listContacts(Context context, JsonWriter out) throws Exception { + ContentResolver cr = context.getContentResolver(); + + SparseArray contactIdToNumberMap = new SparseArray<>(); + String[] projection = { Phone.NUMBER, Phone.CONTACT_ID }; + String selection = Phone.CONTACT_ID + " IS NOT NULL AND " + Phone.NUMBER + " IS NOT NULL"; + try (Cursor phones = cr.query(Phone.CONTENT_URI, projection, selection, null, null)) { + int phoneNumberIdx = phones.getColumnIndexOrThrow(Phone.NUMBER); + int phoneContactIdIdx = phones.getColumnIndexOrThrow(Phone.CONTACT_ID); + while (phones.moveToNext()) { + String number = phones.getString(phoneNumberIdx); + int contactId = phones.getInt(phoneContactIdIdx); + // int type = phones.getInt(phones.getColumnIndex(Phone.TYPE)); + contactIdToNumberMap.put(contactId, number); + } + } + + out.beginArray(); + try (Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, ContactsContract.Contacts.DISPLAY_NAME)) { + int contactDisplayNameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME); + int contactIdIdx = cursor.getColumnIndex(BaseColumns._ID); + + while (cursor.moveToNext()) { + int contactId = cursor.getInt(contactIdIdx); + String number = contactIdToNumberMap.get(contactId); + if (number != null) { + String contactName = cursor.getString(contactDisplayNameIdx); + out.beginObject().name("name").value(contactName).name("number").value(number).endObject(); + } + } + } finally { + out.endArray(); + } + } +} diff --git a/app/src/main/java/com/termux/api/DialogActivity.java b/app/src/main/java/com/termux/api/DialogActivity.java new file mode 100644 index 00000000..bcaf031b --- /dev/null +++ b/app/src/main/java/com/termux/api/DialogActivity.java @@ -0,0 +1,77 @@ +package com.termux.api; + +import java.io.PrintWriter; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; +import android.widget.EditText; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.ResultReturner.ResultWriter; + +public class DialogActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.dialog_textarea_input); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onResume() { + super.onResume(); + + String inputHint = getIntent().getStringExtra("input_hint"); + if (inputHint != null) { + ((EditText) findViewById(R.id.text_input)).setHint(inputHint); + } + + findViewById(R.id.cancel_button).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + ResultReturner.returnData(DialogActivity.this, getIntent(), new ResultWriter() { + @Override + public void writeResult(PrintWriter out) throws Exception { + runOnUiThread(new Runnable() { + @Override + public void run() { + finish(); + } + }); + + } + }); + } + }); + + findViewById(R.id.ok_button).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + ResultReturner.returnData(DialogActivity.this, getIntent(), new ResultWriter() { + @Override + public void writeResult(PrintWriter out) throws Exception { + String text = ((EditText) findViewById(R.id.text_input)).getText().toString(); + out.println(text.trim()); + runOnUiThread(new Runnable() { + @Override + public void run() { + finish(); + } + }); + } + }); + } + }); + } +} diff --git a/app/src/main/java/com/termux/api/DownloadAPI.java b/app/src/main/java/com/termux/api/DownloadAPI.java new file mode 100644 index 00000000..a7b87f0d --- /dev/null +++ b/app/src/main/java/com/termux/api/DownloadAPI.java @@ -0,0 +1,44 @@ +package com.termux.api; + +import java.io.PrintWriter; + +import android.app.DownloadManager; +import android.app.DownloadManager.Request; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.ResultReturner.ResultWriter; + +public class DownloadAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, final Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new ResultWriter() { + @Override + public void writeResult(PrintWriter out) throws Exception { + final Uri downloadUri = intent.getData(); + if (downloadUri == null) { + out.println("No download URI specified"); + return; + } + + String title = intent.getStringExtra("title"); + String description = intent.getStringExtra("description"); + + DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Request req = new Request(downloadUri); + req.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + req.setVisibleInDownloadsUi(true); + + if (title != null) + req.setTitle(title); + + if (description != null) + req.setDescription(description); + + manager.enqueue(req); + } + }); + } +} diff --git a/app/src/main/java/com/termux/api/LocationAPI.java b/app/src/main/java/com/termux/api/LocationAPI.java new file mode 100644 index 00000000..1110299c --- /dev/null +++ b/app/src/main/java/com/termux/api/LocationAPI.java @@ -0,0 +1,155 @@ +package com.termux.api; + +import java.io.IOException; + +import android.content.Context; +import android.content.Intent; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.Looper; +import android.os.SystemClock; +import android.util.JsonWriter; +import android.util.Log; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.TermuxApiLogger; +import com.termux.api.util.ResultReturner.ResultJsonWriter; + +public class LocationAPI { + + private static final String REQUEST_LAST_KNOWN = "last"; + private static final String REQUEST_ONCE = "once"; + private static final String REQUEST_UPDATES = "updates"; + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, final Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { + @Override + public void writeJson(final JsonWriter out) throws Exception { + LocationManager manager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + String provider = intent.getStringExtra("provider"); + if (provider == null) + provider = LocationManager.GPS_PROVIDER; + if (!(provider.equals(LocationManager.GPS_PROVIDER) || provider.equals(LocationManager.NETWORK_PROVIDER) || provider + .equals(LocationManager.PASSIVE_PROVIDER))) { + out.beginObject() + .name("API_ERROR") + .value("Unsupported provider '" + provider + "' - only '" + LocationManager.GPS_PROVIDER + "', '" + + LocationManager.NETWORK_PROVIDER + "' and '" + LocationManager.PASSIVE_PROVIDER + "' supported").endObject(); + return; + } + + String request = intent.getStringExtra("request"); + if (request == null) + request = REQUEST_ONCE; + switch (request) { + case REQUEST_LAST_KNOWN: + Location lastKnownLocation = manager.getLastKnownLocation(provider); + locationToJson(lastKnownLocation, out); + break; + case REQUEST_ONCE: + Looper.prepare(); + manager.requestSingleUpdate(provider, new LocationListener() { + + @Override + public void onStatusChanged(String changedProvider, int status, Bundle extras) { + // TODO Auto-generated method stub + } + + @Override + public void onProviderEnabled(String changedProvider) { + // TODO Auto-generated method stub + } + + @Override + public void onProviderDisabled(String changedProvider) { + // TODO Auto-generated method stub + } + + @Override + public void onLocationChanged(Location location) { + try { + locationToJson(location, out); + } catch (IOException e) { + TermuxApiLogger.error("Writing json", e); + } finally { + Looper.myLooper().quit(); + } + } + }, null); + Looper.loop(); + break; + case REQUEST_UPDATES: + Looper.prepare(); + manager.requestLocationUpdates(provider, 5000, 50.f, new LocationListener() { + + @Override + public void onStatusChanged(String changedProvider, int status, Bundle extras) { + // Do nothing. + } + + @Override + public void onProviderEnabled(String changedProvider) { + // Do nothing. + } + + @Override + public void onProviderDisabled(String changedProvider) { + // Do nothing. + } + + @Override + public void onLocationChanged(Location location) { + try { + locationToJson(location, out); + out.flush(); + } catch (IOException e) { + TermuxApiLogger.error("Writing json", e); + } + } + }, null); + final Looper looper = Looper.myLooper(); + new Thread() { + @Override + public void run() { + try { + Thread.sleep(30 * 1000); + } catch (InterruptedException e) { + Log.e("termux", "INTER", e); + } + looper.quit(); + } + }.start(); + Looper.loop(); + break; + default: + out.beginObject() + .name("API_ERROR") + .value("Unsupported request '" + request + "' - only '" + REQUEST_LAST_KNOWN + "', '" + REQUEST_ONCE + "' and '" + REQUEST_UPDATES + + "' supported").endObject(); + return; + } + } + }); + } + + static void locationToJson(Location lastKnownLocation, JsonWriter out) throws IOException { + if (lastKnownLocation == null) { + out.beginObject().name("API_ERROR").value("Failed to get location").endObject(); + return; + } + out.beginObject(); + out.name("latitude").value(lastKnownLocation.getLatitude()); + out.name("longitude").value(lastKnownLocation.getLongitude()); + out.name("altitude").value(lastKnownLocation.getAltitude()); + out.name("accuracy").value(lastKnownLocation.getAccuracy()); + out.name("bearing").value(lastKnownLocation.getBearing()); + out.name("speed").value(lastKnownLocation.getSpeed()); + long elapsedMs = (SystemClock.elapsedRealtimeNanos() - lastKnownLocation.getElapsedRealtimeNanos()) / 1000000; + out.name("elapsedMs").value(elapsedMs); + out.name("provider").value(lastKnownLocation.getProvider()); + out.endObject(); + } +} diff --git a/app/src/main/java/com/termux/api/NotificationAPI.java b/app/src/main/java/com/termux/api/NotificationAPI.java new file mode 100644 index 00000000..9254fc59 --- /dev/null +++ b/app/src/main/java/com/termux/api/NotificationAPI.java @@ -0,0 +1,43 @@ +package com.termux.api; + +import java.util.UUID; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.termux.api.util.ResultReturner; + +/** Shows a notification. Driven by the termux-show-notification script. */ +public class NotificationAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { + String content = intent.getStringExtra("content"); + String notificationId = intent.getStringExtra("id"); + if (notificationId == null) { + notificationId = UUID.randomUUID().toString(); + } + String title = intent.getStringExtra("title"); + + String url = intent.getStringExtra("url"); + PendingIntent pendingIntent = null; + if (url != null) { + Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + pendingIntent = PendingIntent.getActivity(context, 0, urlIntent, 0); + } + + Notification.Builder notification = new Notification.Builder(context).setSmallIcon(android.R.drawable.ic_popup_reminder).setColor(0xFF000000) + .setContentTitle(title).setContentText(content); + if (pendingIntent != null) + notification.setContentIntent(pendingIntent); + + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + manager.notify(notificationId, 0, notification.build()); + + ResultReturner.noteDone(apiReceiver, intent); + } + +} diff --git a/app/src/main/java/com/termux/api/PhotoActivity.java b/app/src/main/java/com/termux/api/PhotoActivity.java new file mode 100644 index 00000000..df4e1139 --- /dev/null +++ b/app/src/main/java/com/termux/api/PhotoActivity.java @@ -0,0 +1,190 @@ +package com.termux.api; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.ImageFormat; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.os.Bundle; +import android.util.Size; +import android.view.Surface; +import android.view.SurfaceView; + +import com.termux.api.util.TermuxApiLogger; + +public class PhotoActivity extends Activity { + + private SurfaceView surfaceView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + surfaceView = new SurfaceView(this); + setContentView(surfaceView); + } + + @Override + protected void onResume() { + super.onResume(); + takePictureNoPreview(); + } + + static class CompareSizesByArea implements Comparator { + @Override + public int compare(Size lhs, Size rhs) { + // We cast here to ensure the multiplications won't overflow + return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + public void takePictureNoPreview() { + try { + String filePath = getIntent().getStringExtra("file"); + final File tmpFile = new File(filePath); + String cameraId = getIntent().getStringExtra("camera"); + if (cameraId == null) { + cameraId = "0"; + } + + final CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); + + TermuxApiLogger.info("cameraId=" + cameraId + ", filePath=" + tmpFile.getAbsolutePath()); + + CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + // For still image captures, we use the largest available size. + Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); + final ImageReader mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), + ImageFormat.JPEG, 2); + mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(final ImageReader reader) { + TermuxApiLogger.info("onImageAvailable() from mImageReader"); + new Thread() { + @Override + public void run() { + try (final Image mImage = reader.acquireNextImage()) { + ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + try (FileOutputStream output = new FileOutputStream(tmpFile)) { + output.write(bytes); + } catch (Exception e) { + TermuxApiLogger.error("Error writing image", e); + } + } + } + }.start(); + } + }, null); + + manager.openCamera(cameraId, new CameraDevice.StateCallback() { + @Override + public void onOpened(final CameraDevice camera) { + TermuxApiLogger.info("onOpened() from camera"); + try { + final CaptureRequest.Builder captureBuilder = camera + .createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(mImageReader.getSurface()); + + // Use the same AE and AF modes as the preview. + captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, + CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + captureBuilder + .set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH); + + // Orientation jpeg fix, from the Camera2BasicFragment example: + int cameraJpegOrientation; + switch (getWindowManager().getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + cameraJpegOrientation = 90; + break; + case Surface.ROTATION_90: + cameraJpegOrientation = 0; + break; + case Surface.ROTATION_180: + cameraJpegOrientation = 270; + break; + case Surface.ROTATION_270: + cameraJpegOrientation = 180; + break; + default: + cameraJpegOrientation = 0; + } + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, cameraJpegOrientation); + + List outputSurfaces = Collections.singletonList(mImageReader.getSurface()); + camera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + TermuxApiLogger.info("onConfigured() from camera"); + try { + session.stopRepeating(); + session.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(CameraCaptureSession completedSession, + CaptureRequest request, TotalCaptureResult result) { + TermuxApiLogger.info("onCaptureCompleted()"); + camera.close(); + finish(); + } + }, null); + } catch (Exception e) { + TermuxApiLogger.error("onConfigured() error", e); + } + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + TermuxApiLogger.error("onConfigureFailed() error"); + } + }, null); + } catch (Exception e) { + TermuxApiLogger.error("in onOpened", e); + } + } + + @Override + public void onDisconnected(CameraDevice camera) { + TermuxApiLogger.info("onDisconnected() from camera"); + } + + @Override + public void onError(CameraDevice camera, int error) { + TermuxApiLogger.error("Failed opening camera: " + error); + setResult(1); + finish(); + } + + }, null); + } catch (Exception e) { + TermuxApiLogger.error("Error getting camera", e); + } + } + +} diff --git a/app/src/main/java/com/termux/api/SmsInboxAPI.java b/app/src/main/java/com/termux/api/SmsInboxAPI.java new file mode 100644 index 00000000..080fef91 --- /dev/null +++ b/app/src/main/java/com/termux/api/SmsInboxAPI.java @@ -0,0 +1,105 @@ +package com.termux.api; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.PhoneLookup; +import android.provider.Telephony; +import android.provider.Telephony.TextBasedSmsColumns; +import android.util.JsonWriter; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.ResultReturner.ResultJsonWriter; + +/** + * Call with + * + *
+ * $ am broadcast --user 0 -n net.aterm.extras/.SmsLister
+ * 
+ * Broadcasting: Intent { cmp=net.aterm.extras/.SmsLister }
+ * Broadcast completed: result=13, data="http://fornwall.net"
+ * 
+ */ +public class SmsInboxAPI { + + private static final String[] DISPLAY_NAME_PROJECTION = { PhoneLookup.DISPLAY_NAME }; + + static void onReceive(TermuxApiReceiver apiReceiver, final Context context, Intent intent) { + final int offset = intent.getIntExtra("offset", 0); + final int limit = intent.getIntExtra("limit", 50); + + ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { + @Override + public void writeJson(JsonWriter out) throws Exception { + getAllSms(context, out, offset, limit); + } + }); + } + + @SuppressLint("SimpleDateFormat") + public static void getAllSms(Context context, JsonWriter out, int offset, int limit) throws IOException { + ContentResolver cr = context.getContentResolver(); + String sortOrder = "date DESC LIMIT + " + limit + " OFFSET " + offset; + try (Cursor c = cr.query(Telephony.Sms.Inbox.CONTENT_URI, null, null, null, sortOrder)) { + + c.moveToLast(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd kk:mm"); + Map nameCache = new HashMap<>(); + + out.beginArray(); + for (int i = 0, count = c.getCount(); i < count; i++) { + // String smsId = c.getString(c.getColumnIndexOrThrow(Telephony.Sms.Inbox._ID)); + String smsAddress = c.getString(c.getColumnIndexOrThrow(TextBasedSmsColumns.ADDRESS)); + String smsBody = c.getString(c.getColumnIndexOrThrow(TextBasedSmsColumns.BODY)); + boolean read = (c.getInt(c.getColumnIndex(TextBasedSmsColumns.READ)) != 0); + long smsReceivedDate = c.getLong(c.getColumnIndexOrThrow(TextBasedSmsColumns.DATE)); + // long smsSentDate = c.getLong(c.getColumnIndexOrThrow(TextBasedSmsColumns.DATE_SENT)); + + String smsSenderName = getContactNameFromNumber(nameCache, context, smsAddress); + + out.beginObject(); + out.name("read").value(read); + + if (smsSenderName != null) { + out.name("sender").value(smsSenderName); + } + out.name("number").value(smsAddress); + + out.name("received").value(dateFormat.format(new Date(smsReceivedDate))); + // if (Math.abs(smsReceivedDate - smsSentDate) >= 60000) { + // out.write(" (sent "); + // out.write(dateFormat.format(new Date(smsSentDate))); + // out.write(")"); + // } + out.name("body").value(smsBody); + + c.moveToPrevious(); + out.endObject(); + } + out.endArray(); + } + } + + private static String getContactNameFromNumber(Map cache, Context context, String number) { + if (cache.containsKey(number)) + return cache.get(number); + Uri contactUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); + try (Cursor c = context.getContentResolver().query(contactUri, DISPLAY_NAME_PROJECTION, null, null, null)) { + String name = c.moveToFirst() ? c.getString(c.getColumnIndex(PhoneLookup.DISPLAY_NAME)) : null; + cache.put(number, name); + return name; + } + } + +} diff --git a/app/src/main/java/com/termux/api/SmsSendAPI.java b/app/src/main/java/com/termux/api/SmsSendAPI.java new file mode 100644 index 00000000..b2d8c7bc --- /dev/null +++ b/app/src/main/java/com/termux/api/SmsSendAPI.java @@ -0,0 +1,28 @@ +package com.termux.api; + +import java.io.PrintWriter; + +import android.content.Intent; +import android.telephony.SmsManager; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.TermuxApiLogger; + +public class SmsSendAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, final Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new ResultReturner.WithStringInput() { + @Override + public void writeResult(PrintWriter out) throws Exception { + final SmsManager smsManager = SmsManager.getDefault(); + String recipientExtra = intent.getStringExtra("recipient"); + if (recipientExtra == null) { + TermuxApiLogger.error("No 'recipient' extra"); + } else { + smsManager.sendTextMessage(recipientExtra, null, inputString, null, null); + } + } + }); + } + +} diff --git a/app/src/main/java/com/termux/api/SpeechToTextAPI.java b/app/src/main/java/com/termux/api/SpeechToTextAPI.java new file mode 100644 index 00000000..70b74ea2 --- /dev/null +++ b/app/src/main/java/com/termux/api/SpeechToTextAPI.java @@ -0,0 +1,208 @@ +package com.termux.api; + +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.IntentService; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.speech.RecognitionListener; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.TermuxApiLogger; + +public class SpeechToTextAPI { + + public static class SpeechToTextService extends IntentService { + + private static final String STOP_ELEMENT = ""; + + public SpeechToTextService() { + this(SpeechToTextService.class.getSimpleName()); + } + + public SpeechToTextService(String name) { + super(name); + } + + protected SpeechRecognizer mSpeechRecognizer; + final LinkedBlockingQueue queueu = new LinkedBlockingQueue<>(); + + @Override + public void onCreate() { + super.onCreate(); + final Context context = this; + + mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(this); + + mSpeechRecognizer.setRecognitionListener(new RecognitionListener() { + @Override + public void onRmsChanged(float rmsdB) { + // Do nothing. + } + + @Override + public void onResults(Bundle results) { + List recognitions = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + TermuxApiLogger.error("RecognitionListener#onResults(" + recognitions + ")"); + queueu.addAll(recognitions); + } + + @Override + public void onReadyForSpeech(Bundle params) { + // Do nothing. + } + + @Override + public void onPartialResults(Bundle partialResults) { + // Do nothing. + List strings = partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + TermuxApiLogger.error("RecognitionListener#onPartialResults(" + strings + ")"); + queueu.addAll(strings); + } + + @Override + public void onEvent(int eventType, Bundle params) { + // Do nothing. + } + + @Override + public void onError(int error) { + String description; + switch (error) { + case SpeechRecognizer.ERROR_CLIENT: + description = "ERROR_CLIENT"; + break; + case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + description = "ERROR_SPEECH_TIMEOUT"; + break; + case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + description = "ERROR_RECOGNIZER_BUSY"; + break; + case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + description = "ERROR_INSUFFICIENT_PERMISSIONS"; + break; + default: + description = Integer.toString(error); + } + TermuxApiLogger.error("RecognitionListener#onError(" + description + ")"); + queueu.add(STOP_ELEMENT); + } + + @Override + public void onEndOfSpeech() { + TermuxApiLogger.error("RecognitionListener#onEndOfSpeech()"); + queueu.add(STOP_ELEMENT); + } + + @Override + public void onBufferReceived(byte[] buffer) { + // Do nothing. + } + + @Override + public void onBeginningOfSpeech() { + // Do nothing. + } + }); + + PackageManager pm = context.getPackageManager(); + List installedList = pm.queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); + boolean speechRecognitionInstalled = !installedList.isEmpty(); + + if (!speechRecognitionInstalled) { + new AlertDialog.Builder(context).setMessage("For recognition it’s necessary to install \"Google Voice Search\"") + .setTitle("Install Voice Search from Google Play?").setPositiveButton("Install", new DialogInterface.OnClickListener() { // confirm + // button + // Install Button click handler + @Override + public void onClick(DialogInterface dialog, int which) { + Intent installIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.android.voicesearch")); + // setting flags to avoid going in application history (Activity call + // stack) + installIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + context.startActivity(installIntent); + } + }).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 + public void onDestroy() { + super.onDestroy(); + TermuxApiLogger.error("onDestroy"); + mSpeechRecognizer.destroy(); + } + + @Override + protected void onHandleIntent(final Intent intent) { + TermuxApiLogger.error("onHandleIntent"); + ResultReturner.returnData(this, intent, new ResultReturner.WithInput() { + @Override + public void writeResult(PrintWriter out) throws Exception { + while (true) { + String s = queueu.take(); + if (s == STOP_ELEMENT) { + return; + } else { + out.println(s); + } + } + } + }); + + } + } + + public static void onReceive(final Context context, Intent intent) { + context.startService(new Intent(context, SpeechToTextService.class).putExtras(intent.getExtras())); + } + + public static void runFromActivity(final Activity context) { + PackageManager pm = context.getPackageManager(); + List installedList = pm.queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); + boolean speechRecognitionInstalled = !installedList.isEmpty(); + + if (speechRecognitionInstalled) { + Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Select an application"); // user hint + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); + intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1); // quantity of results we want to receive + // context.startActivityForResult(intent, VOICE_RECOGNITION_REQUEST_CODE); + } else { + new AlertDialog.Builder(context).setMessage("For recognition it’s necessary to install \"Google Voice Search\"") + .setTitle("Install Voice Search from Google Play?").setPositiveButton("Install", new DialogInterface.OnClickListener() { // confirm + // button + // Install Button click handler + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.android.voicesearch")); + // setting flags to avoid going in application history (Activity call stack) + intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + context.startActivity(intent); + } + }).setNegativeButton("Cancel", null) // cancel button + .create().show(); + } + } + +} diff --git a/app/src/main/java/com/termux/api/TermuxApiReceiver.java b/app/src/main/java/com/termux/api/TermuxApiReceiver.java new file mode 100644 index 00000000..3eea982c --- /dev/null +++ b/app/src/main/java/com/termux/api/TermuxApiReceiver.java @@ -0,0 +1,64 @@ +package com.termux.api; + +import com.termux.api.util.TermuxApiLogger; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class TermuxApiReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String apiMethod = intent.getStringExtra("api_method"); + if (apiMethod == null) { + TermuxApiLogger.error("Missing 'api_method' extra"); + return; + } + + switch (apiMethod) { + case "BatteryStatus": + BatteryStatusAPI.onReceive(this, context, intent); + break; + case "CameraInfo": + CameraInfoAPI.onReceive(this, context, intent); + break; + case "Clipboard": + ClipboardAPI.onReceive(this, context, intent); + break; + case "ContactList": + ContactListAPI.onReceive(this, context, intent); + break; + case "Dialog": + context.startActivity(new Intent(context, DialogActivity.class).putExtras(intent.getExtras()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + break; + case "Download": + DownloadAPI.onReceive(this, context, intent); + break; + case "Location": + LocationAPI.onReceive(this, context, intent); + break; + case "Notification": + NotificationAPI.onReceive(this, context, intent); + break; + case "SmsInbox": + SmsInboxAPI.onReceive(this, context, intent); + break; + case "SmsSend": + SmsSendAPI.onReceive(this, intent); + break; + case "SpeechToText": + SpeechToTextAPI.onReceive(context, intent); + break; + case "TextToSpeech": + TextToSpeechAPI.onReceive(context, intent); + break; + case "Vibrate": + VibrateAPI.onReceive(this, context, intent); + break; + default: + TermuxApiLogger.error("Unrecognized 'api_method' extra: '" + apiMethod + "'"); + } + } + +} diff --git a/app/src/main/java/com/termux/api/TextToSpeechAPI.java b/app/src/main/java/com/termux/api/TextToSpeechAPI.java new file mode 100644 index 00000000..321fdb34 --- /dev/null +++ b/app/src/main/java/com/termux/api/TextToSpeechAPI.java @@ -0,0 +1,164 @@ +package com.termux.api; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.os.Bundle; +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.Engine; +import android.speech.tts.TextToSpeech.EngineInfo; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.speech.tts.UtteranceProgressListener; +import android.util.JsonWriter; + +import com.termux.api.util.ResultReturner; +import com.termux.api.util.TermuxApiLogger; + +public class TextToSpeechAPI { + + public static void onReceive(final Context context, Intent intent) { + context.startService(new Intent(context, TextToSpeechService.class).putExtras(intent.getExtras())); + } + + public static class TextToSpeechService extends IntentService { + TextToSpeech mTts; + final CountDownLatch mTtsLatch = new CountDownLatch(1); + + public TextToSpeechService() { + super(TextToSpeechService.class.getName()); + } + + @Override + public void onDestroy() { + if (mTts != null) + mTts.shutdown(); + super.onDestroy(); + } + + @Override + protected void onHandleIntent(final Intent intent) { + final String speechLanguage = intent.getStringExtra("language"); + final String speechEngine = intent.getStringExtra("engine"); + final float speechPitch = intent.getFloatExtra("pitch", 1.0f); + + mTts = new TextToSpeech(this, new OnInitListener() { + @Override + public void onInit(int status) { + if (status == TextToSpeech.SUCCESS) { + mTtsLatch.countDown(); + } else { + TermuxApiLogger.error("Failed tts initialization: status=" + status); + stopSelf(); + } + } + }, speechEngine); + + ResultReturner.returnData(this, intent, new ResultReturner.WithInput() { + @Override + public void writeResult(PrintWriter out) throws Exception { + + try { + try { + if (!mTtsLatch.await(10, TimeUnit.SECONDS)) { + TermuxApiLogger.error("Timeout waiting for TTS initialization"); + return; + } + } catch (InterruptedException e) { + TermuxApiLogger.error("Interrupted awaiting TTS initialization"); + return; + } + + if ("LIST_AVAILABLE".equals(speechEngine)) { + try (JsonWriter writer = new JsonWriter(out)) { + writer.setIndent(" "); + String defaultEngineName = mTts.getDefaultEngine(); + writer.beginArray(); + for (EngineInfo info : mTts.getEngines()) { + writer.beginObject(); + writer.name("name").value(info.name); + writer.name("label").value(info.label); + writer.name("default").value(defaultEngineName.equals(info.name)); + writer.endObject(); + } + writer.endArray(); + } + out.println(); + return; + } + + final AtomicInteger ttsDoneUtterancesCount = new AtomicInteger(); + + mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() { + @Override + public void onStart(String utteranceId) { + // Ignore. + } + + @Override + public void onError(String utteranceId) { + TermuxApiLogger.error("UtteranceProgressListener.onError() called"); + synchronized (ttsDoneUtterancesCount) { + ttsDoneUtterancesCount.incrementAndGet(); + ttsDoneUtterancesCount.notify(); + } + } + + @Override + public void onDone(String utteranceId) { + synchronized (ttsDoneUtterancesCount) { + ttsDoneUtterancesCount.incrementAndGet(); + ttsDoneUtterancesCount.notify(); + } + } + }); + + if (speechLanguage != null) { + int setLanguageResult = mTts.setLanguage(new Locale(speechLanguage)); + if (setLanguageResult != TextToSpeech.LANG_AVAILABLE) { + TermuxApiLogger.error("tts.setLanguage('" + speechLanguage + "') returned " + setLanguageResult); + } + } + + mTts.setPitch(speechPitch); + mTts.setSpeechRate(intent.getFloatExtra("rate", 1.0f)); + + String utteranceId = "utterance_id"; + Bundle params = new Bundle(); + params.putInt(Engine.KEY_PARAM_STREAM, AudioManager.STREAM_SYSTEM); + params.putString(Engine.KEY_PARAM_UTTERANCE_ID, utteranceId); + + int submittedUtterances = 0; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty()) { + submittedUtterances++; + mTts.speak(line, TextToSpeech.QUEUE_ADD, params, utteranceId); + } + } + } + + synchronized (ttsDoneUtterancesCount) { + while (ttsDoneUtterancesCount.get() != submittedUtterances) { + ttsDoneUtterancesCount.wait(); + } + } + } catch (Exception e) { + TermuxApiLogger.error("TTS error", e); + } + } + }); + } + } + +} diff --git a/app/src/main/java/com/termux/api/VibrateAPI.java b/app/src/main/java/com/termux/api/VibrateAPI.java new file mode 100644 index 00000000..10f70e4a --- /dev/null +++ b/app/src/main/java/com/termux/api/VibrateAPI.java @@ -0,0 +1,18 @@ +package com.termux.api; + +import android.content.Context; +import android.content.Intent; +import android.os.Vibrator; + +import com.termux.api.util.ResultReturner; + +public class VibrateAPI { + + static void onReceive(TermuxApiReceiver apiReceiver, Context context, Intent intent) { + Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + int milliseconds = intent.getIntExtra("duration_ms", 1000); + vibrator.vibrate(milliseconds); + ResultReturner.noteDone(apiReceiver, intent); + } + +} diff --git a/app/src/main/java/com/termux/api/util/ResultReturner.java b/app/src/main/java/com/termux/api/util/ResultReturner.java new file mode 100644 index 00000000..1fd3f6fb --- /dev/null +++ b/app/src/main/java/com/termux/api/util/ResultReturner.java @@ -0,0 +1,138 @@ +package com.termux.api.util; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.IntentService; +import android.content.BroadcastReceiver; +import android.content.BroadcastReceiver.PendingResult; +import android.content.Intent; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.util.JsonWriter; + +public class ResultReturner { + + /** + * An extra intent parameter which specifies a linux abstract namespace socket address where output from the API + * call should be written. + */ + private static final String SOCKET_OUTPUT_EXTRA = "socket_output"; + + /** + * An extra intent parameter which specifies a linux abstract namespace socket address where input to the API call + * can be read from. + */ + private static final String SOCKET_INPUT_EXTRA = "socket_input"; + + public static interface ResultWriter { + public void writeResult(PrintWriter out) throws Exception; + } + + /** Possible subclass of {@link ResultWriter} when input is to be read from stdin. */ + public static abstract class WithInput implements ResultWriter { + protected InputStream in; + + public void setInput(InputStream inputStream) throws Exception { + this.in = inputStream; + } + } + + /** Possible marker interface for a {@link ResultWriter} when input is to be read from stdin. */ + public static abstract class WithStringInput extends WithInput { + protected String inputString; + + @Override + public final void setInput(InputStream inputStream) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int l; + while ((l = inputStream.read(buffer)) > 0) { + baos.write(buffer, 0, l); + } + inputString = new String(baos.toByteArray(), StandardCharsets.UTF_8).trim(); + } + } + + public static abstract class ResultJsonWriter implements ResultWriter { + @Override + public final void writeResult(PrintWriter out) throws Exception { + JsonWriter writer = new JsonWriter(out); + writer.setIndent(" "); + writeJson(writer); + out.println(); // To add trailing newline. + } + + public abstract void writeJson(JsonWriter out) throws Exception; + } + + /** Just tell termux-api.c that we are done. */ + @SuppressLint("SdCardPath") + public static void noteDone(BroadcastReceiver receiver, final Intent intent) { + returnData(receiver, intent, null); + } + + /** Run in a separate thread, unless the context is an IntentService. */ + @SuppressLint("SdCardPath") + public static void returnData(Object context, final Intent intent, final ResultWriter resultWriter) { + final PendingResult asyncResult = (context instanceof BroadcastReceiver) ? ((BroadcastReceiver) context) + .goAsync() : null; + final Activity activity = (Activity) ((context instanceof Activity) ? context : null); + + final Runnable runnable = new Runnable() { + @Override + public void run() { + try { + try (LocalSocket outputSocket = new LocalSocket()) { + String outputSocketAdress = intent.getStringExtra(SOCKET_OUTPUT_EXTRA); + outputSocket.connect(new LocalSocketAddress(outputSocketAdress)); + try (PrintWriter writer = new PrintWriter(outputSocket.getOutputStream())) { + if (resultWriter != null) { + if (resultWriter instanceof WithInput) { + try (LocalSocket inputSocket = new LocalSocket()) { + String inputSocketAdress = intent.getStringExtra(SOCKET_INPUT_EXTRA); + inputSocket.connect(new LocalSocketAddress(inputSocketAdress)); + ((WithInput) resultWriter).setInput(inputSocket.getInputStream()); + resultWriter.writeResult(writer); + } + } else { + resultWriter.writeResult(writer); + } + } + } + } + + if (asyncResult != null) { + asyncResult.setResultCode(0); + } else if (activity != null) { + activity.setResult(0); + } + } catch (Exception e) { + TermuxApiLogger.error("Error in ResultReturner", e); + if (asyncResult != null) { + asyncResult.setResultCode(1); + } else if (activity != null) { + activity.setResult(1); + } + } finally { + if (asyncResult != null) { + asyncResult.finish(); + } else if (activity != null) { + activity.finish(); + } + } + } + }; + + if (context instanceof IntentService) { + runnable.run(); + } else { + new Thread(runnable).start(); + } + } + +} diff --git a/app/src/main/java/com/termux/api/util/TermuxApiLogger.java b/app/src/main/java/com/termux/api/util/TermuxApiLogger.java new file mode 100644 index 00000000..8ae6e2cc --- /dev/null +++ b/app/src/main/java/com/termux/api/util/TermuxApiLogger.java @@ -0,0 +1,21 @@ +package com.termux.api.util; + +import android.util.Log; + +public class TermuxApiLogger { + + private static final String TAG = "termux-api"; + + public static void info(String message) { + Log.i(TAG, message); + } + + public static void error(String message) { + Log.e(TAG, message); + } + + public static void error(String message, Exception exception) { + Log.e(TAG, message, exception); + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..aa06923d428c4e08592284b260fb81b172940d86 GIT binary patch literal 1094 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!EX7WqAsj$Z!;#Vf4nJ zFn$7IMq>sakZ#Em*NBqf{Irtt#G+J&^73-M%)IR4sEoADcKcY@32-_4j9qksr2j$n9aXnGyUhuDhL; zdy`JZlm!(NL?-4GN3@7}E>7!KXljx9qL6mR#`ee`qs)0rW_vC>_n9&9xcSWYzaP#m zzgO9zufXx2J@mKtr^D?AQ~oinTfOgFlY8$n!4)%peZ9Hax zNY%3b&p+ut``TCjx9kl6ebM~)=XL&!Wt>kZh1spUx>d>P+Y^g|B8CSRbKSmKFbiCj zUEibd{on4#7JJV;)s{KVxX|X}lneW=MQA8CN!SQ)*|n?d^y$;n;x|T|S$(;jw>ke) zZ+?!1gq&R7O}Fc%>;3MVT1h#+e*HQkGBVNn-;50J3AwjD)F#iEKi}O%>gdgzl1>XJ zT)$c47(M;Qty@x;UYh9X>227&x!Fgpxw)Bn{pO!duQ-ajm%0Z1wc>PkcAhb7)}dp^ z*w(FI|ImU@sFUU0yLVMGTiFG?__E|S_O$J{ukf{-?`}2s*riLC9#08!-uZv)Ql2tC zB_$mlorIJWmd%?t&zL#$;DZMX(iQ)@MNcj-QHGM zQt~8H!~WtmD~+jM55Ig7Nls4YXg|DT*RDg49xVd-(D~q-#DgB1tr;d=K5ERd*Eeq6 z+8UtIl4ExJ^qjp5J^$YnbaCl8{uqcXY;2C4Jjwa#=bm)Qzi#gSpJ&gW9ryHuF0bnK zcI}t>PrrQ=d;a{n?rjmSR=v4vtp%j!tJ$n(cU2N}Q4(B#Z(q8{$*GPkh8+rQf=2@8 zE{PQn;JUlJzqUW%a8jU~Kr=^@fFn<7)2ypYnq4;kI}PS4bu{YEVC_=-J6-UILX(6G zRMphwKb5@ZXK{S6U9)LYS$5w^u;O0U%?IL>gA87CpeW`weYxQBE;U_NW6c?*T~K4; z?)Yw)!0KRr_TSQ%#=A1N-Kspu+K~_`c1%sJdhXiyJ^p@&q-6KTYlN-7boFk$$4&-d zqJ41oSM;3PfBZ*xH{|puzxeU6llAkzSDL#SUVY;)3Vv1RFmqX_{PE_G@~vkJJooDV zkJ&DIJ?_>03Cl8~G(}J7?&|1>33?yD|HsqV{hK-ZTlJs34!YW&T~-3j^9-J@elF{r G5}E)iA@I!r literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..84451212932a100e7326c3feadbc7d5fab9258de GIT binary patch literal 786 zcmV+t1MU2YP) ze~>>SBtw>L&i)}@#aM(+4xNOiB}<7(DcM?RHxa@KO3t^#nf5eZdzZ+aCwZpB?@peV z_j})a?-W9gx;{Ama=y}2@+e3dw`ih%tUP^k4E+E;o}Dez<2YC?Z-~b7vvm4%6hKV% zKq=wqjYcWhdi{pX<`$UhAwcwcQA8*gi+uZf9-N^I%EcltvYTA*e$Bqv zlU@)`o)~tFI-K`kB9REWTn-tr-%6#@ZNJttw+H%nXS{!cQkp^aHp85$JE{+1Xj<=jW-_ zYSd~q>h*eXhQkP!D_4Lm1NKtvbUIY4RrdGySy))`SI`A^SH~JSJUqlO4EFZ+n46oU zP$>AI<{ifx0BE<{pH7~zGGKyz08&b^y1Gg> zn?*#}-Q5jH&jAt38HWsr2nPoTEG{n6YPD#!T7K$zA_`VR8dVGz7Z;8fiU`eS^TsIA z?RH0w7vn_ulYrZg5{6-rOeV);bO-*~g~GrT1t7xUJ#JEwfsQ#v83-vOhq+0|?m9gg z2rK;nOeXea&?w~Zy?9zO5MHJyfDl!{bhS}yoHhX!q8x}o{}pNIGjJb3&? z=imcPXd<|OpqU)*0|=Ud05nZGoi{NIJ4OLaOq3|HFJDnDmyro=N1D=D-`++<=qYQK zuzsnBUkX@T-(YQhBS=TrL_~gq?av+`PEM9JcpRLPdwTHw`O?y-pgqRIZ=2KCYTwvZ QKL7v#07*qoM6N<$g5Qf!-T(jq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bc8ecf9f69ed746777d832ec63557059d78cbb99 GIT binary patch literal 1315 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 Vby7Ad7L5NKU~Xa!tuk_p`wR6ji}?Tm literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/dialog_textarea_input.xml b/app/src/main/res/layout/dialog_textarea_input.xml new file mode 100644 index 00000000..00793744 --- /dev/null +++ b/app/src/main/res/layout/dialog_textarea_input.xml @@ -0,0 +1,39 @@ + + + + + + + +