Skip to content

Commit ec7568d

Browse files
Add support for session actions for foreground session commands
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.
1 parent 607ba3e commit ec7568d

File tree

3 files changed

+144
-34
lines changed

3 files changed

+144
-34
lines changed

app/src/main/java/com/termux/app/RunCommandService.java

+62-26
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,43 @@
1818
import com.termux.app.settings.properties.TermuxPropertyConstants;
1919
import com.termux.app.utils.Logger;
2020

21-
import java.io.File;
22-
import java.io.FileInputStream;
23-
import java.io.InputStreamReader;
24-
import java.nio.charset.StandardCharsets;
25-
import java.util.Properties;
26-
2721
/**
2822
* Third-party apps that are not part of termux world can run commands in termux context by either
2923
* sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin
3024
* client.
3125
*
32-
* For the RunCommandService intent to work, there are 2 main requirements:
26+
* For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, there are 2 main requirements:
27+
*
3328
* 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in
34-
* termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/`
35-
* directory.
29+
* termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/`
30+
* directory.
3631
* 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND`
37-
* permission in its `AndroidManifest.xml` and it should be granted by user to the app through the
38-
* app's App Info permissions page in android settings, likely under Additional Permissions.
32+
* permission in its `AndroidManifest.xml` and it should be granted by user to the app through the
33+
* app's App Info permissions page in android settings, likely under Additional Permissions.
34+
*
35+
*
36+
*
37+
* The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent expects the following extras:
38+
*
39+
* 1. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for absolute path of
40+
* command. This is mandatory.
41+
* 2. The {@code String[]} {@link RUN_COMMAND_SERVICE#EXTRA_ARGUMENTS} extra for any arguments to
42+
* pass to command. This is optional.
43+
* 3. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} extra for current working directory
44+
* of command. This is optional and defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}.
45+
* 4. The {@code boolean} {@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} extra whether to run command
46+
* in background or foreground terminal session. This is optional and defaults to {@code false}.
47+
* 5. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_SESSION_ACTION} extra for for session action
48+
* of foreground commands. This is optional and defaults to
49+
* {@link TERMUX_SERVICE#VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY}.
50+
*
51+
*
52+
*
53+
* The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR}
54+
* can optionally be prefixed with "$PREFIX/" or "~/" if an absolute path is not to be given.
55+
* The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and
56+
* "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/".
3957
*
40-
* The absolute path of executable or script must be given in "RUN_COMMAND_PATH" extra.
41-
* The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are
42-
* optional. The workdir defaults to termux home. The background mode defaults to "false".
43-
* The command path and workdir can optionally be prefixed with "$PREFIX/" or "~/" if an absolute
44-
* path is not to be given.
4558
*
4659
* To automatically bring termux session to foreground and start termux commands that were started
4760
* with background mode "false" in android >= 10 without user having to click the notification
@@ -51,10 +64,20 @@
5164
* Check https://github.com/termux/termux-tasker for more details on allow-external-apps and draw
5265
* over apps and other limitations.
5366
*
67+
*
5468
* To reduce the chance of termux being killed by android even further due to violation of not
5569
* being able to call startForeground() within ~5s of service start in android >= 8, the user
5670
* may disable battery optimizations for termux.
5771
*
72+
*
73+
* If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux`
74+
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
75+
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
76+
* errors in logcat and `RUN_COMMAND` won't work.
77+
* https://developer.android.com/training/basics/intents/package-visibility#package-name
78+
*
79+
*
80+
*
5881
* Sample code to run command "top" with java:
5982
* Intent intent = new Intent();
6083
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
@@ -63,6 +86,7 @@
6386
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
6487
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
6588
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
89+
* intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
6690
* startService(intent);
6791
*
6892
* Sample code to run command "top" with "am startservice" command:
@@ -71,13 +95,8 @@
7195
* --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top' \
7296
* --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5' \
7397
* --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home' \
74-
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false'
75-
*
76-
* If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux`
77-
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
78-
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
79-
* errors in logcat and `RUN_COMMAND` won't work.
80-
* https://developer.android.com/training/basics/intents/package-visibility#package-name
98+
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false' \
99+
* --es com.termux.RUN_COMMAND_SESSION_ACTION '0'
81100
*/
82101
public class RunCommandService extends Service {
83102

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

100119
@Override
101120
public void onCreate() {
121+
Logger.logVerbose(LOG_TAG, "onCreate");
102122
runStartForeground();
103123
}
104124

105125
@Override
106126
public int onStartCommand(Intent intent, int flags, int startId) {
127+
Logger.logDebug(LOG_TAG, "onStartCommand");
128+
107129
// Run again in case service is already started and onCreate() is not called
108130
runStartForeground();
109131

110-
// If wrong action passed, then just return
132+
// If invalid action passed, then just return
111133
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
112-
Logger.logError(LOG_TAG, "Unexpected intent action to RunCommandService: " + intent.getAction());
134+
Logger.logError(LOG_TAG, "Invalid intent action to RunCommandService: \"" + intent.getAction() + "\"");
113135
return Service.START_NOT_STICKY;
114136
}
115137

@@ -119,18 +141,32 @@ public int onStartCommand(Intent intent, int flags, int startId) {
119141
return Service.START_NOT_STICKY;
120142
}
121143

122-
Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH))).build();
144+
145+
146+
String commandPath = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
147+
// If invalid commandPath passed, then just return
148+
if (commandPath == null || commandPath.isEmpty()) {
149+
Logger.logError(LOG_TAG, "Invalid coommand path to RunCommandService: \"" + commandPath + "\"");
150+
return Service.START_NOT_STICKY;
151+
}
152+
153+
Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(commandPath)).build();
154+
155+
123156

