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:
agnostic-apollo
2021-03-24 03:56:25 +05:00
parent ef1ab197b6
commit 31371b5e3d
5 changed files with 251 additions and 137 deletions

View File

@@ -10,7 +10,6 @@ import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import com.termux.R;
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.Logger;
import com.termux.app.utils.PluginUtils;
import java.util.Arrays;
import java.util.HashMap;
import com.termux.app.utils.TextDataUtils;
import com.termux.models.ExecutionCommand;
import com.termux.models.ExecutionCommand.ExecutionState;
/**
* 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:
*
* 1. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for absolute path of
* command. This is mandatory.
* 1. The **mandatory** {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for
* absolute path of command.
* 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
* 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
* 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
* 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}.
*
* 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}
@@ -107,6 +112,20 @@ import java.util.HashMap;
* 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 `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`
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
@@ -138,7 +157,7 @@ import java.util.HashMap;
public class RunCommandService extends Service {
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";
@@ -166,78 +185,91 @@ public class RunCommandService extends Service {
// Run again in case service is already started and onCreate() is not called
runStartForeground();
ExecutionCommand executionCommand = new ExecutionCommand();
executionCommand.pluginAPIHelp = this.getString(R.string.run_command_service_api_help);
String errmsg;
// If invalid action passed, then just return
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(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;
}
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
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
if (errmsg != null) {
Logger.logError(LOG_TAG, errmsg);
executionCommand.setStateFailed(1, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
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
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
// 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,
false, false);
if (errmsg != null) {
errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable);
Logger.logError(LOG_TAG, errmsg);
errmsg += "\n" + this.getString(R.string.executable_absolute_path, executionCommand.executable);
executionCommand.setStateFailed(1, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
return Service.START_NOT_STICKY;
}
// If workingDirectory is not null or empty
if (workingDirectory != null && !workingDirectory.isEmpty()) {
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
// 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
// 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}
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
// for working directories.
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory,
errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, executionCommand.workingDirectory,
TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS,
true, true, false,
true);
if (errmsg != null) {
errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory);
Logger.logError(LOG_TAG, errmsg);
errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, executionCommand.workingDirectory);
executionCommand.setStateFailed(1, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand);
return Service.START_NOT_STICKY;
}
}
PluginUtils.dumpExecutionIntentToLog(Log.VERBOSE, LOG_TAG, "RUN_COMMAND Intent", executable, Arrays.asList(arguments), workingDirectory, inBackground, new HashMap<String, Object>() {{
put("sessionAction", sessionAction);
}});
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
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
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.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, arguments);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, inBackground);
if (workingDirectory != null && !workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, workingDirectory);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, sessionAction);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -292,8 +324,8 @@ public class RunCommandService extends Service {
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
}
}

View File

@@ -1,38 +1,31 @@
package com.termux.app.utils;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.app.TermuxConstants;
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.TermuxPropertyConstants;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.termux.models.ReportInfo;
import com.termux.models.ExecutionCommand;
import com.termux.models.UserAction;
public class PluginUtils {
/** Plugin variable for stdout value of termux command */
public static final String PLUGIN_VARIABLE_STDOUT = "%stdout"; // Default: "%stdout"
/** 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"
private static final String NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS = "termux_plugin_command_errors_notification_channel";
private static final String NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS = TermuxConstants.TERMUX_APP_NAME + " Plugin Commands Errors";
/** 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"
@@ -40,34 +33,23 @@ public class PluginUtils {
* 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"
/**
* 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";
/**
* 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 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 stdout The value for {@link TERMUX_SERVICE#EXTRA_STDOUT} extra of {@link TERMUX_SERVICE#EXTRA_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 exitCode The value for {@link TERMUX_SERVICE#EXTRA_EXIT_CODE} extra of {@link TERMUX_SERVICE#EXTRA_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 errmsg The value for {@link TERMUX_SERVICE#EXTRA_ERRMSG} 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_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_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_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_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;
if(pendingIntent == null)
@@ -76,25 +58,25 @@ public class PluginUtils {
label = "Sending execution result to " + pendingIntent.getCreatorPackage();
Logger.logMesssage(logLevel, logTag, label + ":\n" +
TERMUX_SERVICE.EXTRA_STDOUT + ":\n```\n" + stdout + "\n```\n" +
TERMUX_SERVICE.EXTRA_STDERR + ":\n```\n" + stderr + "\n```\n" +
TERMUX_SERVICE.EXTRA_EXIT_CODE + ": `" + exitCode + "`\n" +
TERMUX_SERVICE.EXTRA_ERR + ": `" + errCode + "`\n" +
TERMUX_SERVICE.EXTRA_ERRMSG + ": `" + errmsg + "`");
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT + ":\n```\n" + stdout + "\n```\n" +
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR + ":\n```\n" + stderr + "\n```\n" +
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE + ": `" + exitCode + "`\n" +
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR + ": `" + errCode + "`\n" +
TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG + ": `" + errmsg + "`");
// If pendingIntent is null, then just return
if(pendingIntent == null) return;
final Bundle resultBundle = new Bundle();
resultBundle.putString(TERMUX_SERVICE.EXTRA_STDOUT, stdout);
resultBundle.putString(TERMUX_SERVICE.EXTRA_STDERR, stderr);
if (exitCode != null && !exitCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_EXIT_CODE, Integer.parseInt(exitCode));
if (errCode != null && !errCode.isEmpty()) resultBundle.putInt(TERMUX_SERVICE.EXTRA_ERR, Integer.parseInt(errCode));
resultBundle.putString(TERMUX_SERVICE.EXTRA_ERRMSG, errmsg);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
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_PLUGIN_RESULT_BUNDLE_ERR, Integer.parseInt(errCode));
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
Intent resultIntent = new Intent();
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_BUNDLE, resultBundle);
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
if(context != null) {
try {
@@ -121,60 +103,108 @@ public class PluginUtils {
}
/**
* Send execution result of commands to the {@link PendingIntent} creator received by
* execution service if {@code pendingIntent} is not {@code null}
* Proceses {@link ExecutionCommand} error.
* 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 executable The executable received.
* @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()}.
* @param executionCommand The {@link ExecutionCommand} that failed.
*/
public static void dumpExecutionIntentToLog(int logLevel, String logTag, String label, String executable, List<String> arguments_list, String workingDirectory, boolean inBackground, HashMap<String, Object> additionalExtras) {
if (label == null) label = "Execution Intent";
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand) {
if(context == null || executionCommand == null) return;
StringBuilder executionIntentDump = new StringBuilder();
executionIntentDump.append(label).append(":\n");
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("`");
}
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");
return;
}
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
* nothing is returned. Otherwise following format is returned:
* Get {@link Notification.Builder} for {@link #NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS}
* and {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS}.
*
* ```
* Arg 0: `value`
* Arg 1: 'value`
* ```
*
* @param arguments_list The arguments list.
* @return Returns the formatted arguments list.
* @param context The {@link Context} for operations.
* @param title The title for the notification.
* @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 notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* @return Returns the {@link Notification.Builder}.
*/
public static String getArgumentsStringForLog(List<String> arguments_list) {
if (arguments_list==null || arguments_list.size() == 0) return "";
@Nullable
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");
for(int i = 0; i != arguments_list.size(); i++) {
arguments_list_string.append("Arg ").append(i).append(": `").append(arguments_list.get(i)).append("`\n");
}
arguments_list_string.append("```");
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS, Notification.PRIORITY_HIGH,
title, notifiationText, notificationBigText, pendingIntent, notificationMode);
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);
}
}

View File

@@ -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) {
StringBuilder regex_sb = new StringBuilder();

View 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>

View File

@@ -16,7 +16,8 @@
<string name="application_name">&TERMUX_APP_NAME;</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_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_failsafe">Failsafe</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_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_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_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_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_with">Share With</string>