diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index a3885fe5b8..bd54677e8a 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -178,7 +178,7 @@ public void onCreate(Bundle savedInstanceState) { // Check if a crash happened on last run of the app and show a // notification with the crash details if it did - CrashUtils.notifyCrash(this, LOG_TAG); + CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG); // Load termux shared properties mProperties = new TermuxAppSharedProperties(this); diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 6b4782ebf2..83ce3ab9df 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -11,6 +11,7 @@ import android.view.WindowManager; import com.termux.R; +import com.termux.app.utils.CrashUtils; import com.termux.shared.file.FileUtils; import com.termux.shared.interact.DialogUtils; import com.termux.shared.logger.Logger; @@ -70,14 +71,14 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD // If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) { - File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles(); + File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles(); // If prefix directory is empty or only contains the tmp directory - if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) { - Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory."); - } else { - whenDone.run(); - return; - } + if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) { + Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory."); + } else { + whenDone.run(); + return; + } } else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) { Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination."); } @@ -97,13 +98,15 @@ public void run() { // Delete prefix staging directory or any file at its destination error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true); if (error != null) { - throw new RuntimeException(error.toString()); + showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); + return; } // Delete prefix directory or any file at its destination error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); if (error != null) { - throw new RuntimeException(error.toString()); + showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); + return; } Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\"."); @@ -126,14 +129,22 @@ public void run() { String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; symlinks.add(Pair.create(oldPath, newPath)); - ensureDirectoryExists(new File(newPath).getParentFile()); + error = ensureDirectoryExists(new File(newPath).getParentFile()); + if (error != null) { + showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); + return; + } } } else { String zipEntryName = zipEntry.getName(); File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); boolean isDirectory = zipEntry.isDirectory(); - ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile()); + error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile()); + if (error != null) { + showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); + return; + } if (!isDirectory) { try (FileOutputStream outStream = new FileOutputStream(targetFile)) { @@ -164,23 +175,10 @@ public void run() { Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully."); activity.runOnUiThread(whenDone); + } catch (final Exception e) { - Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e); - activity.runOnUiThread(() -> { - try { - new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) - .setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> { - dialog.dismiss(); - activity.finish(); - }).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { - dialog.dismiss(); - FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); - TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone); - }).show(); - } catch (WindowManager.BadTokenException e1) { - // Activity already dismissed - ignore. - } - }); + showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e))); + } finally { activity.runOnUiThread(() -> { try { @@ -194,6 +192,30 @@ public void run() { }.start(); } + public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) { + Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message); + + // Send a notification with the exception so that the user knows why bootstrap setup failed + CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true); + + activity.runOnUiThread(() -> { + try { + new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) + .setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> { + dialog.dismiss(); + activity.finish(); + }) + .setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { + dialog.dismiss(); + FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); + TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone); + }).show(); + } catch (WindowManager.BadTokenException e1) { + // Activity already dismissed - ignore. + } + }); + } + static void setupStorageSymlinks(final Context context) { final String LOG_TAG = "termux-storage"; @@ -208,7 +230,8 @@ public void run() { error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath()); if (error != null) { Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage()); - Logger.logErrorExtended(LOG_TAG, error.toString()); + Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString()); + CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true); return; } @@ -245,19 +268,16 @@ public void run() { Logger.logInfo(LOG_TAG, "Storage symlinks created successfully."); } catch (Exception e) { - Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e); + Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage()); + Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e); + CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true); } } }.start(); } - private static void ensureDirectoryExists(File directory) { - Error error; - - error = FileUtils.createDirectoryFile(directory.getAbsolutePath()); - if (error != null) { - throw new RuntimeException(error.toString()); - } + private static Error ensureDirectoryExists(File directory) { + return FileUtils.createDirectoryFile(directory.getAbsolutePath()); } public static byte[] loadZipBytes() { diff --git a/app/src/main/java/com/termux/app/utils/CrashUtils.java b/app/src/main/java/com/termux/app/utils/CrashUtils.java index 63cd03562d..3a59d9c19f 100644 --- a/app/src/main/java/com/termux/app/utils/CrashUtils.java +++ b/app/src/main/java/com/termux/app/utils/CrashUtils.java @@ -30,8 +30,8 @@ public class CrashUtils { private static final String LOG_TAG = "CrashUtils"; /** - * Notify the user of a previous app crash by reading the crash info from the crash log file at - * {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been + * Notify the user of an app crash at last run by reading the crash info from the crash log file + * at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been * created by {@link com.termux.shared.crash.CrashHandler}. * * If the crash log file exists and is not empty and @@ -44,10 +44,9 @@ public class CrashUtils { * @param context The {@link Context} for operations. * @param logTagParam The log tag to use for logging. */ - public static void notifyCrash(final Context context, final String logTagParam) { + public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) { if (context == null) return; - TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context); if (preferences == null) return; @@ -84,29 +83,58 @@ public void run() { if (reportString.isEmpty()) return; - // Send a notification to show the crash log which when clicked will open the {@link ReportActivity} - // to show the details of the crash - String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report"; + Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\"."); + + sendCrashReportNotification(context, logTag, reportString, false); + } + }.start(); + } + + /** + * Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} + * and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}. + * + * @param context The {@link Context} for operations. + * @param logTag The log tag to use for logging. + * @param reportString The text for the crash report. + * @param forceNotification If set to {@code true}, then a notification will be shown + * regardless of if pending intent is {@code null} or + * {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} + * is {@code false}. + */ + public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) { + if (context == null) return; + + TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context); + if (preferences == null) return; - Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification."); + // If user has disabled notifications for crashes + if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification) + return; - Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true)); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG); - // Setup the notification channel if not already set up - setupCrashReportsNotificationChannel(context); + // Send a notification to show the crash log which when clicked will open the {@link ReportActivity} + // to show the details of the crash + String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report"; - // Build the notification - Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE); - if (builder == null) return; + Logger.logDebug(logTag, "Sending \"" + title + "\" notification."); - // Send the notification - int nextNotificationId = NotificationUtils.getNextNotificationId(context); - NotificationManager notificationManager = NotificationUtils.getNotificationManager(context); - if (notificationManager != null) - notificationManager.notify(nextNotificationId, builder.build()); - } - }.start(); + Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true)); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + // Setup the notification channel if not already set up + setupCrashReportsNotificationChannel(context); + + // Build the notification + Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, 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()); } /**