2021-03-19 13:40:26 +00:00
package com.termux.app.utils ;
import android.app.Activity ;
2021-03-23 22:56:25 +00:00
import android.app.Notification ;
import android.app.NotificationManager ;
2021-03-19 13:40:26 +00:00
import android.app.PendingIntent ;
import android.content.Context ;
import android.content.Intent ;
import android.os.Bundle ;
2021-03-23 22:56:25 +00:00
import androidx.annotation.Nullable ;
2021-03-19 13:40:26 +00:00
import com.termux.R ;
2021-04-07 06:31:30 +00:00
import com.termux.shared.notification.NotificationUtils ;
import com.termux.shared.termux.TermuxConstants ;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE ;
2021-03-23 22:56:25 +00:00
import com.termux.app.activities.ReportActivity ;
2021-04-07 06:31:30 +00:00
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 ;
2021-03-27 14:08:26 +00:00
import com.termux.app.models.ReportInfo ;
import com.termux.app.models.ExecutionCommand ;
import com.termux.app.models.UserAction ;
2021-04-07 06:31:30 +00:00
import com.termux.shared.data.DataUtils ;
import com.termux.shared.markdown.MarkdownUtils ;
import com.termux.shared.termux.TermuxUtils ;
2021-03-19 13:40:26 +00:00
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 " ;
/ * *
2021-03-25 04:47:59 +00:00
* Process { @link ExecutionCommand } result .
*
2021-03-26 11:33:46 +00:00
* 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 .
2021-03-19 13:40:26 +00:00
*
2021-03-23 22:56:25 +00:00
* @param context The { @link Context } that will be used to send result intent to the { @link PendingIntent } creator .
2021-03-19 13:40:26 +00:00
* @param logTag The log tag to use for logging .
2021-03-25 04:47:59 +00:00
* @param executionCommand The { @link ExecutionCommand } to process .
2021-03-19 13:40:26 +00:00
* /
2021-03-25 04:47:59 +00:00
public static void processPluginExecutionCommandResult ( final Context context , String logTag , final ExecutionCommand executionCommand ) {
if ( executionCommand = = null ) return ;
2021-03-28 04:08:45 +00:00
logTag = DataUtils . getDefaultIfNull ( logTag , LOG_TAG ) ;
2021-03-26 11:33:46 +00:00
2021-03-25 04:47:59 +00:00
if ( ! executionCommand . hasExecuted ( ) ) {
2021-03-26 11:33:46 +00:00
Logger . logWarn ( logTag , " Ignoring call to processPluginExecutionCommandResult() since the execution command has not been ExecutionState.EXECUTED " ) ;
2021-03-25 04:47:59 +00:00
return ;
}
Logger . logDebug ( LOG_TAG , executionCommand . toString ( ) ) ;
2021-03-19 13:40:26 +00:00
2021-03-26 11:33:46 +00:00
boolean result = true ;
2021-03-19 13:40:26 +00:00
2021-03-26 11:33:46 +00:00
// 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 ;
2021-03-19 13:40:26 +00:00
2021-03-26 11:33:46 +00:00
//Combine errmsg and stacktraces
if ( executionCommand . isStateFailed ( ) ) {
errmsg = Logger . getMessageAndStackTracesString ( executionCommand . errmsg , executionCommand . throwableList ) ;
2021-03-19 13:40:26 +00:00
}
2021-03-26 11:33:46 +00:00
// Send pluginPendingIntent to its creator
result = sendPluginExecutionCommandResultPendingIntent ( context , logTag , executionCommand . getCommandIdAndLabelLogString ( ) , executionCommand . stdout , executionCommand . stderr , executionCommand . exitCode , executionCommand . errCode , errmsg , executionCommand . pluginPendingIntent ) ;
2021-03-19 13:40:26 +00:00
}
2021-03-26 11:33:46 +00:00
if ( ! executionCommand . isStateFailed ( ) & & result )
2021-03-25 04:47:59 +00:00
executionCommand . setState ( ExecutionCommand . ExecutionState . SUCCESS ) ;
2021-03-19 13:40:26 +00:00
}
/ * *
2021-03-26 11:33:46 +00:00
* Process { @link ExecutionCommand } error .
2021-03-25 04:47:59 +00:00
*
* The ExecutionCommand currentState must be equal to { @link ExecutionCommand . ExecutionState # FAILED } .
2021-03-26 11:33:46 +00:00
* The { @link ExecutionCommand # errCode } must have been set to a value greater than
* { @link ExecutionCommand # RESULT_CODE_OK } .
2021-03-23 22:56:25 +00:00
* The { @link ExecutionCommand # errmsg } and any { @link ExecutionCommand # throwableList } must also
* be set with appropriate error info .
2021-03-26 11:33:46 +00:00
*
* 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
2021-03-23 22:56:25 +00:00
* enabled , then a flash and a notification will be shown for the error as well
2021-04-07 06:31:30 +00:00
* on the { @link TermuxConstants # TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME } channel instead of just logging
2021-03-26 11:33:46 +00:00
* the error .
2021-03-19 13:40:26 +00:00
*
2021-03-23 22:56:25 +00:00
* @param context The { @link Context } for operations .
2021-03-19 13:40:26 +00:00
* @param logTag The log tag to use for logging .
2021-03-23 22:56:25 +00:00
* @param executionCommand The { @link ExecutionCommand } that failed .
2021-03-26 11:33:46 +00:00
* @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 } .
2021-03-19 13:40:26 +00:00
* /
2021-03-26 11:33:46 +00:00
public static void processPluginExecutionCommandError ( final Context context , String logTag , final ExecutionCommand executionCommand , boolean forceNotification ) {
2021-03-23 22:56:25 +00:00
if ( context = = null | | executionCommand = = null ) return ;
2021-03-19 13:40:26 +00:00
2021-03-28 04:08:45 +00:00
logTag = DataUtils . getDefaultIfNull ( logTag , LOG_TAG ) ;
2021-03-26 11:33:46 +00:00
2021-03-25 04:47:59 +00:00
if ( ! executionCommand . isStateFailed ( ) ) {
2021-03-26 11:33:46 +00:00
Logger . logWarn ( logTag , " Ignoring call to processPluginExecutionCommandError() since the execution command does not have ExecutionState.FAILED state " ) ;
2021-03-23 22:56:25 +00:00
return ;
}
2021-03-19 13:40:26 +00:00
2021-03-23 22:56:25 +00:00
// Log the error and any exception
2021-03-25 04:47:59 +00:00
Logger . logStackTracesWithMessage ( logTag , " ( " + executionCommand . errCode + " ) " + executionCommand . errmsg , executionCommand . throwableList ) ;
2021-03-19 13:40:26 +00:00
2021-03-26 11:33:46 +00:00
// 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 ;
}
2021-03-23 22:56:25 +00:00
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences ( context ) ;
// If user has disabled notifications for plugin, then just return
2021-03-26 11:33:46 +00:00
if ( ! preferences . getPluginErrorNotificationsEnabled ( ) & & ! forceNotification )
2021-03-23 22:56:25 +00:00
return ;
// Flash the errmsg
Logger . showToast ( context , executionCommand . errmsg , true ) ;
2021-03-19 13:40:26 +00:00
2021-03-23 22:56:25 +00:00
// 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 " ;
Implement crash handler and reporting
Now whenever the Termux app crashes, the crash report (stacktrace, app and device info) will be logged to ~/crash_log.md file. When the user will reopen the app, a notification will be shown which when clicked will show the crash report content in the ReportActivity. The activity will have important links like email, reddit, github issues of termux app and packages at which the user can optionally report an issue if necessary after copying the crash report text. The ~/crash_log.md file will be moved to ~/crash_log-backup.md so that a notification is not shown again on next startup and can be viewed again via SAF, etc.
This will allow reports for bugs that are submitted to have complete and useful info, specially in markdown format, making lives of devs a tad bit easier. Also more bugs that are rare might be submitted since users will have the info to report with and know where to report at.
ToDo:
- The TermuxConstants.TERMUX_SUPPORT_EMAIL_URL needs to be updated with a valid support email once its set up. The TermuxUtils.getReportIssueMarkdownString() function currently also has "email" lines commented out which will need to be uncommented.
- Currently, crashes will only be handled for the main app thread, other threads will have to manually hooked into where necessary.
2021-04-06 11:15:00 +00:00
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 ) ) ;
2021-03-23 22:56:25 +00:00
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
2021-04-07 06:31:30 +00:00
CharSequence notificationText = MarkdownUtils . getSpannedMarkdownText ( context , executionCommand . errmsg ) ;
//CharSequence notificationText = executionCommand.errmsg;
2021-03-23 22:56:25 +00:00
// Build the notification
2021-04-07 06:31:30 +00:00
Notification . Builder builder = getPluginCommandErrorsNotificationBuilder ( context , title , notificationText , notificationText , pendingIntent , NotificationUtils . NOTIFICATION_MODE_VIBRATE ) ;
2021-03-23 22:56:25 +00:00
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 ( ) ) ;
2021-03-26 11:33:46 +00:00
}
/ * *
* 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 ;
2021-03-28 04:08:45 +00:00
logTag = DataUtils . getDefaultIfNull ( logTag , LOG_TAG ) ;
2021-03-26 11:33:46 +00:00
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 ( ) ) {
2021-03-28 04:08:45 +00:00
truncatedStdout = DataUtils . getTruncatedCommandOutput ( stdout , DataUtils . TRANSACTION_SIZE_LIMIT_IN_BYTES , false , false , false ) ;
2021-03-26 11:33:46 +00:00
} else if ( stdout = = null | | stdout . isEmpty ( ) ) {
2021-03-28 04:08:45 +00:00
truncatedStderr = DataUtils . getTruncatedCommandOutput ( stderr , DataUtils . TRANSACTION_SIZE_LIMIT_IN_BYTES , false , false , false ) ;
2021-03-26 11:33:46 +00:00
} else {
2021-03-28 04:08:45 +00:00
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 ) ;
2021-03-26 11:33:46 +00:00
}
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
2021-04-07 06:31:30 +00:00
String truncatedErrmsg = DataUtils . getTruncatedCommandOutput ( errmsg , DataUtils . TRANSACTION_SIZE_LIMIT_IN_BYTES / 4 , true , false , false ) ;
2021-03-26 11:33:46 +00:00
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 ;
2021-03-19 13:40:26 +00:00
}
2021-03-25 04:47:59 +00:00
2021-03-19 13:40:26 +00:00
/ * *
2021-04-07 06:31:30 +00:00
* Get { @link Notification . Builder } for { @link TermuxConstants # TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID }
* and { @link TermuxConstants # TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME } .
2021-03-19 13:40:26 +00:00
*
2021-03-23 22:56:25 +00:00
* @param context The { @link Context } for operations .
* @param title The title for the notification .
2021-04-07 06:31:30 +00:00
* @param notificationText The second line text of the notification .
2021-03-23 22:56:25 +00:00
* @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 } .
2021-03-19 13:40:26 +00:00
* /
2021-03-23 22:56:25 +00:00
@Nullable
2021-04-07 06:31:30 +00:00
public static Notification . Builder getPluginCommandErrorsNotificationBuilder ( final Context context , final CharSequence title , final CharSequence notificationText , final CharSequence notificationBigText , final PendingIntent pendingIntent , final int notificationMode ) {
2021-03-19 13:40:26 +00:00
2021-03-23 22:56:25 +00:00
Notification . Builder builder = NotificationUtils . geNotificationBuilder ( context ,
2021-04-07 06:31:30 +00:00
TermuxConstants . TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID , Notification . PRIORITY_HIGH ,
title , notificationText , notificationBigText , pendingIntent , notificationMode ) ;
2021-03-23 22:56:25 +00:00
if ( builder = = null ) return null ;
// Enable timestamp
builder . setShowWhen ( true ) ;
2021-03-19 13:40:26 +00:00
2021-03-23 22:56:25 +00:00
// 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 ;
2021-03-19 13:40:26 +00:00
}
2021-03-23 22:56:25 +00:00
/ * *
2021-04-07 06:31:30 +00:00
* Setup the notification channel for { @link TermuxConstants # TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID } and
* { @link TermuxConstants # TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME } .
2021-03-23 22:56:25 +00:00
*
* @param context The { @link Context } for operations .
* /
public static void setupPluginCommandErrorsNotificationChannel ( final Context context ) {
2021-04-07 06:31:30 +00:00
NotificationUtils . setupNotificationChannel ( context , TermuxConstants . TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID ,
TermuxConstants . TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME , NotificationManager . IMPORTANCE_HIGH ) ;
2021-03-23 22:56:25 +00:00
}
2021-03-25 04:47:59 +00:00
/ * *
* 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 ;
}
2021-03-19 13:40:26 +00:00
}