mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-06 10:45:23 +08:00
Fully integrate ExectionCommand into RunCommandService
Users will now also be shown flashes and notifications in addition to log entries for missing allow-external-apps permission or for invalid extras passed like the executable. The flashes and notifications can be controlled with the Termux Settings -> Debugging -> Plugin Error Notifications toggle
This commit is contained in:
@@ -10,7 +10,6 @@ import android.net.Uri;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||||
@@ -18,9 +17,9 @@ import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
|||||||
import com.termux.app.utils.FileUtils;
|
import com.termux.app.utils.FileUtils;
|
||||||
import com.termux.app.utils.Logger;
|
import com.termux.app.utils.Logger;
|
||||||
import com.termux.app.utils.PluginUtils;
|
import com.termux.app.utils.PluginUtils;
|
||||||
|
import com.termux.app.utils.TextDataUtils;
|
||||||
import java.util.Arrays;
|
import com.termux.models.ExecutionCommand;
|
||||||
import java.util.HashMap;
|
import com.termux.models.ExecutionCommand.ExecutionState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Third-party apps that are not part of termux world can run commands in termux context by either
|
* Third-party apps that are not part of termux world can run commands in termux context by either
|
||||||
@@ -88,18 +87,24 @@ import java.util.HashMap;
|
|||||||
*
|
*
|
||||||
* The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent expects the following extras:
|
* 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
|
* 1. The **mandatory** {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for
|
||||||
* command. This is mandatory.
|
* absolute path of command.
|
||||||
* 2. The {@code String[]} {@link RUN_COMMAND_SERVICE#EXTRA_ARGUMENTS} extra for any arguments to
|
* 2. The {@code String[]} {@link RUN_COMMAND_SERVICE#EXTRA_ARGUMENTS} extra for any arguments to
|
||||||
* pass to command. This is optional.
|
* pass to command.
|
||||||
* 3. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} extra for current working directory
|
* 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}.
|
* of command. This defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}.
|
||||||
* 4. The {@code boolean} {@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} extra whether to run command
|
* 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}.
|
* in background or foreground terminal session. This defaults to {@code false}.
|
||||||
* 5. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_SESSION_ACTION} extra for for session action
|
* 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
|
* of foreground commands. This defaults to
|
||||||
* {@link TERMUX_SERVICE#VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY}.
|
* {@link TERMUX_SERVICE#VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY}.
|
||||||
*
|
* 6. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_LABEL} extra for label of the command.
|
||||||
|
* 7. The markdown {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_DESCRIPTION} extra for
|
||||||
|
* description of the command. This should ideally be get short.
|
||||||
|
* 8. The markdown {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_HELP} extra for help of
|
||||||
|
* the command. This can add details about the command. 3rd party apps can provide more info
|
||||||
|
* to users for setting up commands. Ideally a url link should be provided that goes into full
|
||||||
|
* details.
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR}
|
* The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR}
|
||||||
@@ -107,6 +112,20 @@ import java.util.HashMap;
|
|||||||
* The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and
|
* 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 "/".
|
* "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/".
|
||||||
*
|
*
|
||||||
|
*
|
||||||
|
* The `EXTRA_COMMAND_*` extras are used for logging and are their values are provided to users in case
|
||||||
|
* of failure in a popup. The popup shown is in commonmark-spec markdown using markwon library so
|
||||||
|
* make sure to follow its formatting rules. Also make sure to end lines with 2 blank spaces to prevent
|
||||||
|
* word-wrap wherever needed.
|
||||||
|
* It's the users and 3rd party apps responsibility to use them wisely. There are also android
|
||||||
|
* internal intent size limits (roughly 500KB) that must not exceed when sending intents so make sure
|
||||||
|
* the combined size of ALL extras is less than that.
|
||||||
|
* There are also limits on the arguments size you can pass to commands or the full command string
|
||||||
|
* length that can be run, which is likely equal to 131072 bytes or 128KB on an android device.
|
||||||
|
* Check https://github.com/termux/termux-tasker#arguments-and-result-data-limits for more details.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
* If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.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
|
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
||||||
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
|
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
|
||||||
@@ -138,7 +157,7 @@ import java.util.HashMap;
|
|||||||
public class RunCommandService extends Service {
|
public class RunCommandService extends Service {
|
||||||
|
|
||||||
private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
|
private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
|
||||||
private static final int NOTIFICATION_ID = 1338;
|
public static final int NOTIFICATION_ID = 1338;
|
||||||
|
|
||||||
private static final String LOG_TAG = "RunCommandService";
|
private static final String LOG_TAG = "RunCommandService";
|
||||||
|
|
||||||
@@ -166,78 +185,91 @@ public class RunCommandService extends Service {
|
|||||||
// Run again in case service is already started and onCreate() is not called
|
// Run again in case service is already started and onCreate() is not called
|
||||||
runStartForeground();
|
runStartForeground();
|
||||||
|
|
||||||
|
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||||
|
executionCommand.pluginAPIHelp = this.getString(R.string.run_command_service_api_help);
|
||||||
|
|
||||||
String errmsg;
|
String errmsg;
|
||||||
|
|
||||||
// If invalid action passed, then just return
|
// If invalid action passed, then just return
|
||||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction());
|
errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction());
|
||||||
Logger.logError(LOG_TAG, errmsg);
|
executionCommand.setStateFailed(1, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
|
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
||||||
|
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
||||||
|
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||||
|
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
executionCommand.commandLabel = TextDataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
|
||||||
|
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||||
|
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
|
||||||
|
|
||||||
|
if(!executionCommand.setState(ExecutionState.PRE_EXECUTION))
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
|
||||||
// If "allow-external-apps" property to not set to "true", then just return
|
// If "allow-external-apps" property to not set to "true", then just return
|
||||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||||
if (errmsg != null) {
|
if (errmsg != null) {
|
||||||
Logger.logError(LOG_TAG, errmsg);
|
executionCommand.setStateFailed(1, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
String executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
|
||||||
String[] arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
|
||||||
boolean inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
|
||||||
String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
|
||||||
String sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
|
||||||
|
|
||||||
// Get canonical path of executable
|
// Get canonical path of executable
|
||||||
executable = FileUtils.getCanonicalPath(executable, null, true);
|
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||||
|
|
||||||
// If executable is not a regular file, or is not readable or executable, then just return
|
// If executable is not a regular file, or is not readable or executable, then just return
|
||||||
// Setting of missing read and execute permissions is not done
|
// Setting of missing read and execute permissions is not done
|
||||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executable,
|
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executionCommand.executable,
|
||||||
null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS,
|
null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS,
|
||||||
false, false);
|
false, false);
|
||||||
if (errmsg != null) {
|
if (errmsg != null) {
|
||||||
errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable);
|
errmsg += "\n" + this.getString(R.string.executable_absolute_path, executionCommand.executable);
|
||||||
Logger.logError(LOG_TAG, errmsg);
|
executionCommand.setStateFailed(1, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If workingDirectory is not null or empty
|
// If workingDirectory is not null or empty
|
||||||
if (workingDirectory != null && !workingDirectory.isEmpty()) {
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||||
// Get canonical path of workingDirectory
|
// Get canonical path of workingDirectory
|
||||||
workingDirectory = FileUtils.getCanonicalPath(workingDirectory, null, true);
|
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||||
|
|
||||||
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||||
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||||
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||||
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||||
// for working directories.
|
// for working directories.
|
||||||
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory,
|
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, executionCommand.workingDirectory,
|
||||||
TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
|
TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
|
||||||
true, true, false,
|
true, true, false,
|
||||||
true);
|
true);
|
||||||
if (errmsg != null) {
|
if (errmsg != null) {
|
||||||
errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory);
|
errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, executionCommand.workingDirectory);
|
||||||
Logger.logError(LOG_TAG, errmsg);
|
executionCommand.setStateFailed(1, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginUtils.dumpExecutionIntentToLog(Log.VERBOSE, LOG_TAG, "RUN_COMMAND Intent", executable, Arrays.asList(arguments), workingDirectory, inBackground, new HashMap<String, Object>() {{
|
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||||
put("sessionAction", sessionAction);
|
|
||||||
}});
|
|
||||||
|
|
||||||
Uri executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executable)).build();
|
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||||
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executableUri);
|
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||||
execIntent.setClass(this, TermuxService.class);
|
execIntent.setClass(this, TermuxService.class);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, arguments);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, inBackground);
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
||||||
if (workingDirectory != null && !workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, workingDirectory);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, sessionAction);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||||
|
|
||||||
// Start TERMUX_SERVICE and pass it execution intent
|
// Start TERMUX_SERVICE and pass it execution intent
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -292,8 +324,8 @@ public class RunCommandService extends Service {
|
|||||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||||
|
|
||||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
|
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
|
||||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
manager.createNotificationChannel(channel);
|
notificationManager.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,38 +1,31 @@
|
|||||||
package com.termux.app.utils;
|
package com.termux.app.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxConstants;
|
import com.termux.app.TermuxConstants;
|
||||||
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.app.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.app.settings.preferences.TermuxPreferenceConstants;
|
||||||
import com.termux.app.settings.properties.SharedProperties;
|
import com.termux.app.settings.properties.SharedProperties;
|
||||||
import com.termux.app.settings.properties.TermuxPropertyConstants;
|
import com.termux.app.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.models.ReportInfo;
|
||||||
import java.util.HashMap;
|
import com.termux.models.ExecutionCommand;
|
||||||
import java.util.List;
|
import com.termux.models.UserAction;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class PluginUtils {
|
public class PluginUtils {
|
||||||
|
|
||||||
/** Plugin variable for stdout value of termux command */
|
private static final String NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS = "termux_plugin_command_errors_notification_channel";
|
||||||
public static final String PLUGIN_VARIABLE_STDOUT = "%stdout"; // Default: "%stdout"
|
private static final String NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS = TermuxConstants.TERMUX_APP_NAME + " Plugin Commands Errors";
|
||||||
/** Plugin variable for stderr value of termux command */
|
|
||||||
public static final String PLUGIN_VARIABLE_STDERR = "%stderr"; // Default: "%stderr"
|
|
||||||
/** Plugin variable for exit code value of termux command */
|
|
||||||
public static final String PLUGIN_VARIABLE_EXIT_CODE = "%result"; // Default: "%result"
|
|
||||||
/** Plugin variable for err value of termux command */
|
|
||||||
public static final String PLUGIN_VARIABLE_ERR = "%err"; // Default: "%err"
|
|
||||||
/** Plugin variable for errmsg value of termux command */
|
|
||||||
public static final String PLUGIN_VARIABLE_ERRMSG = "%errmsg"; // Default: "%errmsg"
|
|
||||||
|
|
||||||
/** Intent {@code Parcelable} extra containing original intent received from plugin host app by FireReceiver */
|
|
||||||
public static final String EXTRA_ORIGINAL_INTENT = "originalIntent"; // Default: "originalIntent"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
||||||
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
||||||
@@ -40,34 +33,23 @@ public class PluginUtils {
|
|||||||
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
||||||
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A regex to validate if a string matches a valid plugin host variable name with the percent sign "%" prefix.
|
|
||||||
* Valid values: A string containing a percent sign character "%", followed by 1 alphanumeric character,
|
|
||||||
* followed by 2 or more alphanumeric or underscore "_" characters but does not end with an underscore "_"
|
|
||||||
*/
|
|
||||||
public static final String PLUGIN_HOST_VARIABLE_NAME_MATCH_EXPRESSION = "%[a-zA-Z0-9][a-zA-Z0-9_]{2,}(?<!_)";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "PluginUtils";
|
private static final String LOG_TAG = "PluginUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send execution result of commands to the {@link PendingIntent} creator received by
|
* Send execution result of commands to the {@link PendingIntent} creator received by
|
||||||
* execution service if {@code pendingIntent} is not {@code null}
|
* execution service if {@code pendingIntent} is not {@code null}.
|
||||||
*
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
* @param logLevel The log level to dump the result.
|
* @param logLevel The log level to dump the result.
|
||||||
* @param logTag The log tag to use for logging.
|
* @param logTag The log tag to use for logging.
|
||||||
* @param context The {@link Context} that will be used to send result intent to the PluginResultsService.
|
|
||||||
* @param pendingIntent The {@link PendingIntent} sent by creator to the execution service.
|
* @param pendingIntent The {@link PendingIntent} sent by creator to the execution service.
|
||||||
* @param stdout The value for {@link TERMUX_SERVICE#EXTRA_STDOUT} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
* @param stdout The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent.
|
||||||
* @param stderr The value for {@link TERMUX_SERVICE#EXTRA_STDERR} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
* @param stderr The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent.
|
||||||
* @param exitCode The value for {@link TERMUX_SERVICE#EXTRA_EXIT_CODE} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
* @param exitCode The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent.
|
||||||
* @param errCode The value for {@link TERMUX_SERVICE#EXTRA_ERR} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
* @param errCode The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERR} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent.
|
||||||
* @param errmsg The value for {@link TERMUX_SERVICE#EXTRA_ERRMSG} extra of {@link TERMUX_SERVICE#EXTRA_RESULT_BUNDLE} bundle of intent.
|
* @param errmsg The value for {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG} extra of {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle of intent.
|
||||||
*/
|
*/
|
||||||
public static void sendExecuteResultToResultsService(final int logLevel, final String logTag, final Context context, final PendingIntent pendingIntent, final String stdout, final String stderr, final String exitCode, final String errCode, final String errmsg) {
|
public static void sendExecuteResultToResultsService(final Context context, final int logLevel, final String logTag, final PendingIntent pendingIntent, final String stdout, final String stderr, final String exitCode, final String errCode, final String errmsg) {
|
||||||
String label;
|
String label;
|
||||||
|
|
||||||
if(pendingIntent == null)
|
if(pendingIntent == null)
|
||||||
@@ -76,25 +58,25 @@ public class PluginUtils {
|
|||||||
label = "Sending execution result to " + pendingIntent.getCreatorPackage();
|
label = "Sending execution result to " + pendingIntent.getCreatorPackage();
|
||||||
|
|
||||||
Logger.logMesssage(logLevel, logTag, label + ":\n" +
|
Logger.logMesssage(logLevel, logTag, label + ":\n" +
|
||||||
TERMUX_SERVICE.EXTRA_STDOUT + ":\n```\n" + stdout + "\n```\n" +
|
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT + ":\n```\n" + stdout + "\n```\n" +
|
||||||
TERMUX_SERVICE.EXTRA_STDERR + ":\n```\n" + stderr + "\n```\n" +
|
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR + ":\n```\n" + stderr + "\n```\n" +
|
||||||
TERMUX_SERVICE.EXTRA_EXIT_CODE + ": `" + exitCode + "`\n" +
|
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE + ": `" + exitCode + "`\n" +
|
||||||
TERMUX_SERVICE.EXTRA_ERR + ": `" + errCode + "`\n" +
|
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR + ": `" + errCode + "`\n" +
|
||||||
TERMUX_SERVICE.EXTRA_ERRMSG + ": `" + errmsg + "`");
|
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG + ": `" + errmsg + "`");
|
||||||
|
|
||||||
// If pendingIntent is null, then just return
|
// If pendingIntent is null, then just return
|
||||||
if(pendingIntent == null) return;
|
if(pendingIntent == null) return;
|
||||||
|
|
||||||
final Bundle resultBundle = new Bundle();
|
final Bundle resultBundle = new Bundle();
|
||||||
|
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_STDOUT, stdout);
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_STDERR, stderr);
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
||||||
if (exitCode != null && !exitCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_EXIT_CODE, Integer.parseInt(exitCode));
|
if (exitCode != null && !exitCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, Integer.parseInt(exitCode));
|
||||||
if (errCode != null && !errCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_ERR, Integer.parseInt(errCode));
|
if (errCode != null && !errCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, Integer.parseInt(errCode));
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_ERRMSG, errmsg);
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
||||||
|
|
||||||
Intent resultIntent = new Intent();
|
Intent resultIntent = new Intent();
|
||||||
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_BUNDLE, resultBundle);
|
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
||||||
|
|
||||||
if(context != null) {
|
if(context != null) {
|
||||||
try {
|
try {
|
||||||
@@ -121,60 +103,108 @@ public class PluginUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send execution result of commands to the {@link PendingIntent} creator received by
|
* Proceses {@link ExecutionCommand} error.
|
||||||
* execution service if {@code pendingIntent} is not {@code null}
|
* The {@link ExecutionCommand#errCode} must have been set to a non-zero value.
|
||||||
|
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
||||||
|
* be set with appropriate error info.
|
||||||
|
* If the {@link TermuxPreferenceConstants.TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a flash and a notification will be shown for the error as well
|
||||||
|
* on the {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS} channel.
|
||||||
*
|
*
|
||||||
* @param logLevel The log level to dump the result.
|
* @param context The {@link Context} for operations.
|
||||||
* @param logTag The log tag to use for logging.
|
* @param logTag The log tag to use for logging.
|
||||||
* @param executable The executable received.
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
* @param arguments_list The arguments list for executable received.
|
|
||||||
* @param workingDirectory The working directory for the command received.
|
|
||||||
* @param inBackground The command should be run in background.
|
|
||||||
* @param additionalExtras The {@link HashMap} for additional extras received. The key will be
|
|
||||||
* used as the label to log the value. The object will be converted
|
|
||||||
* to {@link String} with a call to {@code value.toString()}.
|
|
||||||
*/
|
*/
|
||||||
public static void dumpExecutionIntentToLog(int logLevel, String logTag, String label, String executable, List<String> arguments_list, String workingDirectory, boolean inBackground, HashMap<String, Object> additionalExtras) {
|
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||||
if (label == null) label = "Execution Intent";
|
if(context == null || executionCommand == null) return;
|
||||||
|
|
||||||
StringBuilder executionIntentDump = new StringBuilder();
|
if(executionCommand.errCode == null || executionCommand.errCode == 0) {
|
||||||
|
Logger.logWarn(LOG_TAG, "Ignoring call to processPluginExecutionCommandError() since the execution command errCode has not been set to a non-zero value");
|
||||||
executionIntentDump.append(label).append(":\n");
|
return;
|
||||||
executionIntentDump.append("Executable: `").append(executable).append("`\n");
|
|
||||||
executionIntentDump.append("Arguments:").append(getArgumentsStringForLog(arguments_list)).append("\n");
|
|
||||||
executionIntentDump.append("Working Directory: `").append(workingDirectory).append("`\n");
|
|
||||||
executionIntentDump.append("inBackground: `").append(inBackground).append("`");
|
|
||||||
|
|
||||||
if(additionalExtras != null) {
|
|
||||||
for (Map.Entry<String, Object> entry : additionalExtras.entrySet()) {
|
|
||||||
executionIntentDump.append("\n").append(entry.getKey()).append(": `").append(entry.getValue()).append("`");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.logMesssage(logLevel, logTag, executionIntentDump.toString());
|
// Log the error and any exception
|
||||||
|
logTag = TextDataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
Logger.logStackTracesWithMessage(logTag, executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||||
|
// If user has disabled notifications for plugin, then just return
|
||||||
|
if (!preferences.getPluginErrorNotificationsEnabled())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flash the errmsg
|
||||||
|
Logger.showToast(context, executionCommand.errmsg, true);
|
||||||
|
|
||||||
|
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the error
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, ExecutionCommand.getDetailedMarkdownString(executionCommand), true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupPluginCommandErrorsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Use markdown in notification
|
||||||
|
CharSequence notifiationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
||||||
|
//CharSequence notifiationText = executionCommand.errmsg;
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notifiationText, notifiationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if(builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if(notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts arguments list to log friendly format. If arguments are null or of size 0, then
|
* Get {@link Notification.Builder} for {@link #NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS}
|
||||||
* nothing is returned. Otherwise following format is returned:
|
* and {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS}.
|
||||||
*
|
*
|
||||||
* ```
|
* @param context The {@link Context} for operations.
|
||||||
* Arg 0: `value`
|
* @param title The title for the notification.
|
||||||
* Arg 1: 'value`
|
* @param notifiationText The second line text of the notification.
|
||||||
* ```
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
*
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
* @param arguments_list The arguments list.
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
* @return Returns the formatted arguments list.
|
* @return Returns the {@link Notification.Builder}.
|
||||||
*/
|
*/
|
||||||
public static String getArgumentsStringForLog(List<String> arguments_list) {
|
@Nullable
|
||||||
if (arguments_list==null || arguments_list.size() == 0) return "";
|
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
StringBuilder arguments_list_string = new StringBuilder("\n```\n");
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
for(int i = 0; i != arguments_list.size(); i++) {
|
NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS, Notification.PRIORITY_HIGH,
|
||||||
arguments_list_string.append("Arg ").append(i).append(": `").append(arguments_list.get(i)).append("`\n");
|
title, notifiationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
}
|
|
||||||
arguments_list_string.append("```");
|
|
||||||
|
|
||||||
return arguments_list_string.toString();
|
if(builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link #NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS} and
|
||||||
|
* {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS,
|
||||||
|
NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -97,6 +97,19 @@ public class TextDataUtils {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the object itself if it is not {@code null}, otherwise default.
|
||||||
|
*
|
||||||
|
* @param object The {@link Object} to check.
|
||||||
|
* @param def The default {@link Object}.
|
||||||
|
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
|
||||||
|
*/
|
||||||
|
public static <T> T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) {
|
||||||
|
return (object == null) ? def : object;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||||
|
|
||||||
StringBuilder regex_sb = new StringBuilder();
|
StringBuilder regex_sb = new StringBuilder();
|
||||||
|
37
app/src/main/res/drawable/ic_error_notification.xml
Normal file
37
app/src/main/res/drawable/ic_error_notification.xml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!--
|
||||||
|
Updated notification icon compliant with system icons guidelines
|
||||||
|
https://material.io/design/iconography/system-icons.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M19.59,14
|
||||||
|
l-2.09,2.09
|
||||||
|
L15.41,14
|
||||||
|
L14,15.41
|
||||||
|
l2.09,2.09
|
||||||
|
L14,19.59
|
||||||
|
L15.41,21
|
||||||
|
l2.09,-2.08
|
||||||
|
L19.59,21
|
||||||
|
L21,19.59
|
||||||
|
l-2.08,-2.09
|
||||||
|
L21,15.41
|
||||||
|
L19.59,14
|
||||||
|
z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</vector>
|
@@ -16,7 +16,8 @@
|
|||||||
<string name="application_name">&TERMUX_APP_NAME;</string>
|
<string name="application_name">&TERMUX_APP_NAME;</string>
|
||||||
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
||||||
<string name="run_command_permission_label">Run commands in &TERMUX_APP_NAME; environment</string>
|
<string name="run_command_permission_label">Run commands in &TERMUX_APP_NAME; environment</string>
|
||||||
<string name="run_command_permission_description">execute arbitrary commands within &TERMUX_APP_NAME; environment</string>
|
<string name="run_command_permission_description">execute arbitrary commands within &TERMUX_APP_NAME;
|
||||||
|
environment</string>
|
||||||
<string name="new_session">New session</string>
|
<string name="new_session">New session</string>
|
||||||
<string name="new_session_failsafe">Failsafe</string>
|
<string name="new_session_failsafe">Failsafe</string>
|
||||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
<string name="toggle_soft_keyboard">Keyboard</string>
|
||||||
@@ -85,9 +86,10 @@
|
|||||||
<string name="validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
<string name="validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
||||||
<string name="validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
<string name="validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions fafiled: \"%1$s\"\nException: %2$s</string>
|
||||||
|
|
||||||
<string name="run_command_service_invalid_action">Invalid intent action to RunCommandService: \"%1$s\"</string>
|
<string name="run_command_service_invalid_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||||
<string name="run_command_service_invalid_command_path">Invalid coommand path to RunCommandService: \"%1$s\"</string>
|
<string name="run_command_service_invalid_command_path">Invalid coommand path to RunCommandService: `%1$s`</string>
|
||||||
<string name="run_command_service_allow_external_apps_ungranted_warning">RunCommandService require allow-external-apps property to be set to \"true\" in &TERMUX_PROPERTIES_PRIMARY_PATH_SHORT; file.</string>
|
<string name="run_command_service_allow_external_apps_ungranted_warning">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||||
|
<string name="run_command_service_api_help">Visit https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/app/RunCommandService.java for more info on RUN_COMMAND Intent usage.</string>
|
||||||
|
|
||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="share_with">Share With</string>
|
<string name="share_with">Share With</string>
|
||||||
|
Reference in New Issue
Block a user