Skip to content

Commit

Permalink
Add support for session actions for foreground session commands
Browse files Browse the repository at this point in the history
The `TERMUX_SERVICE.EXTRA_SESSION_ACTION` extra can be passed to define what should happen when a foreground session
command is received for the `TERMUX_SERVICE.ACTION_SERVICE_EXECUTE` intent to `TermuxService`, like from `RunCommandService` or `Termux:Tasker`. The user can define whether the new session should be automatically switched to or if existing session should remain as the current session. The user can also define if foreground session commands should open the `TermuxActivity` or if they should run in the "background" in the Termux notification. The user can click the notification to open the sessions. Check `TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION*` values to see various behaviors.

This also solves the old "issue" that if a foreground command was received while an existing session was already in the foreground, the new session won't be switched to automatically. It only brought the new session to the foreground if the activity was not already in foreground, since a call to `mTermuxSessionClient.setCurrentSession(newSession)` wasn't being made.
  • Loading branch information
agnostic-apollo committed Mar 19, 2021
1 parent 607ba3e commit ec7568d
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 34 deletions.
88 changes: 62 additions & 26 deletions app/src/main/java/com/termux/app/RunCommandService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,43 @@
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:
* 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.
* 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.
* 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
Expand All @@ -51,10 +64,20 @@
* 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");
Expand All @@ -63,6 +86,7 @@
* 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:
Expand All @@ -71,13 +95,8 @@
* --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 {

Expand All @@ -99,17 +118,20 @@ public IBinder onBind(Intent intent) {

@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;
}

Expand All @@ -119,18 +141,32 @@ public int onStartCommand(Intent intent, int flags, int startId) {
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 {
Expand Down
55 changes: 47 additions & 8 deletions app/src/main/java/com/termux/app/TermuxService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand Down Expand Up @@ -276,10 +277,13 @@ private void actionServiceExecute(Intent intent) {

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);
}
}

Expand All @@ -301,7 +305,7 @@ public void onBackgroundJobExited(final BackgroundJob task) {
}

/** 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);
Expand All @@ -315,20 +319,55 @@ private void executeForegroundCommand(Intent intent, String executablePath, Stri
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) {
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/com/termux/app/utils/TextDataUtils.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,6 +23,14 @@ public static String getTruncatedCommandOutput(String text, int maxLength) {
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;

Expand All @@ -32,6 +42,14 @@ public static float getFloatFromString(String value, float def) {
}
}

/**
* 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;

Expand All @@ -43,6 +61,23 @@ public static int getIntFromString(String value, int def) {
}
}

/**
* 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.
*/
Expand Down

0 comments on commit ec7568d

Please sign in to comment.