diff --git a/README.md b/README.md index 2fc25b279..f518e4432 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ allowed to call the API methods in this app). ## Installation -Latest version is `v0.51.0`. +Latest version is `v0.52.0`. Termux:API application can be obtained from [F-Droid](https://f-droid.org/en/packages/com.termux.api/). diff --git a/app/build.gradle b/app/build.gradle index a50127dcc..cab462071 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.termux.api" minSdk project.properties.minSdkVersion.toInteger() targetSdk project.properties.targetSdkVersion.toInteger() - versionCode 1000 - versionName "0.51.0" + versionCode 1001 + versionName "0.52.0" if (appVersionName) versionName = appVersionName validateVersionName(versionName) @@ -35,7 +35,7 @@ android { release { minifyEnabled true shrinkResources false // Reproducible builds - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } debug { @@ -73,7 +73,7 @@ dependencies { implementation "androidx.media:media:1.7.0" implementation "androidx.preference:preference:1.2.1" - implementation "com.termux.termux-app:termux-shared:9ee1c9d5ad" + implementation "com.termux.termux-app:termux-shared:da3a0ac4e2" // Use if below libraries are published locally by termux-app with `./gradlew publishReleasePublicationToMavenLocal` and used with `mavenLocal()`. // If updates are done, republish there and sync project with gradle files here // https://github.com/termux/termux-app/wiki/Termux-Libraries diff --git a/app/src/main/java/com/termux/api/apis/BatteryStatusAPI.java b/app/src/main/java/com/termux/api/apis/BatteryStatusAPI.java index 1d3c41f45..9ff4193df 100644 --- a/app/src/main/java/com/termux/api/apis/BatteryStatusAPI.java +++ b/app/src/main/java/com/termux/api/apis/BatteryStatusAPI.java @@ -87,7 +87,11 @@ public void writeJson(JsonWriter out) throws Exception { batteryPlugged = "PLUGGED_" + pluggedInt; } - double batteryTemperature = batteryStatus.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1) / 10.f; + // Android returns battery temperature as int in tenths of degrees Celsius, like 255, so convert it to a decimal like 25.5°C. + // - https://cs.android.com/android/platform/superproject/+/android-15.0.0_r1:hardware/interfaces/health/aidl/android/hardware/health/HealthInfo.aidl;l=77-80 + double batteryTemperature = ((double) batteryStatus.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, Integer.MIN_VALUE)) / 10f; + // Round the value to 1 decimal place. + batteryTemperature = (double) Math.round(batteryTemperature * 10.0f) / 10.0f; String batteryStatusString; int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); @@ -145,7 +149,7 @@ public void writeJson(JsonWriter out) throws Exception { out.name("health").value(batteryHealth); out.name("plugged").value(batteryPlugged); out.name("status").value(batteryStatusString); - out.name("temperature").value(String.format("%.1f", batteryTemperature)); + out.name("temperature").value(batteryTemperature); out.name("voltage").value(batteryVoltage); out.name("current").value(batteryCurrentNow); out.name("current_average").value(getIntProperty(batteryManager, BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE)); diff --git a/app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java b/app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java index eb7977938..bfd12a2d1 100644 --- a/app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java +++ b/app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java @@ -21,6 +21,7 @@ import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import java.io.File; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -56,29 +57,52 @@ private static String formatJobInfo(JobInfo jobInfo) { description.add(String.format(Locale.ENGLISH, "(network: %s)", jobInfo.getRequiredNetwork().toString())); } - return String.format(Locale.ENGLISH, "Job %d: %s\t%s", jobInfo.getId(), path, + return String.format(Locale.ENGLISH, "Job %d: %s %s", jobInfo.getId(), path, TextUtils.join(" ", description)); } public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Intent intent) { Logger.logDebug(LOG_TAG, "onReceive"); - final String scriptPath = intent.getStringExtra("script"); - - final int jobId = intent.getIntExtra("job_id", 0); - final boolean pending = intent.getBooleanExtra("pending", false); - final boolean cancel = intent.getBooleanExtra("cancel", false); final boolean cancelAll = intent.getBooleanExtra("cancel_all", false); - final int periodicMillis = intent.getIntExtra("period_ms", 0); + if (pending) { + ResultReturner.returnData(apiReceiver, intent, out -> { + runDisplayPendingJobsAction(context, out); + }); + } else if (cancelAll) { + ResultReturner.returnData(apiReceiver, intent, out -> { + runCancelAllJobsAction(context, out); + }); + } else if (cancel) { + ResultReturner.returnData(apiReceiver, intent, out -> { + runCancelJobAction(context, intent, out); + }); + } else { + ResultReturner.returnData(apiReceiver, intent, out -> { + runScheduleJobAction(context, intent, out); + }); + } + } + + private static void runScheduleJobAction(Context context, Intent intent, PrintWriter out) { + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + + int jobId = intent.getIntExtra("job_id", 0); + + Logger.logVerbose(LOG_TAG, "schedule_job: Running action for job " + jobId); + + String scriptPath = intent.getStringExtra("script"); String networkType = intent.getStringExtra("network"); - final boolean batteryNotLow = intent.getBooleanExtra("battery_not_low", true); - final boolean charging = intent.getBooleanExtra("charging", false); - final boolean persisted = intent.getBooleanExtra("persisted", false); - final boolean idle = intent.getBooleanExtra("idle", false); - final boolean storageNotLow = intent.getBooleanExtra("storage_not_low", false); + int periodicMillis = intent.getIntExtra("period_ms", 0); + boolean batteryNotLow = intent.getBooleanExtra("battery_not_low", true); + boolean charging = intent.getBooleanExtra("charging", false); + boolean persisted = intent.getBooleanExtra("persisted", false); + boolean idle = intent.getBooleanExtra("idle", false); + boolean storageNotLow = intent.getBooleanExtra("storage_not_low", false); + int networkTypeCode; if (networkType != null) { @@ -108,29 +132,14 @@ public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Int } - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - - if (pending) { - displayPendingJobs(apiReceiver, intent, jobScheduler); - return; - } - if (cancelAll) { - displayPendingJobs(apiReceiver, intent, jobScheduler); - ResultReturner.returnData(apiReceiver, intent, out -> out.println("Cancelling all jobs")); - jobScheduler.cancelAll(); - return; - } else if (cancel) { - cancelJob(apiReceiver, intent, jobScheduler, jobId); - return; - } - - // Schedule new job if (scriptPath == null) { - ResultReturner.returnData(apiReceiver, intent, out -> out.println("No script path given")); + Logger.logErrorPrivate(LOG_TAG, "schedule_job: " + "Script path not passed"); + out.println("No script path given"); return; } - final File file = new File(scriptPath); - final String fileCheckMsg; + + File file = new File(scriptPath); + String fileCheckMsg; if (!file.isFile()) { fileCheckMsg = "No such file: %s"; } else if (!file.canRead()) { @@ -142,10 +151,12 @@ public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Int } if (!fileCheckMsg.isEmpty()) { - ResultReturner.returnData(apiReceiver, intent, out -> out.println(String.format(fileCheckMsg, scriptPath))); + Logger.logErrorPrivate(LOG_TAG, "schedule_job: " + String.format(fileCheckMsg, scriptPath)); + out.println(String.format(fileCheckMsg, scriptPath)); return; } + PersistableBundle extras = new PersistableBundle(); extras.putString(JobSchedulerService.SCRIPT_FILE_PATH, file.getAbsolutePath()); @@ -163,47 +174,95 @@ public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Int } if (periodicMillis > 0) { + // For Android `>= 7`, the minimum period is 900000ms (15 minutes). + // - https://developer.android.com/reference/android/app/job/JobInfo#getMinPeriodMillis() + // - https://cs.android.com/android/_/android/platform/frameworks/base/+/10be4e90 builder = builder.setPeriodic(periodicMillis); } - JobInfo job = builder.build(); + JobInfo jobInfo = builder.build(); + final int scheduleResponse = jobScheduler.schedule(jobInfo); + String message = String.format(Locale.ENGLISH, "Scheduling %s - response %d", formatJobInfo(jobInfo), scheduleResponse); + printMessage(out, "schedule_job", message); + + displayPendingJob(out, jobScheduler, "schedule_job", "Pending", jobId); + } + + private static void runDisplayPendingJobsAction(Context context, PrintWriter out) { + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + + Logger.logVerbose(LOG_TAG, "display_pending_jobs: Running action"); + displayPendingJobs(out, jobScheduler, "display_pending_jobs", "Pending"); + } + + private static void runCancelAllJobsAction(Context context, PrintWriter out) { + Logger.logVerbose(LOG_TAG, "cancel_all_jobs: Running action"); + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + int jobsCount = displayPendingJobs(out, jobScheduler, "cancel_all_jobs", "Cancelling"); + if (jobsCount >= 0) { + Logger.logVerbose(LOG_TAG, "cancel_all_jobs: Cancelling " + jobsCount + " jobs"); + jobScheduler.cancelAll(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private static void runCancelJobAction(Context context, Intent intent, PrintWriter out) { + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - final int scheduleResponse = jobScheduler.schedule(job); + if (!intent.hasExtra("job_id")) { + Logger.logErrorPrivate(LOG_TAG, "cancel_job: Job id not passed"); + out.println("Job id not passed"); + return; + } - final String message = String.format(Locale.ENGLISH, "Scheduling %s - response %d", formatJobInfo(job), scheduleResponse); - Logger.logInfo(LOG_TAG, message); - ResultReturner.returnData(apiReceiver, intent, out -> out.println(message)); + int jobId = intent.getIntExtra("job_id", 0); + Logger.logVerbose(LOG_TAG, "cancel_job: Running action for job " + jobId); + + if (displayPendingJob(out, jobScheduler, "cancel_job", "Cancelling", jobId)) { + Logger.logVerbose(LOG_TAG, "cancel_job: Cancelling job " + jobId); + jobScheduler.cancel(jobId); + } + } - displayPendingJobs(apiReceiver, intent, jobScheduler); + private static boolean displayPendingJob(PrintWriter out, JobScheduler jobScheduler, + String actionTag, String actionLabel, int jobId) { + JobInfo jobInfo = jobScheduler.getPendingJob(jobId); + if (jobInfo == null) { + printMessage(out, actionTag, String.format(Locale.ENGLISH, "No job %d found", jobId)); + return false; + } + + printMessage(out, actionTag, String.format(Locale.ENGLISH, actionLabel + " %s", formatJobInfo(jobInfo))); + return true; } - private static void displayPendingJobs(TermuxApiReceiver apiReceiver, Intent intent, JobScheduler jobScheduler) { - // Display pending jobs - final List jobs = jobScheduler.getAllPendingJobs(); + + private static int displayPendingJobs(PrintWriter out, JobScheduler jobScheduler, String actionTag, String actionLabel) { + List jobs = jobScheduler.getAllPendingJobs(); if (jobs.isEmpty()) { - ResultReturner.returnData(apiReceiver, intent, out -> out.println("No pending jobs")); - return; + printMessage(out, actionTag, "No jobs found"); + return 0; } StringBuilder stringBuilder = new StringBuilder(); + boolean jobAdded = false; for (JobInfo job : jobs) { - stringBuilder.append(String.format(Locale.ENGLISH, "Pending %s\n", formatJobInfo(job))); + if (jobAdded) stringBuilder.append("\n"); + stringBuilder.append(String.format(Locale.ENGLISH, actionLabel + " %s", formatJobInfo(job))); + jobAdded = true; } - ResultReturner.returnData(apiReceiver, intent, out -> out.println(stringBuilder.toString())); + printMessage(out, actionTag, stringBuilder.toString()); + + return jobs.size(); } - @RequiresApi(api = Build.VERSION_CODES.N) - private static void cancelJob(TermuxApiReceiver apiReceiver, Intent intent, JobScheduler jobScheduler, int jobId) { - final JobInfo jobInfo = jobScheduler.getPendingJob(jobId); - if (jobInfo == null) { - ResultReturner.returnData(apiReceiver, intent, out -> out.println(String.format(Locale.ENGLISH, "No job %d found", jobId))); - return; - } - ResultReturner.returnData(apiReceiver, intent, out -> out.println(String.format(Locale.ENGLISH, "Cancelling %s", formatJobInfo(jobInfo)))); - jobScheduler.cancel(jobId); + + private static void printMessage(PrintWriter out, String actionTag, String message) { + Logger.logVerbose(LOG_TAG, actionTag + ": " + message); + out.println(message); } diff --git a/app/src/main/java/com/termux/api/apis/NfcAPI.java b/app/src/main/java/com/termux/api/apis/NfcAPI.java index a402c1384..d0d607941 100644 --- a/app/src/main/java/com/termux/api/apis/NfcAPI.java +++ b/app/src/main/java/com/termux/api/apis/NfcAPI.java @@ -16,6 +16,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import com.termux.api.util.PendingIntentUtils; import com.termux.api.util.ResultReturner; import com.termux.shared.logger.Logger; @@ -33,7 +34,8 @@ public static void onReceive(final Context context, final Intent intent) { public static class NfcActivity extends AppCompatActivity { - private NfcAdapter adapter; + private Intent mIntent; + private NfcAdapter mAdapter; static String socket_input; static String socket_output; String mode; @@ -66,24 +68,26 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = this.getIntent(); if (intent != null) { + mIntent = intent; mode = intent.getStringExtra("mode"); if (null == mode) mode = "noData"; - param =intent.getStringExtra("param"); + param = intent.getStringExtra("param"); if (null == param) param = "noData"; - value=intent.getStringExtra("value"); + value = intent.getStringExtra("value"); if (null == socket_input) socket_input = intent.getStringExtra("socket_input"); if (null == socket_output) socket_output = intent.getStringExtra("socket_output"); if (mode.equals("noData")) { errorNfc(this, intent,""); finish(); + return; } } NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this); - if((null==adapter)||(!adapter.isEnabled())){ - errorNfc(this,intent,""); + if (adapter == null || !adapter.isEnabled()) { + errorNfc(this, intent,""); finish(); } } @@ -93,14 +97,24 @@ protected void onResume() { Logger.logVerbose(LOG_TAG, "onResume"); super.onResume(); - adapter = NfcAdapter.getDefaultAdapter(this); + + mAdapter = NfcAdapter.getDefaultAdapter(this); + if (mAdapter == null || !mAdapter.isEnabled()) { + if (mIntent != null) + errorNfc(this, mIntent,""); + finish(); + return; + } + + // - https://developer.android.com/develop/connectivity/nfc/advanced-nfc#foreground-dispatch Intent intentNew = new Intent(this, NfcActivity.class).addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intentNew, 0); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intentNew, + PendingIntentUtils.getPendingIntentMutableFlag()); IntentFilter[] intentFilter = new IntentFilter[]{ new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)}; - adapter.enableForegroundDispatch(this, pendingIntent, intentFilter, null); + mAdapter.enableForegroundDispatch(this, pendingIntent, intentFilter, null); } @Override @@ -125,7 +139,7 @@ protected void onNewIntent(Intent intent) { protected void onPause() { Logger.logDebug(LOG_TAG, "onPause"); - adapter.disableForegroundDispatch(this); + mAdapter.disableForegroundDispatch(this); super.onPause(); } diff --git a/app/src/main/java/com/termux/api/apis/NotificationAPI.java b/app/src/main/java/com/termux/api/apis/NotificationAPI.java index caee093a3..bc4ca8ae4 100644 --- a/app/src/main/java/com/termux/api/apis/NotificationAPI.java +++ b/app/src/main/java/com/termux/api/apis/NotificationAPI.java @@ -22,6 +22,8 @@ import com.termux.api.R; import com.termux.api.TermuxAPIConstants; import com.termux.api.TermuxApiReceiver; +import com.termux.api.util.PendingIntentUtils; +import com.termux.api.util.PluginUtils; import com.termux.api.util.ResultReturner; import com.termux.shared.logger.Logger; import com.termux.shared.shell.command.ExecutionCommand; @@ -200,27 +202,35 @@ static Pair buildNotification(final Context notification.setWhen(System.currentTimeMillis()); notification.setShowWhen(true); - String SmallIcon = intent.getStringExtra("icon"); - - if (SmallIcon != null) { - final Class clz = R.drawable.class; - final Field[] fields = clz.getDeclaredFields(); - for (Field field : fields) { - String name = field.getName(); - if (name.equals("ic_" + SmallIcon + "_black_24dp")) { - try { - notification.setSmallIcon(field.getInt(clz)); - } catch (Exception e) { - break; - } + + String smallIconName = intent.getStringExtra("icon"); + if (smallIconName != null) { + // TODO: Add prefix to all icons used by `NotificationAPI` to force keep only those icons when providing termux-api-app as a library. + // TODO: Add new icons from https://fonts.google.com/icons | https://github.com/google/material-design-icons + // Use `String.format()` so that resource shrinker does not remove icons used by `NotificationAPI` for release builds. + // - https://web.archive.org/web/20250516083801/https://developer.android.com/build/shrink-code#keep-resources + String smallIconResourceName = String.format("ic_%1s_black_24dp", smallIconName); + Integer smallIconResourceId = null; + try { + //noinspection DiscouragedApi + smallIconResourceId = context.getResources().getIdentifier(smallIconResourceName, "drawable", context.getPackageName()); + if (smallIconResourceId == 0) { + smallIconResourceId = null; + Logger.logError(LOG_TAG, "Failed to find \"" + smallIconResourceName + "\" icon"); } + } catch (Exception e) { + Logger.logError(LOG_TAG, "Failed to find \"" + smallIconResourceName + "\" icon: " + e.getMessage()); + } + + if (smallIconResourceId != null) { + notification.setSmallIcon(smallIconResourceId); } } - String ImagePath = intent.getStringExtra("image-path"); - if (ImagePath != null) { - File imgFile = new File(ImagePath); + String imagePath = intent.getStringExtra("image-path"); + if (imagePath != null) { + File imgFile = new File(imagePath); if (imgFile.exists()) { Bitmap myBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath()); @@ -238,7 +248,7 @@ static Pair buildNotification(final Context String mediaNext = intent.getStringExtra("media-next"); if (mediaPrevious != null && mediaPause != null && mediaPlay != null && mediaNext != null) { - if (SmallIcon == null) { + if (smallIconName == null) { notification.setSmallIcon(android.R.drawable.ic_media_play); } @@ -421,6 +431,10 @@ static Intent createExecuteIntent(String action){ static PendingIntent createAction(final Context context, String action){ Intent executeIntent = createExecuteIntent(action); - return PendingIntent.getService(context, 0, executeIntent, 0); + // Use unique request code for each action created so that pending intent extras are updated + // and do not conflict when same action is recreated or between actions of different notifications. + return PendingIntent.getService(context, + PluginUtils.getLastPendingIntentRequestCode(context), executeIntent, + PendingIntentUtils.getPendingIntentImmutableFlag()); } } diff --git a/app/src/main/java/com/termux/api/util/PendingIntentUtils.java b/app/src/main/java/com/termux/api/util/PendingIntentUtils.java new file mode 100644 index 000000000..b8db213b4 --- /dev/null +++ b/app/src/main/java/com/termux/api/util/PendingIntentUtils.java @@ -0,0 +1,34 @@ +package com.termux.api.util; + +import android.app.PendingIntent; +import android.os.Build; + +public class PendingIntentUtils { + + /** + * Get {@link PendingIntent#FLAG_IMMUTABLE} flag. + * + * - https://developer.android.com/guide/components/intents-filters#DeclareMutabilityPendingIntent + * - https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability + */ + public static int getPendingIntentImmutableFlag() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + return PendingIntent.FLAG_IMMUTABLE; + else + return 0; + } + + /** + * Get {@link PendingIntent#FLAG_MUTABLE} flag. + * + * - https://developer.android.com/guide/components/intents-filters#DeclareMutabilityPendingIntent + * - https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability + */ + public static int getPendingIntentMutableFlag() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + return PendingIntent.FLAG_MUTABLE; + else + return 0; + } + +} diff --git a/app/src/main/java/com/termux/api/util/PluginUtils.java b/app/src/main/java/com/termux/api/util/PluginUtils.java new file mode 100644 index 000000000..080f6a181 --- /dev/null +++ b/app/src/main/java/com/termux/api/util/PluginUtils.java @@ -0,0 +1,36 @@ +package com.termux.api.util; + +import android.app.PendingIntent; +import android.content.Context; + +import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_API_APP; +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; + +public class PluginUtils { + + /** + * Try to get the next unique {@link PendingIntent} request code that isn't already being used by + * the app and which would create a unique {@link PendingIntent} that doesn't conflict with that + * of any other execution commands. + * + * @param context The {@link Context} for operations. + * @return Returns the request code that should be safe to use. + */ + public synchronized static int getLastPendingIntentRequestCode(final Context context) { + if (context == null) return TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context); + if (preferences == null) return TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + int lastPendingIntentRequestCode = preferences.getLastPendingIntentRequestCode(); + + int nextPendingIntentRequestCode = lastPendingIntentRequestCode + 1; + + if (nextPendingIntentRequestCode == Integer.MAX_VALUE || nextPendingIntentRequestCode < 0) + nextPendingIntentRequestCode = TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + preferences.setLastPendingIntentRequestCode(nextPendingIntentRequestCode); + return nextPendingIntentRequestCode; + } + +} diff --git a/app/src/main/java/com/termux/api/util/ResultReturner.java b/app/src/main/java/com/termux/api/util/ResultReturner.java index 8634ccbda..279176e56 100644 --- a/app/src/main/java/com/termux/api/util/ResultReturner.java +++ b/app/src/main/java/com/termux/api/util/ResultReturner.java @@ -29,7 +29,6 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -223,6 +222,10 @@ public static LocalSocketAddress getApiLocalSocketAddress(@NonNull Context conte } } + public static boolean shouldRunThreadForResultRunnable(Object context) { + return !(context instanceof IntentService); + } + /** * Run in a separate thread, unless the context is an IntentService. */ @@ -231,6 +234,10 @@ public static void returnData(Object context, final Intent intent, final ResultW final Activity activity = (Activity) ((context instanceof Activity) ? context : null); final PendingResult asyncResult = receiver != null ? receiver.goAsync() : null; + // Store caller function stack trace to add to exception messages thrown inside `Runnable` + // lambda in case its run in a thread as it will not be included by default. + final Throwable callerStackTrace = shouldRunThreadForResultRunnable(context) ? new Exception("Called by:") : null; + final Runnable runnable = () -> { PrintWriter writer = null; LocalSocket outputSocket = null; @@ -276,6 +283,8 @@ public static void returnData(Object context, final Intent intent, final ResultW } } catch (Throwable t) { String message = "Error in " + LOG_TAG; + if (callerStackTrace != null) + t.addSuppressed(callerStackTrace); Logger.logStackTraceWithMessage(LOG_TAG, message, t); TermuxPluginUtils.sendPluginCommandErrorNotification(ResultReturner.context, LOG_TAG, @@ -308,11 +317,11 @@ public static void returnData(Object context, final Intent intent, final ResultW } }; - if (context instanceof IntentService) { - runnable.run(); - } else { + if (shouldRunThreadForResultRunnable(context)) { new Thread(runnable).start(); - } + } else { + runnable.run(); + } } public static void setContext(Context context) {