124157
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, programUri);
125158
execIntent.setClass(this, TermuxService.class);
126159
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS));
127160
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false));
161+
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION));
128162

129163
String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
130164
if (workingDirectory != null && !workingDirectory.isEmpty()) {
131165
execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, getExpandedTermuxPath(workingDirectory));
132166
}
133167

168+
169+
134170
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
135171
this.startForegroundService(execIntent);
136172
} else {

app/src/main/java/com/termux/app/TermuxService.java

+47-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.termux.app.terminal.TermuxSessionClient;
2828
import com.termux.app.terminal.TermuxSessionClientBase;
2929
import com.termux.app.utils.Logger;
30+
import com.termux.app.utils.TextDataUtils;
3031
import com.termux.terminal.TerminalEmulator;
3132
import com.termux.terminal.TerminalSession;
3233
import com.termux.terminal.TerminalSessionClient;
@@ -276,10 +277,13 @@ private void actionServiceExecute(Intent intent) {
276277

277278
PendingIntent pendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
278279

280+
int sessionAction = TextDataUtils.getIntStoredAsStringFromBundle(intent.getExtras(),
281+
TERMUX_SERVICE.EXTRA_SESSION_ACTION, TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY);
282+
279283
if (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false)) {
280284
executeBackgroundCommand(executablePath, arguments, cwd, pendingIntent);
281285
} else {
282-
executeForegroundCommand(intent, executablePath, arguments, cwd);
286+
executeForegroundCommand(intent, executablePath, arguments, cwd, sessionAction);
283287
}
284288
}
285289

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

303307
/** Execute a shell command in a foreground terminal session. */
304-
private void executeForegroundCommand(Intent intent, String executablePath, String[] arguments, String cwd) {
308+
private void executeForegroundCommand(Intent intent, String executablePath, String[] arguments, String cwd, int sessionAction) {
305309
Logger.logDebug(LOG_TAG, "Starting foreground command");
306310

307311
boolean failsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
@@ -315,20 +319,55 @@ private void executeForegroundCommand(Intent intent, String executablePath, Stri
315319
newSession.mSessionName = name;
316320
}
317321

318-
// Make the newly created session the current one to be displayed:
319-
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this);
320-
preferences.setCurrentSession(newSession.mHandle);
321-
322322
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
323323
// activity in is foreground
324324
if(mTermuxSessionClient != null)
325325
mTermuxSessionClient.terminalSessionListNotifyUpdated();
326326

327-
// Launch the main Termux app, which will now show the current session:
328-
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
327+
handleSessionAction(sessionAction, newSession);
329328
}
330329

