diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b2930376..23475866 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + = 10 without user having to click the notification manually, + * requires termux to be granted draw over apps permission due to new restrictions + * of starting activities from the background, this also applies to Termux:Tasker plugin. + * + * To reduce the chance of termux being killed by android even further due to violation of not + * being able to call startForeground() within ~5s of service start in android >= 8, the user + * may disable battery optimizations for termux. * * Sample code to run command "top" with java: * Intent intent = new Intent(); @@ -50,6 +68,9 @@ public class RunCommandService extends Service { public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR"; public static final String RUN_COMMAND_BACKGROUND = "com.termux.RUN_COMMAND_BACKGROUND"; + private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel"; + private static final int NOTIFICATION_ID = 1338; + class LocalBinder extends Binder { public final RunCommandService service = RunCommandService.this; } @@ -61,14 +82,23 @@ public class RunCommandService extends Service { return mBinder; } + @Override + public void onCreate() { + runStartForeground(); + } + + @Override public int onStartCommand(Intent intent, int flags, int startId) { + // Run again in case service is already started and onCreate() is not called + runStartForeground(); + if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) { - Uri programUri = new Uri.Builder().scheme("com.termux.file").path(intent.getStringExtra(RUN_COMMAND_PATH)).build(); + Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri); execIntent.setClass(this, TermuxService.class); execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS)); - execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, intent.getStringExtra(RUN_COMMAND_WORKDIR)); + execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, parsePath(intent.getStringExtra(RUN_COMMAND_WORKDIR))); execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -78,9 +108,56 @@ public class RunCommandService extends Service { } } + runStopForeground(); + return Service.START_NOT_STICKY; } + private void runStartForeground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setupNotificationChannel(); + startForeground(NOTIFICATION_ID, buildNotification()); + } + } + + private void runStopForeground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stopForeground(true); + } + } + + private Notification buildNotification() { + Notification.Builder builder = new Notification.Builder(this); + builder.setContentTitle(getText(R.string.application_name) + " Run Command"); + builder.setSmallIcon(R.drawable.ic_service_notification); + + // Use a low priority: + builder.setPriority(Notification.PRIORITY_LOW); + + // No need to show a timestamp: + builder.setShowWhen(false); + + // Background color for small notification icon: + builder.setColor(0xFF607D8B); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setChannelId(NOTIFICATION_CHANNEL_ID); + } + + return builder.build(); + } + + private void setupNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + String channelName = "Termux Run Command"; + int importance = NotificationManager.IMPORTANCE_LOW; + + NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } + private boolean allowExternalApps() { File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties"); if (!propsFile.exists()) @@ -99,4 +176,14 @@ public class RunCommandService extends Service { return props.getProperty("allow-external-apps", "false").equals("true"); } + + /** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */ + private String parsePath(String path) { + if(path != null && !path.isEmpty()) { + path = path.replaceAll("^\\$PREFIX\\/", TermuxService.PREFIX_PATH + "/"); + path = path.replaceAll("^~\\/", TermuxService.HOME_PATH + "/"); + } + + return path; + } }