1
0
mirror of https://github.com/danog/termux-api.git synced 2024-11-26 11:54:40 +01:00

Initial push

The project was just converted from Eclipse to Android Studio, so
there may be some glitches.
This commit is contained in:
Fredrik Fornwall 2015-07-26 02:23:21 +02:00
parent 9e043ef50d
commit 1f2e562ac7
38 changed files with 2150 additions and 2 deletions

39
.gitignore vendored Normal file
View File

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

18
.idea/gradle.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

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

33
app/build.gradle Normal file
View File

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

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux.api"
android:sharedUserId="com.termux"
android:versionCode="2"
android:versionName="0.2" >
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<!-- Resolve phone numbers to contact names: -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Some of the used permissions imply uses-feature, so we need to make it optional.
See http://developer.android.com/guide/topics/manifest/uses-feature-element.html#permissions -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.location" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<uses-feature android:name="android.hardware.telephony" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Material.Light" >
<receiver android:name="com.termux.api.TermuxApiReceiver"/>
<activity android:name="com.termux.api.PhotoActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"/>
<activity android:name="com.termux.api.DialogActivity" android:theme="@android:style/Theme.Material.Light.Dialog.Alert" android:noHistory="true" android:excludeFromRecents="true" android:exported="false"/>
<service android:name="com.termux.api.SpeechToTextAPI$SpeechToTextService"/>
<service android:name="com.termux.api.TextToSpeechAPI$TextToSpeechService" />
</application>
</manifest>

View File

@ -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();
}
});
}
}

View File

@ -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();
}
});
}
}

View File

@ -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.
}
}
});
}
}

View File

@ -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<String> 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();
}
}
}

View File

@ -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();
}
});
}
});
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<Size> {
@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<Surface> 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);
}
}
}

View File

@ -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
*
* <pre>
* $ am broadcast --user 0 -n net.aterm.extras/.SmsLister
*
* Broadcasting: Intent { cmp=net.aterm.extras/.SmsLister }
* Broadcast completed: result=13, data="http://fornwall.net"
* </pre>
*/
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<String, String> 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<String, String> 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;
}
}
}

View File

@ -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);
}
}
});
}
}

View File

@ -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<String> 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<String> 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<String> 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<ResolveInfo> installedList = pm.queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
boolean speechRecognitionInstalled = !installedList.isEmpty();
if (!speechRecognitionInstalled) {
new AlertDialog.Builder(context).setMessage("For recognition its 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<ResolveInfo> 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 its 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();
}
}
}

View File

@ -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 + "'");
}
}
}

View File

@ -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);
}
}
});
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<EditText
android:id="@+id/text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top|start"
android:inputType="textMultiLine"
android:lines="10"
android:scrollbars="vertical" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal" >
<Button
android:id="@+id/cancel_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel" />
<Button
android:id="@+id/ok_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,14 @@
<resources>
<!--
Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels.
-->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="metaButtonBarStyle" format="reference" />
<attr name="metaButtonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,5 @@
<resources>
<color name="black_overlay">#66000000</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Termux:API</string>
</resources>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FullscreenTheme" parent="android:Theme.Holo">
<item name="android:actionBarStyle">@style/FullscreenActionBarStyle</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@null</item>
<item name="metaButtonBarStyle">?android:attr/buttonBarStyle</item>
<item name="metaButtonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
</style>
<style name="FullscreenActionBarStyle" parent="android:Widget.Holo.ActionBar">
<item name="android:background">@color/black_overlay</item>
</style>
<style name="ButtonBar">
<item name="android:paddingLeft">2dp</item>
<item name="android:paddingTop">5dp</item>
<item name="android:paddingRight">2dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="android:background">@android:drawable/bottom_bar</item>
</style>
<style name="ButtonBarButton" />
</resources>

15
build.gradle Normal file
View File

@ -0,0 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip

164
gradlew vendored Executable file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradlew.bat vendored Normal file
View File

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View File

@ -0,0 +1 @@
include ':app'