mirror of https://github.com/termux/termux-app
330 lines
17 KiB
Java
330 lines
17 KiB
Java
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.shared.notification.NotificationUtils;
|
|
import com.termux.shared.termux.TermuxConstants;
|
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
|
import com.termux.app.activities.ReportActivity;
|
|
import com.termux.shared.logger.Logger;
|
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
|
import com.termux.shared.settings.properties.SharedProperties;
|
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
|
import com.termux.app.models.ReportInfo;
|
|
import com.termux.app.models.ExecutionCommand;
|
|
import com.termux.app.models.UserAction;
|
|
import com.termux.shared.data.DataUtils;
|
|
import com.termux.shared.markdown.MarkdownUtils;
|
|
import com.termux.shared.termux.TermuxUtils;
|
|
|
|
public class PluginUtils {
|
|
|
|
/** 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"
|
|
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
|
* 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"
|
|
|
|
private static final String LOG_TAG = "PluginUtils";
|
|
|
|
/**
|
|
* Process {@link ExecutionCommand} result.
|
|
*
|
|
* The ExecutionCommand currentState must be greater or equal to
|
|
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
|
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
|
* are sent back to the {@link PendingIntent} creator.
|
|
*
|
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
|
* @param logTag The log tag to use for logging.
|
|
* @param executionCommand The {@link ExecutionCommand} to process.
|
|
*/
|
|
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
|
if (executionCommand == null) return;
|
|
|
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
|
|
if(!executionCommand.hasExecuted()) {
|
|
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command has not been ExecutionState.EXECUTED");
|
|
return;
|
|
}
|
|
|
|
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
|
|
|
boolean result = true;
|
|
|
|
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
|
// send pluginPendingIntent to its creator with the result
|
|
if(executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
|
String errmsg = executionCommand.errmsg;
|
|
|
|
//Combine errmsg and stacktraces
|
|
if (executionCommand.isStateFailed()) {
|
|
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
|
}
|
|
|
|
// Send pluginPendingIntent to its creator
|
|
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
|
}
|
|
|
|
if(!executionCommand.isStateFailed() && result)
|
|
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
|
}
|
|
|
|
/**
|
|
* Process {@link ExecutionCommand} error.
|
|
*
|
|
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
|
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
|
|
* {@link ExecutionCommand#RESULT_CODE_OK}.
|
|
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
|
* be set with appropriate error info.
|
|
*
|
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
|
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
|
|
* are sent back to the {@link PendingIntent} creator.
|
|
*
|
|
* Otherwise if the {@link 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 TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
|
* the error.
|
|
*
|
|
* @param context The {@link Context} for operations.
|
|
* @param logTag The log tag to use for logging.
|
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
|
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
|
* regardless of if pending intent is {@code null} or
|
|
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
|
* is {@code false}.
|
|
*/
|
|
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
|
if(context == null || executionCommand == null) return;
|
|
|
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
|
|
if(!executionCommand.isStateFailed()) {
|
|
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command does not have ExecutionState.FAILED state");
|
|
return;
|
|
}
|
|
|
|
// Log the error and any exception
|
|
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
|
|
|
|
|
|
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
|
// send pluginPendingIntent to its creator with the errors
|
|
if(executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
|
String errmsg = executionCommand.errmsg;
|
|
|
|
//Combine errmsg and stacktraces
|
|
if (executionCommand.isStateFailed()) {
|
|
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
|
}
|
|
|
|
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
|
|
|
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
|
if (!forceNotification) return;
|
|
}
|
|
|
|
|
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
|
// If user has disabled notifications for plugin, then just return
|
|
if (!preferences.getPluginErrorNotificationsEnabled() && !forceNotification)
|
|
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";
|
|
|
|
StringBuilder reportString = new StringBuilder();
|
|
|
|
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
|
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
|
|
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,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 notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
|
//CharSequence notificationText = executionCommand.errmsg;
|
|
|
|
// Build the notification
|
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, 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());
|
|
|
|
}
|
|
|
|
/**
|
|
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
|
|
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
|
|
*
|
|
*
|
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
|
* @param logTag The log tag to use for logging.
|
|
* @param label The label of {@link ExecutionCommand}.
|
|
* @param stdout The stdout of {@link ExecutionCommand}.
|
|
* @param stderr The stderr of {@link ExecutionCommand}.
|
|
* @param exitCode The exitCode of {@link ExecutionCommand}.
|
|
* @param errCode The errCode of {@link ExecutionCommand}.
|
|
* @param errmsg The errmsg of {@link ExecutionCommand}.
|
|
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
|
|
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
|
|
*/
|
|
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
|
|
if(context == null || pluginPendingIntent == null) return false;
|
|
|
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
|
|
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
|
|
|
|
String truncatedStdout = null;
|
|
String truncatedStderr = null;
|
|
|
|
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
|
|
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
|
|
|
|
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
|
if(stderr == null || stderr.isEmpty()) {
|
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
|
} else if (stdout == null || stdout.isEmpty()) {
|
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
|
} else {
|
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
|
}
|
|
|
|
if(truncatedStdout != null && truncatedStdout.length() < stdout.length()){
|
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
|
stdout = truncatedStdout;
|
|
}
|
|
|
|
if(truncatedStderr != null && truncatedStderr.length() < stderr.length()){
|
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
|
stderr = truncatedStderr;
|
|
}
|
|
|
|
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
|
|
|
|
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
|
// trim from end to preserve start of stacktraces
|
|
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
|
if(truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()){
|
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
|
errmsg = truncatedErrmsg;
|
|
}
|
|
|
|
|
|
final Bundle resultBundle = new Bundle();
|
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
|
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
|
|
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
|
|
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
|
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
|
|
|
Intent resultIntent = new Intent();
|
|
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
|
|
|
try {
|
|
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
|
} catch (PendingIntent.CanceledException e) {
|
|
// The caller doesn't want the result? That's fine, just ignore
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
|
*
|
|
* @param context The {@link Context} for operations.
|
|
* @param title The title for the notification.
|
|
* @param notificationText 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}.
|
|
*/
|
|
@Nullable
|
|
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
|
|
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
|
|
|
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 TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
|
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
|
*
|
|
* @param context The {@link Context} for operations.
|
|
*/
|
|
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
|
*
|
|
* @param context The {@link Context} to get error string.
|
|
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
|
*/
|
|
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
|
String errmsg = null;
|
|
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
|
|
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
|
}
|
|
|
|
return errmsg;
|
|
}
|
|
|
|
}
|