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 00000000..aa06923d
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ
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 00000000..84451212
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ
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 00000000..bc8ecf9f
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..742e5629
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..18c48661
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png differ
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..4c562c69
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..327c0604
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #66000000
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..64edecd8
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Termux:API
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..0725f3eb
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..88d246d4
--- /dev/null
+++ b/build.gradle
@@ -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()
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8c0fb64a
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..0c71e760
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..91a7e269
--- /dev/null
+++ b/gradlew
@@ -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 "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/gradlew.bat
@@ -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
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..e7b4def4
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'