diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index e6672656..f695967e 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -18,30 +18,43 @@ import com.termux.app.settings.properties.SharedProperties; import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.app.utils.Logger; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Properties; - /** * Third-party apps that are not part of termux world can run commands in termux context by either * sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin * client. * - * For the RunCommandService intent to work, there are 2 main requirements: - * 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in - * termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/` - * directory. - * 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND` - * permission in its `AndroidManifest.xml` and it should be granted by user to the app through the - * app's App Info permissions page in android settings, likely under Additional Permissions. + * For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, there are 2 main requirements: + * + * 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in + * termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/` + * directory. + * 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND` + * permission in its `AndroidManifest.xml` and it should be granted by user to the app through the + * app's App Info permissions page in android settings, likely under Additional Permissions. + * + * + * + * The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent expects the following extras: + * + * 1. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for absolute path of + * command. This is mandatory. + * 2. The {@code String[]} {@link RUN_COMMAND_SERVICE#EXTRA_ARGUMENTS} extra for any arguments to + * pass to command. This is optional. + * 3. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} extra for current working directory + * of command. This is optional and defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}. + * 4. The {@code boolean} {@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} extra whether to run command + * in background or foreground terminal session. This is optional and defaults to {@code false}. + * 5. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_SESSION_ACTION} extra for for session action + * of foreground commands. This is optional and defaults to + * {@link TERMUX_SERVICE#VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY}. + * + * + * + * The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} + * can optionally be prefixed with "$PREFIX/" or "~/" if an absolute path is not to be given. + * The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and + * "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/". * - * The absolute path of executable or script must be given in "RUN_COMMAND_PATH" extra. - * The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are - * optional. The workdir defaults to termux home. The background mode defaults to "false". - * The command path and workdir can optionally be prefixed with "$PREFIX/" or "~/" if an absolute - * path is not to be given. * * To automatically bring termux session to foreground and start termux commands that were started * with background mode "false" in android >= 10 without user having to click the notification @@ -51,10 +64,20 @@ import java.util.Properties; * Check https://github.com/termux/termux-tasker for more details on allow-external-apps and draw * over apps and other limitations. * + * * 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. * + * + * If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux` + * package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its + * `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED` + * errors in logcat and `RUN_COMMAND` won't work. + * https://developer.android.com/training/basics/intents/package-visibility#package-name + * + * + * * Sample code to run command "top" with java: * Intent intent = new Intent(); * intent.setClassName("com.termux", "com.termux.app.RunCommandService"); @@ -63,6 +86,7 @@ import java.util.Properties; * intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"}); * intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home"); * intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false); + * intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0"); * startService(intent); * * Sample code to run command "top" with "am startservice" command: @@ -71,13 +95,8 @@ import java.util.Properties; * --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top' \ * --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5' \ * --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home' \ - * --ez com.termux.RUN_COMMAND_BACKGROUND 'false' - * - * If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux` - * package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its - * `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED` - * errors in logcat and `RUN_COMMAND` won't work. - * https://developer.android.com/training/basics/intents/package-visibility#package-name + * --ez com.termux.RUN_COMMAND_BACKGROUND 'false' \ + * --es com.termux.RUN_COMMAND_SESSION_ACTION '0' */ public class RunCommandService extends Service { @@ -99,17 +118,20 @@ public class RunCommandService extends Service { @Override public void onCreate() { + Logger.logVerbose(LOG_TAG, "onCreate"); runStartForeground(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { + Logger.logDebug(LOG_TAG, "onStartCommand"); + // Run again in case service is already started and onCreate() is not called runStartForeground(); - // If wrong action passed, then just return + // If invalid action passed, then just return if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { - Logger.logError(LOG_TAG, "Unexpected intent action to RunCommandService: " + intent.getAction()); + Logger.logError(LOG_TAG, "Invalid intent action to RunCommandService: \"" + intent.getAction() + "\""); return Service.START_NOT_STICKY; } @@ -119,18 +141,32 @@ public class RunCommandService extends Service { return Service.START_NOT_STICKY; } - Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH))).build(); + + + String commandPath = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); + // If invalid commandPath passed, then just return + if (commandPath == null || commandPath.isEmpty()) { + Logger.logError(LOG_TAG, "Invalid coommand path to RunCommandService: \"" + commandPath + "\""); + return Service.START_NOT_STICKY; + } + + Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(commandPath)).build(); + + Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, programUri); execIntent.setClass(this, TermuxService.class); execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS)); execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false)); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION)); String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); if (workingDirectory != null && !workingDirectory.isEmpty()) { execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, getExpandedTermuxPath(workingDirectory)); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { this.startForegroundService(execIntent); } else { diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index cc960e90..4b43564e 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -27,6 +27,7 @@ import com.termux.app.settings.preferences.TermuxAppSharedPreferences; import com.termux.app.terminal.TermuxSessionClient; import com.termux.app.terminal.TermuxSessionClientBase; import com.termux.app.utils.Logger; +import com.termux.app.utils.TextDataUtils; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; @@ -276,10 +277,13 @@ public final class TermuxService extends Service { PendingIntent pendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); + int sessionAction = TextDataUtils.getIntStoredAsStringFromBundle(intent.getExtras(), + TERMUX_SERVICE.EXTRA_SESSION_ACTION, TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY); + if (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false)) { executeBackgroundCommand(executablePath, arguments, cwd, pendingIntent); } else { - executeForegroundCommand(intent, executablePath, arguments, cwd); + executeForegroundCommand(intent, executablePath, arguments, cwd, sessionAction); } } @@ -301,7 +305,7 @@ public final class TermuxService extends Service { } /** Execute a shell command in a foreground terminal session. */ - private void executeForegroundCommand(Intent intent, String executablePath, String[] arguments, String cwd) { + private void executeForegroundCommand(Intent intent, String executablePath, String[] arguments, String cwd, int sessionAction) { Logger.logDebug(LOG_TAG, "Starting foreground command"); boolean failsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); @@ -315,20 +319,55 @@ public final class TermuxService extends Service { newSession.mSessionName = name; } - // Make the newly created session the current one to be displayed: - TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this); - preferences.setCurrentSession(newSession.mHandle); - // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if // activity in is foreground if(mTermuxSessionClient != null) mTermuxSessionClient.terminalSessionListNotifyUpdated(); - // Launch the main Termux app, which will now show the current session: - startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + handleSessionAction(sessionAction, newSession); } + private void setCurrentStoredSession(TerminalSession newSession) { + // Make the newly created session the current one to be displayed: + TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this); + preferences.setCurrentSession(newSession.mHandle); + } + /** Process session action for new session. */ + private void handleSessionAction(int sessionAction, TerminalSession newSession) { + Logger.logDebug(LOG_TAG, "Processing sessionAction \"" + sessionAction + "\" for session \"" + newSession.mSessionName + "\""); + + switch (sessionAction) { + case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY: + setCurrentStoredSession(newSession); + if(mTermuxSessionClient != null) + mTermuxSessionClient.setCurrentSession(newSession); + startTermuxActivity(); + break; + case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY: + if(mTerminalSessions.size() == 1) + setCurrentStoredSession(newSession); + startTermuxActivity(); + break; + case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY: + setCurrentStoredSession(newSession); + if(mTermuxSessionClient != null) + mTermuxSessionClient.setCurrentSession(newSession); + break; + case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY: + if(mTerminalSessions.size() == 1) + setCurrentStoredSession(newSession); + break; + default: + Logger.logError(LOG_TAG, "Invalid sessionAction: \"" + sessionAction + "\""); + break; + } + } + + /** Launch the {@link }TermuxActivity} to bring it to foreground. */ + private void startTermuxActivity() { + startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } /** Create a terminal session. */ public TerminalSession createTerminalSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { diff --git a/app/src/main/java/com/termux/app/utils/TextDataUtils.java b/app/src/main/java/com/termux/app/utils/TextDataUtils.java index cbf8c3fb..aebacdae 100644 --- a/app/src/main/java/com/termux/app/utils/TextDataUtils.java +++ b/app/src/main/java/com/termux/app/utils/TextDataUtils.java @@ -1,5 +1,7 @@ package com.termux.app.utils; +import android.os.Bundle; + import java.util.LinkedHashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -21,6 +23,14 @@ public class TextDataUtils { return text; } + /** + * Get the {@code float} from a {@link String}. + * + * @param value The {@link String value. + * @param def The default value if failed to read a valid value. + * @return Returns the {@code float} value after parsing the {@link String} value, otherwise + * returns default if failed to read a valid value, like in case of an exception. + */ public static float getFloatFromString(String value, float def) { if(value == null) return def; @@ -32,6 +42,14 @@ public class TextDataUtils { } } + /** + * Get the {@code int} from a {@link String}. + * + * @param value The {@link String value. + * @param def The default value if failed to read a valid value. + * @return Returns the {@code int} value after parsing the {@link String} value, otherwise + * returns default if failed to read a valid value, like in case of an exception. + */ public static int getIntFromString(String value, int def) { if(value == null) return def; @@ -43,6 +61,23 @@ public class TextDataUtils { } } + /** + * Get an {@code int} from {@link Bundle} that is stored as a {@link String}. + * + * @param bundle The {@link Bundle} to get the value from. + * @param key The key for the value. + * @param def The default value if failed to read a valid value. + * @return Returns the {@code int} value after parsing the {@link String} value stored in + * {@link Bundle}, otherwise returns default if failed to read a valid value, + * like in case of an exception. + */ + public static int getIntStoredAsStringFromBundle(Bundle bundle, String key, int def) { + if(bundle == null) return def; + return getIntFromString(bundle.getString(key, Integer.toString(def)), def); + } + + + /** * If value is not in the range [min, max], set it to either min or max. */