Skip to content

Commit e408fdc

Browse files
Show crash notification when bootstrap installation or setup storage failures
Sometimes users report that bootstrap installation failed on their devices but provide no details. Since they don't check logcat for the exception or exception is one time only, we can't know what happened. Although, reasons are likely root ownership files. The notification will show the full stacktrace including suppressed ones for why failure occurred and hopefully be easier to find the problems and we can get reports too.
1 parent 53c1a49 commit e408fdc

File tree

3 files changed

+107
-59
lines changed

3 files changed

+107
-59
lines changed

app/src/main/java/com/termux/app/TermuxActivity.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public void onCreate(Bundle savedInstanceState) {
178178

179179
// Check if a crash happened on last run of the app and show a
180180
// notification with the crash details if it did
181-
CrashUtils.notifyCrash(this, LOG_TAG);
181+
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
182182

183183
// Load termux shared properties
184184
mProperties = new TermuxAppSharedProperties(this);

app/src/main/java/com/termux/app/TermuxInstaller.java

+56-36
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.view.WindowManager;
1212

1313
import com.termux.R;
14+
import com.termux.app.utils.CrashUtils;
1415
import com.termux.shared.file.FileUtils;
1516
import com.termux.shared.interact.DialogUtils;
1617
import com.termux.shared.logger.Logger;
@@ -70,14 +71,14 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD
7071

7172
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
7273
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
73-
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
74+
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
7475
// If prefix directory is empty or only contains the tmp directory
75-
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()))) {
76-
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
77-
} else {
78-
whenDone.run();
79-
return;
80-
}
76+
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()))) {
77+
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
78+
} else {
79+
whenDone.run();
80+
return;
81+
}
8182
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
8283
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
8384
}
@@ -97,13 +98,15 @@ public void run() {
9798
// Delete prefix staging directory or any file at its destination
9899
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
99100
if (error != null) {
100-
throw new RuntimeException(error.toString());
101+
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
102+
return;
101103
}
102104

103105
// Delete prefix directory or any file at its destination
104106
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
105107
if (error != null) {
106-
throw new RuntimeException(error.toString());
108+
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
109+
return;
107110
}
108111

109112
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
@@ -126,14 +129,22 @@ public void run() {
126129
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
127130
symlinks.add(Pair.create(oldPath, newPath));
128131

129-
ensureDirectoryExists(new File(newPath).getParentFile());
132+
error = ensureDirectoryExists(new File(newPath).getParentFile());
133+
if (error != null) {
134+
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
135+
return;
136+
}
130137
}
131138
} else {
132139
String zipEntryName = zipEntry.getName();
133140
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
134141
boolean isDirectory = zipEntry.isDirectory();
135142

136-
ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
143+
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
144+
if (error != null) {
145+
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
146+
return;
147+
}
137148

138149
if (!isDirectory) {
139150
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
@@ -164,23 +175,10 @@ public void run() {
164175

165176
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
166177
activity.runOnUiThread(whenDone);
178+
167179
} catch (final Exception e) {
168-
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
169-
activity.runOnUiThread(() -> {
170-
try {
171-
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
172-
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
173-
dialog.dismiss();
174-
activity.finish();
175-
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
176-
dialog.dismiss();
177-
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
178-
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
179-
}).show();
180-
} catch (WindowManager.BadTokenException e1) {
181-
// Activity already dismissed - ignore.
182-
}
183-
});
180+
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
181+
184182
} finally {
185183
activity.runOnUiThread(() -> {
186184
try {
@@ -194,6 +192,30 @@ public void run() {
194192
}.start();
195193
}
196194

195+
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
196+
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
197+
198+
// Send a notification with the exception so that the user knows why bootstrap setup failed
199+
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true);
200+
201+
activity.runOnUiThread(() -> {
202+
try {
203+
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
204+
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
205+
dialog.dismiss();
206+
activity.finish();
207+
})
208+
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
209+
dialog.dismiss();
210+
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
211+
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
212+
}).show();
213+
} catch (WindowManager.BadTokenException e1) {
214+
// Activity already dismissed - ignore.
215+
}
216+
});
217+
}
218+
197219
static void setupStorageSymlinks(final Context context) {
198220
final String LOG_TAG = "termux-storage";
199221

@@ -208,7 +230,8 @@ public void run() {
208230
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
209231
if (error != null) {
210232
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
211-
Logger.logErrorExtended(LOG_TAG, error.toString());
233+
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
234+
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true);
212235
return;
213236
}
214237

@@ -245,19 +268,16 @@ public void run() {
245268

246269
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
247270
} catch (Exception e) {
248-
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
271+
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
272+
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
273+
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true);
249274
}
250275
}
251276
}.start();
252277
}
253278