330+
private void setCurrentStoredSession(TerminalSession newSession) {
331+
// Make the newly created session the current one to be displayed:
332+
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this);
333+
preferences.setCurrentSession(newSession.mHandle);
334+
}
331335

336+
/** Process session action for new session. */
337+
private void handleSessionAction(int sessionAction, TerminalSession newSession) {
338+
Logger.logDebug(LOG_TAG, "Processing sessionAction \"" + sessionAction + "\" for session \"" + newSession.mSessionName + "\"");
339+
340+
switch (sessionAction) {
341+
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
342+
setCurrentStoredSession(newSession);
343+
if(mTermuxSessionClient != null)
344+
mTermuxSessionClient.setCurrentSession(newSession);
345+
startTermuxActivity();
346+
break;
347+
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
348+
if(mTerminalSessions.size() == 1)
349+
setCurrentStoredSession(newSession);
350+
startTermuxActivity();
351+
break;
352+
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
353+
setCurrentStoredSession(newSession);
354+
if(mTermuxSessionClient != null)
355+
mTermuxSessionClient.setCurrentSession(newSession);
356+
break;
357+
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
358+
if(mTerminalSessions.size() == 1)
359+
setCurrentStoredSession(newSession);
360+
break;
361+
default:
362+
Logger.logError(LOG_TAG, "Invalid sessionAction: \"" + sessionAction + "\"");
363+
break;
364+
}
365+
}
366+
367+
/** Launch the {@link }TermuxActivity} to bring it to foreground. */
368+
private void startTermuxActivity() {
369+
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
370+
}
332371

333372
/** Create a terminal session. */
334373
public TerminalSession createTerminalSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {

app/src/main/java/com/termux/app/utils/TextDataUtils.java

+35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.termux.app.utils;
22

3+
import android.os.Bundle;
4+
35
import java.util.LinkedHashSet;
46
import java.util.regex.Matcher;
57
import java.util.regex.Pattern;
@@ -21,6 +23,14 @@ public static String getTruncatedCommandOutput(String text, int maxLength) {
2123
return text;
2224
}
2325

26+
/**
27+
* Get the {@code float} from a {@link String}.
28+
*
29+
* @param value The {@link String value.
30+
* @param def The default value if failed to read a valid value.
31+
* @return Returns the {@code float} value after parsing the {@link String} value, otherwise
32+
* returns default if failed to read a valid value, like in case of an exception.
33+
*/
2434
public static float getFloatFromString(String value, float def) {
2535
if(value == null) return def;
2636

@@ -32,6 +42,14 @@ public static float getFloatFromString(String value, float def) {
3242
}
3343
}
3444

45+
/**
46+
* Get the {@code int} from a {@link String}.
47+
*
48+
* @param value The {@link String value.
49+
* @param def The default value if failed to read a valid value.
50+
* @return Returns the {@code int} value after parsing the {@link String} value, otherwise
51+
* returns default if failed to read a valid value, like in case of an exception.
52+
*/
3553
public static int getIntFromString(String value, int def) {
3654
if(value == null) return def;
3755

@@ -43,6 +61,23 @@ public static int getIntFromString(String value, int def) {
4361
}
4462
}
4563

64+
/**
65+
* Get an {@code int} from {@link Bundle} that is stored as a {@link String}.
66+
*
67+
* @param bundle The {@link Bundle} to get the value from.
68+
* @param key The key for the value.
69+
* @param def The default value if failed to read a valid value.
70+
* @return Returns the {@code int} value after parsing the {@link String} value stored in
71+
* {@link Bundle}, otherwise returns default if failed to read a valid value,
72+
* like in case of an exception.
73+
*/
74+
public static int getIntStoredAsStringFromBundle(Bundle bundle, String key, int def) {
75+
if(bundle == null) return def;
76+
return getIntFromString(bundle.getString(key, Integer.toString(def)), def);
77+
}
78+
79+
80+
4681
/**
4782
* If value is not in the range [min, max], set it to either min or max.
4883
*/

0 commit comments

Comments
 (0)