From 31371b5e3df946fe1b5524e24aadd826b853d6b4 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 03:56:25 +0500 Subject: [PATCH] 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 --- .../com/termux/app/RunCommandService.java | 116 ++++++---- .../com/termux/app/utils/PluginUtils.java | 212 ++++++++++-------- .../com/termux/app/utils/TextDataUtils.java | 13 ++ .../res/drawable/ic_error_notification.xml | 37 +++ app/src/main/res/values/strings.xml | 10 +- 5 files changed, 251 insertions(+), 137 deletions(-) create mode 100644 app/src/main/res/drawable/ic_error_notification.xml diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index a5236e7c..f8abb0a9 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -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() {{ - 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); } } diff --git a/app/src/main/java/com/termux/app/utils/PluginUtils.java b/app/src/main/java/com/termux/app/utils/PluginUtils.java index e59af7f8..0fd2a7e9 100644 --- a/app/src/main/java/com/termux/app/utils/PluginUtils.java +++ b/app/src/main/java/com/termux/app/utils/PluginUtils.java @@ -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,}(? arguments_list, String workingDirectory, boolean inBackground, HashMap 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 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 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); + } + } diff --git a/app/src/main/java/com/termux/app/utils/TextDataUtils.java b/app/src/main/java/com/termux/app/utils/TextDataUtils.java index aebacdae..fdde1b07 100644 --- a/app/src/main/java/com/termux/app/utils/TextDataUtils.java +++ b/app/src/main/java/com/termux/app/utils/TextDataUtils.java @@ -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 getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) { + return (object == null) ? def : object; + } + + + public static LinkedHashSet extractUrls(String text) { StringBuilder regex_sb = new StringBuilder(); diff --git a/app/src/main/res/drawable/ic_error_notification.xml b/app/src/main/res/drawable/ic_error_notification.xml new file mode 100644 index 00000000..67f17712 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_notification.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21947fa5..4226e329 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,8 @@ &TERMUX_APP_NAME; &TERMUX_APP_NAME; user Run commands in &TERMUX_APP_NAME; environment - execute arbitrary commands within &TERMUX_APP_NAME; environment + execute arbitrary commands within &TERMUX_APP_NAME; + environment New session Failsafe Keyboard @@ -85,9 +86,10 @@ Validating file existence and permissions fafiled: \"%1$s\"\nException: %2$s Validating directory existence and permissions fafiled: \"%1$s\"\nException: %2$s - Invalid intent action to RunCommandService: \"%1$s\" - Invalid coommand path to RunCommandService: \"%1$s\" - RunCommandService require allow-external-apps property to be set to \"true\" in &TERMUX_PROPERTIES_PRIMARY_PATH_SHORT; file. + Invalid intent action to RunCommandService: `%1$s` + Invalid coommand path to RunCommandService: `%1$s` + RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file. + 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. Share Share With