254-
private static void ensureDirectoryExists(File directory) {
255-
Error error;
256-
257-
error = FileUtils.createDirectoryFile(directory.getAbsolutePath());
258-
if (error != null) {
259-
throw new RuntimeException(error.toString());
260-
}
279+
private static Error ensureDirectoryExists(File directory) {
280+
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
261281
}
262282

263283
public static byte[] loadZipBytes() {

app/src/main/java/com/termux/app/utils/CrashUtils.java

+50-22
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public class CrashUtils {
3030
private static final String LOG_TAG = "CrashUtils";
3131

3232
/**
33-
* Notify the user of a previous app crash by reading the crash info from the crash log file at
34-
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
33+
* Notify the user of an app crash at last run by reading the crash info from the crash log file
34+
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
3535
* created by {@link com.termux.shared.crash.CrashHandler}.
3636
*
3737
* If the crash log file exists and is not empty and
@@ -44,10 +44,9 @@ public class CrashUtils {
4444
* @param context The {@link Context} for operations.
4545
* @param logTagParam The log tag to use for logging.
4646
*/
47-
public static void notifyCrash(final Context context, final String logTagParam) {
47+
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
4848
if (context == null) return;
4949

50-
5150
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
5251
if (preferences == null) return;
5352

@@ -84,29 +83,58 @@ public void run() {
8483
if (reportString.isEmpty())
8584
return;
8685

87-
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
88-
// to show the details of the crash
89-
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
86+
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
87+
88+
sendCrashReportNotification(context, logTag, reportString, false);
89+
}
90+
}.start();
91+
}
92+
93+
/**
94+
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
95+
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
96+
*
97+
* @param context The {@link Context} for operations.
98+
* @param logTag The log tag to use for logging.
99+
* @param reportString The text for the crash report.
100+
* @param forceNotification If set to {@code true}, then a notification will be shown
101+
* regardless of if pending intent is {@code null} or
102+
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
103+
* is {@code false}.
104+
*/
105+
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) {
106+
if (context == null) return;
107+
108+
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
109+
if (preferences == null) return;
90110

91-
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
111+
// If user has disabled notifications for crashes
112+
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
113+
return;
92114

93-
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
94-
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
115+
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
95116

96-
// Setup the notification channel if not already set up
97-
setupCrashReportsNotificationChannel(context);
117+
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
118+
// to show the details of the crash
119+
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
98120

99-
// Build the notification
100-
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
101-
if (builder == null) return;
121+
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
102122

103-
// Send the notification
104-
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
105-
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
106-
if (notificationManager != null)
107-
notificationManager.notify(nextNotificationId, builder.build());
108-
}
109-
}.start();
123+
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
124+
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
125+
126+
// Setup the notification channel if not already set up
127+
setupCrashReportsNotificationChannel(context);
128+
129+
// Build the notification
130+
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
131+
if (builder == null) return;
132+
133+
// Send the notification
134+
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
135+
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
136+
if (notificationManager != null)
137+
notificationManager.notify(nextNotificationId, builder.build());
110138
}
111139

112140
/**

0 commit comments

Comments
 (0)