|
10 | 10 | import android.os.Binder;
|
11 | 11 | import android.os.Build;
|
12 | 12 | import android.os.IBinder;
|
| 13 | +import android.util.Log; |
13 | 14 |
|
14 | 15 | import com.termux.R;
|
15 | 16 | import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
16 | 17 | import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
17 |
| -import com.termux.app.settings.properties.SharedProperties; |
18 |
| -import com.termux.app.settings.properties.TermuxPropertyConstants; |
| 18 | +import com.termux.app.utils.FileUtils; |
19 | 19 | import com.termux.app.utils.Logger;
|
| 20 | +import com.termux.app.utils.PluginUtils; |
| 21 | + |
| 22 | +import java.util.Arrays; |
| 23 | +import java.util.HashMap; |
20 | 24 |
|
21 | 25 | /**
|
22 | 26 | * Third-party apps that are not part of termux world can run commands in termux context by either
|
23 | 27 | * sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin
|
24 | 28 | * client.
|
25 | 29 | *
|
26 |
| - * For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, there are 2 main requirements: |
| 30 | + * For the {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent to work, here are the requirements: |
27 | 31 | *
|
28 |
| - * 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in |
29 |
| - * termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/` |
30 |
| - * directory. |
31 |
| - * 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND` |
| 32 | + * 1. `com.termux.permission.RUN_COMMAND` permission (Mandatory) |
| 33 | + * The Intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND` |
32 | 34 | * 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. |
| 35 | + * app's `App Info` `Permissions` page in Android Settings, likely under `Additional Permissions`. |
| 36 | + * This is a security measure to prevent any other apps from running commands in `Termux` context |
| 37 | + * which do not have the required permission granted to them. |
| 38 | + * |
| 39 | + * For `Tasker` you can grant it with: |
| 40 | + * `Android Settings` -> `Apps` -> `Tasker` -> `Permissions` -> `Additional permissions` -> |
| 41 | + * `Run commands in Termux environment`. |
| 42 | + * |
| 43 | + * 2. `allow-external-apps` property (Mandatory) |
| 44 | + * The `allow-external-apps` property must be set to "true" in `~/.termux/termux.properties` in |
| 45 | + * Termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/` |
| 46 | + * directory. Check https://github.com/termux/termux-tasker#allow-external-apps-property-optional |
| 47 | + * for more info. |
| 48 | + * |
| 49 | + * 3. `Draw Over Apps` permission (Optional) |
| 50 | + * For android `>= 10` there are new |
| 51 | + * [restrictions](https://developer.android.com/guide/components/activities/background-starts) |
| 52 | + * that prevent activities from starting from the background. This prevents the background |
| 53 | + * {@link TermuxService} from starting a terminal session in the foreground and running the |
| 54 | + * commands until the user manually clicks `Termux` notification in the status bar dropdown |
| 55 | + * notifications list. This only affects commands that are to be executed in a terminal |
| 56 | + * session and not the background ones. `Termux` version `>= 0.100` |
| 57 | + * requests the `Draw Over Apps` permission so that users can bypass this restriction so |
| 58 | + * that commands can automatically start running without user intervention. |
| 59 | + * You can grant `Termux` the `Draw Over Apps` permission from its `App Info` activity: |
| 60 | + * `Android Settings` -> `Apps` -> `Termux` -> `Advanced` -> `Draw over other apps`. |
| 61 | + * |
| 62 | + * 4. `Storage` permission (Optional) |
| 63 | + * Termux app must be granted `Storage` permission if the executable is accessing or working |
| 64 | + * directory is set to path in external shared storage. The common paths which usually refer to |
| 65 | + * it are `~/storage`, `/sdcard`, `/storage/emulated/0` etc. |
| 66 | + * You can grant `Termux` the `Storage` permission from its `App Info` activity: |
| 67 | + * For Android version < 11: |
| 68 | + * `Android Settings` -> `Apps` -> `Termux` -> `Permissions` -> `Storage`. |
| 69 | + * For Android version >= 11 |
| 70 | + * `Android Settings` -> `Apps` -> `Termux` -> `Permissions` -> `Files and media` -> |
| 71 | + * `Allowed management of all files`. |
| 72 | + * NOTE: For Android version >= 11, sometimes you will get `Permission Denied` errors for |
| 73 | + * external shared storage even when you have granted `Files and media` permission. To solve |
| 74 | + * this, Deny the permission and then Allow it again and restart Termux. |
| 75 | + * Also check https://wiki.termux.com/wiki/Termux-setup-storage |
| 76 | + * |
| 77 | + * 5. Battery Optimizations (May be mandatory depending on device) |
| 78 | + * Some devices kill apps aggressively or prevent apps from starting from background. |
| 79 | + * If Termux is running into such problems, then exempt it from such restrictions. |
| 80 | + * The user may also disable battery optimizations for Termux to reduce the chances of Termux |
| 81 | + * being killed by Android even further due to violation of not being able to call |
| 82 | + * `startForeground()` within ~5s of service start in android >= 8. |
| 83 | + * Check https://dontkillmyapp.com/ for device specfic info to opt-out of battery optimiations. |
| 84 | + * |
| 85 | + * You may also want to check https://github.com/termux/termux-tasker |
34 | 86 | *
|
35 | 87 | *
|
36 | 88 | *
|
|
55 | 107 | * The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and
|
56 | 108 | * "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/".
|
57 | 109 | *
|
58 |
| - * |
59 |
| - * To automatically bring termux session to foreground and start termux commands that were started |
60 |
| - * with background mode "false" in android >= 10 without user having to click the notification |
61 |
| - * manually requires termux to be granted draw over apps permission due to new restrictions |
62 |
| - * of starting activities from the background, this also applies to Termux:Tasker plugin. |
63 |
| - * |
64 |
| - * Check https://github.com/termux/termux-tasker for more details on allow-external-apps and draw |
65 |
| - * over apps and other limitations. |
66 |
| - * |
67 |
| - * |
68 |
| - * To reduce the chance of termux being killed by android even further due to violation of not |
69 |
| - * being able to call startForeground() within ~5s of service start in android >= 8, the user |
70 |
| - * may disable battery optimizations for termux. |
71 |
| - * |
72 |
| - * |
73 | 110 | * If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux`
|
74 | 111 | * package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
75 | 112 | * `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
|
@@ -129,44 +166,80 @@ public int onStartCommand(Intent intent, int flags, int startId) {
|
129 | 166 | // Run again in case service is already started and onCreate() is not called
|
130 | 167 | runStartForeground();
|
131 | 168 |
|
| 169 | + String errmsg; |
| 170 | + |
132 | 171 | // If invalid action passed, then just return
|
133 | 172 | if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
134 |
| - Logger.logError(LOG_TAG, "Invalid intent action to RunCommandService: \"" + intent.getAction() + "\""); |
| 173 | + errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction()); |
| 174 | + Logger.logError(LOG_TAG, errmsg); |
135 | 175 | return Service.START_NOT_STICKY;
|
136 | 176 | }
|
137 | 177 |
|
138 |
| - // If allow-external-apps property is not set to "true" |
139 |
| - if (!SharedProperties.isPropertyValueTrue(this, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) { |
140 |
| - Logger.logError(LOG_TAG, "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file"); |
| 178 | + // If "allow-external-apps" property to not set to "true", then just return |
| 179 | + errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this); |
| 180 | + if (errmsg != null) { |
| 181 | + Logger.logError(LOG_TAG, errmsg); |
141 | 182 | return Service.START_NOT_STICKY;
|
142 | 183 | }
|
143 | 184 |
|
144 | 185 |
|
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 + "\""); |
| 186 | + String executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); |
| 187 | + String[] arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS); |
| 188 | + boolean inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false); |
| 189 | + String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); |
| 190 | + String sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); |
| 191 | + |
| 192 | + // Get canonical path of executable |
| 193 | + executable = FileUtils.getCanonicalPath(executable, null, true); |
| 194 | + |
| 195 | + // If executable is not a regular file, or is not readable or executable, then just return |
| 196 | + // Setting of missing read and execute permissions is not done |
| 197 | + errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executable, |
| 198 | + null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, |
| 199 | + false, false); |
| 200 | + if (errmsg != null) { |
| 201 | + errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable); |
| 202 | + Logger.logError(LOG_TAG, errmsg); |
150 | 203 | return Service.START_NOT_STICKY;
|
151 | 204 | }
|
152 | 205 |
|
153 |
| - Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(commandPath)).build(); |
| 206 | + // If workingDirectory is not null or empty |
| 207 | + if (workingDirectory != null && !workingDirectory.isEmpty()) { |
| 208 | + // Get canonical path of workingDirectory |
| 209 | + workingDirectory = FileUtils.getCanonicalPath(workingDirectory, null, true); |
| 210 | + |
| 211 | + // If workingDirectory is not a directory, or is not readable or writable, then just return |
| 212 | + // Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is |
| 213 | + // under {@link TermuxConstants#TERMUX_FILES_DIR_PATH} |
| 214 | + // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required |
| 215 | + // for working directories. |
| 216 | + errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory, |
| 217 | + TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, |
| 218 | + true, true, false, |
| 219 | + true); |
| 220 | + if (errmsg != null) { |
| 221 | + errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory); |
| 222 | + Logger.logError(LOG_TAG, errmsg); |
| 223 | + return Service.START_NOT_STICKY; |
| 224 | + } |
| 225 | + } |
154 | 226 |
|
| 227 | + PluginUtils.dumpExecutionIntentToLog(Log.VERBOSE, LOG_TAG, "RUN_COMMAND Intent", executable, Arrays.asList(arguments), workingDirectory, inBackground, new HashMap<String, Object>() {{ |
| 228 | + put("sessionAction", sessionAction); |
| 229 | + }}); |
155 | 230 |
|
| 231 | + Uri executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executable)).build(); |
156 | 232 |
|
157 |
| - Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, programUri); |
| 233 | + // Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE |
| 234 | + Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executableUri); |
158 | 235 | execIntent.setClass(this, TermuxService.class);
|
159 |
| - execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS)); |
160 |
| - 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)); |
162 |
| - |
163 |
| - String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); |
164 |
| - if (workingDirectory != null && !workingDirectory.isEmpty()) { |
165 |
| - execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, getExpandedTermuxPath(workingDirectory)); |
166 |
| - } |
167 |
| - |
| 236 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, arguments); |
| 237 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, inBackground); |
| 238 | + if (workingDirectory != null && !workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, workingDirectory); |
| 239 | + execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, sessionAction); |
168 | 240 |
|
169 | 241 |
|
| 242 | + // Start TERMUX_SERVICE and pass it execution intent |
170 | 243 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
171 | 244 | this.startForegroundService(execIntent);
|
172 | 245 | } else {
|
@@ -223,15 +296,4 @@ private void setupNotificationChannel() {
|
223 | 296 | manager.createNotificationChannel(channel);
|
224 | 297 | }
|
225 | 298 |
|
226 |
| - /** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */ |
227 |
| - public static String getExpandedTermuxPath(String path) { |
228 |
| - if(path != null && !path.isEmpty()) { |
229 |
| - path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH); |
230 |
| - path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/"); |
231 |
| - path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH); |
232 |
| - path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/"); |
233 |
| - } |
234 |
| - |
235 |
| - return path; |
236 |
| - } |
237 | 299 | }
|
0 commit comments