From 97b41002cb34c7e5b95c2a36216cf460ce527aee Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sun, 13 Apr 2025 23:06:34 +0500 Subject: [PATCH 01/13] Fixed(BatteryStatusAPI): Output double value instead of string for temperature field with value rounded up to 1 decimal place `String.format("%.1f", batteryTemperature)` was used in d5364ef3 to round up the value to 1 decimal place, which resulted in field being a string in json output and with a comma-as-decimal-separater depending on device locale, like `25,5` instead of `25.5`, so we stay with double now. Closes #763 --- .../main/java/com/termux/api/apis/BatteryStatusAPI.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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)); From 922f3585814174734ec4ac034a1ff55c13d9c5da Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 17:03:22 +0500 Subject: [PATCH 02/13] Fixed: Manually add stack trace of caller function that called `ResultReturner.returnData()` to exception messages as exceptions thrown inside `Runnable` lambda thread will not include it by default For example, exceptions like following don't tell which API call failed and where. ``` Error in ResultReturner: java.io.IOException: Connection refused at android.net.LocalSocketImpl.connectLocal(Native Method) at android.net.LocalSocketImpl.connect(LocalSocketImpl.java:259) at android.net.LocalSocket.connect(LocalSocket.java:162) at com.termux.api.util.ResultReturner.lambda$returnData$0(ResultReturner.java:244) at com.termux.api.util.ResultReturner$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) at java.lang.Thread.run(Thread.java:1119) ``` Now it will be something like this ``` Error in ResultReturner: java.io.IOException: Connection refused at android.net.LocalSocketImpl.connectLocal(Native Method) at android.net.LocalSocketImpl.connect(LocalSocketImpl.java:259) at android.net.LocalSocket.connect(LocalSocket.java:162) at com.termux.api.util.ResultReturner.lambda$returnData$0(ResultReturner.java:244) at com.termux.api.util.ResultReturner$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) at java.lang.Thread.run(Thread.java:1119) Suppressed: java.lang.Exception: Called by: at com.termux.api.util.ResultReturner.returnData(ResultReturner.java:234) at com.termux.api.apis.BatteryStatusAPI.onReceive(BatteryStatusAPI.java:27) at com.termux.api.TermuxApiReceiver.doWork(TermuxApiReceiver.java:91) at com.termux.api.TermuxApiReceiver.onReceive(TermuxApiReceiver.java:65) at android.app.ActivityThread.handleReceiver(ActivityThread.java:5029) at android.app.ActivityThread.-$$Nest$mhandleReceiver(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2577) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loopOnce(Looper.java:248) at android.os.Looper.loop(Looper.java:338) at android.app.ActivityThread.main(ActivityThread.java:9067) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932) ``` --- .../com/termux/api/util/ResultReturner.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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..631357849 100644 --- a/app/src/main/java/com/termux/api/util/ResultReturner.java +++ b/app/src/main/java/com/termux/api/util/ResultReturner.java @@ -223,6 +223,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 +235,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 +284,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 +318,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) { From 02b1c66ce94b8cba5a4d1a46cb82ac4a6cee2314 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 17:29:26 +0500 Subject: [PATCH 03/13] Changed: Remove unused imports --- app/src/main/java/com/termux/api/util/ResultReturner.java | 1 - 1 file changed, 1 deletion(-) 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 631357849..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; From 7c1759a3a5f2372b76f0d09cc9d305d702150c97 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 18:03:25 +0500 Subject: [PATCH 04/13] Fixed(NotificationAPI): Fix `termux-notification` actions not updating or conflicting between notifications due to same request code being used for the `PendingIntent` of all actions and also specify `PendingIntent.FLAG_IMMUTABLE` to prevent any modification - https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability Closes #677, Closes #764 --- .../com/termux/api/apis/NotificationAPI.java | 8 ++++- .../termux/api/util/PendingIntentUtils.java | 34 ++++++++++++++++++ .../java/com/termux/api/util/PluginUtils.java | 36 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/termux/api/util/PendingIntentUtils.java create mode 100644 app/src/main/java/com/termux/api/util/PluginUtils.java 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..71adba3b7 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; @@ -421,6 +423,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; + } + +} From 1639e73f6704fd92c05d16f75e87c4010cbd343d Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 18:44:52 +0500 Subject: [PATCH 05/13] Fixed(NotificationAPI): Fix `termux-notification --icon` not working for release builds as icon drawables were being removed by resource shrinker possibly related to 6ef2618f Use standard Android API to get resource id for drawable icon instead of using reflection into drawable class. Additionally use `String.format()` so that resource shrinker does not remove icons used by `NotificationAPI` for release builds. Closes #483 --- .../com/termux/api/apis/NotificationAPI.java | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) 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 71adba3b7..bc4ca8ae4 100644 --- a/app/src/main/java/com/termux/api/apis/NotificationAPI.java +++ b/app/src/main/java/com/termux/api/apis/NotificationAPI.java @@ -202,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()); @@ -240,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); } From 28bc8c856c3f85cc7c888962b0516be6f782b36c Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 19:47:17 +0500 Subject: [PATCH 06/13] Fixed: Fix disabling obfuscation added in 6ef2618f caused due to wrong proguard file being used. This caused Termux classes like `ReportInfo` to be obfuscated making stacktraces harder to read. ``` java.lang.RuntimeException: Unable to start activity ComponentInfo{com.termux/com.termux.shared.activities.ReportActivity}: android.os.BadParcelableException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = I0.a) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3864) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4006) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:111) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2462) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loopOnce(Looper.java:240) at android.os.Looper.loop(Looper.java:351) at android.app.ActivityThread.main(ActivityThread.java:8377) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013) Caused by: android.os.BadParcelableException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = I0.a) at android.os.Parcel.readSerializableInternal(Parcel.java:5113) at android.os.Parcel.readValue(Parcel.java:4655) at android.os.Parcel.readValue(Parcel.java:4363) at android.os.Parcel.-$$Nest$mreadValue(Unknown Source:0) at android.os.Parcel$LazyValue.apply(Parcel.java:4461) at android.os.Parcel$LazyValue.apply(Parcel.java:4420) at android.os.BaseBundle.getValueAt(BaseBundle.java:394) at android.os.BaseBundle.getValue(BaseBundle.java:374) at android.os.BaseBundle.getValue(BaseBundle.java:357) at android.os.BaseBundle.getValue(BaseBundle.java:350) at android.os.BaseBundle.getSerializable(BaseBundle.java:1451) at android.os.Bundle.getSerializable(Bundle.java:1144) at com.termux.shared.activities.ReportActivity.updateUI(ReportActivity.java:136) at com.termux.shared.activities.ReportActivity.onCreate(ReportActivity.java:89) at android.app.Activity.performCreate(Activity.java:8397) at android.app.Activity.performCreate(Activity.java:8370) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1403) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3837) ... 12 more Caused by: java.lang.ClassNotFoundException: I0.a at java.lang.Class.classForName(Native Method) at java.lang.Class.forName(Class.java:536) at android.os.Parcel$2.resolveClass(Parcel.java:5090) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1733) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1624) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1902) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1442) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:430) at android.os.Parcel.readSerializableInternal(Parcel.java:5096) ... 29 more Caused by: java.lang.ClassNotFoundException: I0.a ... 38 more ``` Related commit https://github.com/termux/termux-app/commit/da3a0ac4 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a50127dcc..06c217aae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { From c986fb09787ecf18e0058e14f0a01615851d64ba Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 19:51:22 +0500 Subject: [PATCH 07/13] Fixed(NfcAPI): Fix `termux-nfc -t x` resulting in error notification being shown as method was not returned from after sending error resulting in `ResultReturner.returnData()` being called twice ``` Error in ResultReturner: java.io.IOException: Connection refused at android.net.LocalSocketImpl.connectLocal(Native Method) at android.net.LocalSocketImpl.connect(LocalSocketImpl.java:259) at android.net.LocalSocket.connect(LocalSocket.java:162) at com.termux.api.util.ResultReturner.lambda$returnData$0(SourceFile:250) at com.termux.api.util.ResultReturner.$r8$lambda$RFR2zSHu5FsJH7JvuCx4CPnUmMY(SourceFile:0) at com.termux.api.util.ResultReturner$$ExternalSyntheticLambda0.run(SourceFile:0) at java.lang.Thread.run(Thread.java:1119) Suppressed: java.lang.Exception: Called by: at com.termux.api.util.ResultReturner.returnData(SourceFile:239) at com.termux.api.apis.NfcAPI$NfcActivity.errorNfc(SourceFile:48) at com.termux.api.apis.NfcAPI$NfcActivity.onCreate(SourceFile:87) at android.app.Activity.performCreate(Activity.java:9155) at android.app.Activity.performCreate(Activity.java:9133) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1521) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4262) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4467) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:222) at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133) at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2823) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loopOnce(Looper.java:248) at android.os.Looper.loop(Looper.java:338) at android.app.ActivityThread.main(ActivityThread.java:9067) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932) ``` --- app/src/main/java/com/termux/api/apis/NfcAPI.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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..ef60192eb 100644 --- a/app/src/main/java/com/termux/api/apis/NfcAPI.java +++ b/app/src/main/java/com/termux/api/apis/NfcAPI.java @@ -69,21 +69,22 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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(); } } From 4ba140ddecda5b27545de94cb7361a58dd966ef2 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 20:11:17 +0500 Subject: [PATCH 08/13] Fixed(NfcAPI): Check if `NfcAdapter` is available and not `null` in `NfcActivity.onResume()` in addition to `onCreate()` before calling its methods otherwise would trigger a `NullPointerException` --- .../main/java/com/termux/api/apis/NfcAPI.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 ef60192eb..d32635dfc 100644 --- a/app/src/main/java/com/termux/api/apis/NfcAPI.java +++ b/app/src/main/java/com/termux/api/apis/NfcAPI.java @@ -33,7 +33,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,6 +67,7 @@ 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"; @@ -94,14 +96,21 @@ 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; + } Intent intentNew = new Intent(this, NfcActivity.class).addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intentNew, 0); 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 @@ -126,7 +135,7 @@ protected void onNewIntent(Intent intent) { protected void onPause() { Logger.logDebug(LOG_TAG, "onPause"); - adapter.disableForegroundDispatch(this); + mAdapter.disableForegroundDispatch(this); super.onPause(); } From 5040a66f81efe0d6ce4747983f388f4df36718fb Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 20:13:24 +0500 Subject: [PATCH 09/13] Fixed(NfcAPI): Explicitly specify `PendingIntent.FLAG_MUTABLE` flag for `PendingIntent` passed to `NfcAdapter.enableForegroundDispatch()` - https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability --- app/src/main/java/com/termux/api/apis/NfcAPI.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 d32635dfc..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; @@ -104,8 +105,11 @@ protected void onResume() { 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), From 9be2a6d8fb5bcb7bc3a5219741caf734772ec28d Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 22 May 2025 20:16:50 +0500 Subject: [PATCH 10/13] Added(JobSchedulerAPI): Add docs for `JobInfo` minimum period being restricted to `900000ms` (`15` minutes) for Android `>= 7` --- app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java | 3 +++ 1 file changed, 3 insertions(+) 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..e765a21a1 100644 --- a/app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java +++ b/app/src/main/java/com/termux/api/apis/JobSchedulerAPI.java @@ -163,6 +163,9 @@ 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); } From 6fbf61aa0e1baa47e8e507904e7c0e79f79222c4 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 23 May 2025 01:27:46 +0500 Subject: [PATCH 11/13] Changed|Fixed(JobSchedulerAPI): Fix `ResultReturner.returnData()` being called multiple times for different printing since 51a02f4f and 61262f75 throwing `Connection refused` exceptions for calls after the first one instead of printing, and also refactor actions out to functions and change action output format The `ResultReturner.returnData()` will be called once for each action. The schedule job action will not print all pending jobs at end and only the pending job that was scheduled if successful. Users should use `termux-job-scheduler -p` to get all pending jobs. The `termux-job-scheduler --p` action will print `No jobs found` instead of `No pending jobs` if no pending job exists. The `termux-job-scheduler --cancel-all` action will now print all jobs to be cancelled, or `No jobs found` found. The job description will use `4` spaces instead of a tab after the path. ``` Error in ResultReturner: java.io.IOException: Connection refused at android.net.LocalSocketImpl.connectLocal(Native Method) at android.net.LocalSocketImpl.connect(LocalSocketImpl.java:259) at android.net.LocalSocket.connect(LocalSocket.java:162) at com.termux.api.util.ResultReturner.lambda$returnData$0(ResultReturner.java:250) at com.termux.api.util.ResultReturner$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) at java.lang.Thread.run(Thread.java:1119) Suppressed: java.lang.Exception: Called by: at com.termux.api.util.ResultReturner.returnData(ResultReturner.java:239) at com.termux.api.apis.JobSchedulerAPI.displayPendingJobs(JobSchedulerAPI.java:197) at com.termux.api.apis.JobSchedulerAPI.onReceive(JobSchedulerAPI.java:181) at com.termux.api.TermuxApiReceiver.doWork(TermuxApiReceiver.java:146) at com.termux.api.TermuxApiReceiver.onReceive(TermuxApiReceiver.java:65) at android.app.ActivityThread.handleReceiver(ActivityThread.java:5029) at android.app.ActivityThread.-$$Nest$mhandleReceiver(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2577) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loopOnce(Looper.java:248) at android.os.Looper.loop(Looper.java:338) at android.app.ActivityThread.main(ActivityThread.java:9067) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932) ``` ```shell ~ $ termux-job-scheduler -s ~/test1.sh --period-ms 900000 --job-id 1 Scheduling Job 1: /data/data/com.termux/files/home/test1.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) - response 1 Pending Job 1: /data/data/com.termux/files/home/test1.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) ~ $ termux-job-scheduler -s ~/test2.sh --period-ms 900000 --job-id 2 Scheduling Job 2: /data/data/com.termux/files/home/test2.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) - response 1 Pending Job 2: /data/data/com.termux/files/home/test2.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) ~ $ termux-job-scheduler -s ~/test3.sh --period-ms 900000 --job-id 3 Scheduling Job 3: /data/data/com.termux/files/home/test3.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) - response 1 Pending Job 3: /data/data/com.termux/files/home/test3.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) ~ $ termux-job-scheduler -p Pending Job 3: /data/data/com.termux/files/home/test3.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) Pending Job 2: /data/data/com.termux/files/home/test2.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) Pending Job 1: /data/data/com.termux/files/home/test1.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) ~ $ termux-job-scheduler --cancel --job-id 2 Cancelling Job 2: /data/data/com.termux/files/home/test2.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) ~ $ termux-job-scheduler --cancel-all Cancelling Job 3: /data/data/com.termux/files/home/test3.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) Cancelling Job 1: /data/data/com.termux/files/home/test1.sh (periodic: 900000ms) (battery not low) (network: NetworkRequest [ NONE id=0, [ Capabilities: INTERNET&TRUSTED&VALIDATED&NOT_VCN_MANAGED&NOT_BANDWIDTH_CONSTRAINED Uid: 10215 UnderlyingNetworks: Null] ]) ~ $ termux-job-scheduler --cancel-all No jobs found ~ $ termux-job-scheduler -p No jobs found ~ $ termux-job-scheduler --cancel --job-id 2 No job 2 found ``` Closes #762, Closes #773 --- .../com/termux/api/apis/JobSchedulerAPI.java | 166 ++++++++++++------ 1 file changed, 111 insertions(+), 55 deletions(-) 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 e765a21a1..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()); @@ -169,44 +180,89 @@ public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Int 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); } From a3a231e6abb9ee76420f65a9fb14d3f440e3df97 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 23 May 2025 02:40:24 +0500 Subject: [PATCH 12/13] Changed: Bump `termux-shared` to `da3a0ac4e2` Related commit https://github.com/termux/termux-app/commit/4de0caac Related commit https://github.com/termux/termux-app/commit/da3a0ac4 Related commit https://github.com/termux/termux-api/commit/28bc8c85 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 06c217aae..630208986 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 From 7e225c97f58018d3f78d6fae17470782aadd8c17 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 23 May 2025 02:43:04 +0500 Subject: [PATCH 13/13] Release: v0.52.0 --- README.md | 2 +- app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 630208986..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)