Skip to content

Commit 69e4b57

Browse files
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.
1 parent 07e6ecd commit 69e4b57

12 files changed

+329
-27
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
3535
import com.termux.app.activities.HelpActivity;
3636
import com.termux.app.activities.SettingsActivity;
37+
import com.termux.app.crash.CrashUtils;
3738
import com.termux.app.settings.preferences.TermuxAppSharedPreferences;
3839
import com.termux.app.terminal.TermuxSessionsListViewController;
3940
import com.termux.app.terminal.io.TerminalToolbarViewPager;
@@ -155,6 +156,10 @@ public void onCreate(Bundle savedInstanceState) {
155156

156157
Logger.logDebug(LOG_TAG, "onCreate");
157158

159+
// Check if a crash happened on last run of the app and show a
160+
// notification with the crash details if it did
161+
CrashUtils.notifyCrash(this, LOG_TAG);
162+
158163
// Load termux shared preferences and properties
159164
mPreferences = new TermuxAppSharedPreferences(this);
160165
mProperties = new TermuxSharedProperties(this);

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import android.app.Application;
44

5+
import com.termux.app.crash.CrashHandler;
56
import com.termux.app.settings.preferences.TermuxAppSharedPreferences;
67
import com.termux.app.utils.Logger;
78

@@ -10,10 +11,14 @@ public class TermuxApplication extends Application {
1011
public void onCreate() {
1112
super.onCreate();
1213

13-
updateLogLevel();
14+
// Set crash handler for the app
15+
CrashHandler.setCrashHandler(this);
16+
17+
// Set log level for the app
18+
setLogLevel();
1419
}
1520

16-
private void updateLogLevel() {
21+
private void setLogLevel() {
1722
// Load the log level from shared preferences and set it to the {@link Loggger.CURRENT_LOG_LEVEL}
1823
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(getApplicationContext());
1924
preferences.setLogLevel(null, preferences.getLogLevel());

app/src/main/java/com/termux/app/activities/ReportActivity.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import com.termux.app.TermuxConstants;
1919
import com.termux.app.utils.MarkdownUtils;
2020
import com.termux.app.utils.ShareUtils;
21-
import com.termux.app.utils.TermuxUtils;
2221
import com.termux.app.models.ReportInfo;
2322

2423
import org.commonmark.node.FencedCodeBlock;
@@ -32,6 +31,7 @@ public class ReportActivity extends AppCompatActivity {
3231
private static final String EXTRA_REPORT_INFO = "report_info";
3332

3433
ReportInfo mReportInfo;
34+
String mReportMarkdownString;
3535
String mReportActivityMarkdownString;
3636

3737
@Override
@@ -131,11 +131,11 @@ public void onBackPressed() {
131131
public boolean onOptionsItemSelected(final MenuItem item) {
132132
int id = item.getItemId();
133133
if (id == R.id.menu_item_share_report) {
134-
if (mReportInfo != null)
135-
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportActivityMarkdownString);
134+
if (mReportMarkdownString != null)
135+
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
136136
} else if (id == R.id.menu_item_copy_report) {
137-
if (mReportInfo != null)
138-
ShareUtils.copyTextToClipboard(this, mReportActivityMarkdownString, null);
137+
if (mReportMarkdownString != null)
138+
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
139139
}
140140

141141
return false;
@@ -145,7 +145,16 @@ public boolean onOptionsItemSelected(final MenuItem item) {
145145
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
146146
*/
147147
private void generateReportActivityMarkdownString() {
148-
mReportActivityMarkdownString = ReportInfo.getReportInfoMarkdownString(this, mReportInfo);
148+
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
149+
150+
mReportActivityMarkdownString = "";
151+
if(mReportInfo.reportStringPrefix != null)
152+
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
153+
154+
mReportActivityMarkdownString += mReportMarkdownString;
155+
156+
if(mReportInfo.reportStringSuffix != null)
157+
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
149158
}
150159

151160

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.termux.app.crash;
2+
3+
import android.content.Context;
4+
5+
import androidx.annotation.NonNull;
6+
7+
/**
8+
* Catches uncaught exceptions and logs them.
9+
*/
10+
public class CrashHandler implements Thread.UncaughtExceptionHandler {
11+
12+
private final Context context;
13+
private final Thread.UncaughtExceptionHandler defaultUEH;
14+
15+
private CrashHandler(final Context context) {
16+
this.context = context;
17+
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
18+
}
19+
20+
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
21+
CrashUtils.logCrash(context,thread, throwable);
22+
defaultUEH.uncaughtException(thread, throwable);
23+
}
24+
25+
/**
26+
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
27+
*/
28+
public static void setCrashHandler(final Context context) {
29+
if(!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
30+
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
31+
}
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package com.termux.app.crash;
2+
3+
import android.app.Notification;
4+
import android.app.NotificationManager;
5+
import android.app.PendingIntent;
6+
import android.content.Context;
7+
import android.content.Intent;
8+
9+
import androidx.annotation.Nullable;
10+
11+
import com.termux.R;
12+
import com.termux.app.activities.ReportActivity;
13+
import com.termux.app.file.FileUtils;
14+
import com.termux.app.models.ReportInfo;
15+
import com.termux.app.models.UserAction;
16+
import com.termux.app.settings.preferences.TermuxAppSharedPreferences;
17+
import com.termux.app.settings.preferences.TermuxPreferenceConstants;
18+
import com.termux.app.utils.DataUtils;
19+
import com.termux.app.utils.Logger;
20+
import com.termux.app.utils.MarkdownUtils;
21+
import com.termux.app.utils.NotificationUtils;
22+
import com.termux.app.utils.TermuxUtils;
23+
24+
import com.termux.app.TermuxConstants;
25+
26+
import java.nio.charset.Charset;
27+
28+
public class CrashUtils {
29+
30+
private static final String NOTIFICATION_CHANNEL_ID_CRASH_REPORT_ERRORS = "termux_crash_reports_notification_channel";
31+
private static final String NOTIFICATION_CHANNEL_NAME_CRASH_REPORT_ERRORS = TermuxConstants.TERMUX_APP_NAME + " Crash Reports";
32+
33+
private static final String LOG_TAG = "CrashUtils";
34+
35+
/**
36+
* Log a crash in the crash log file at
37+
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
38+
*
39+
* @param context The {@link Context} for operations.
40+
* @param thread The {@link Thread} in which the crash happened.
41+
* @param thread The {@link Throwable} thrown for the crash.
42+
*/
43+
public static void logCrash(final Context context, final Thread thread, final Throwable throwable) {
44+
45+
StringBuilder reportString = new StringBuilder();
46+
47+
reportString.append("## Crash Details\n");
48+
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
49+
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", TermuxUtils.getCurrentTimeStamp(), "-"));
50+
51+
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTraceStringArray(throwable)));
52+
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
53+
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
54+
55+
// Log report string to logcat
56+
Logger.logError(reportString.toString());
57+
58+
// Write report string to crash log file
59+
String errmsg = FileUtils.writeStringToFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false);
60+
if(errmsg != null) {
61+
Logger.logError(LOG_TAG, errmsg);
62+
}
63+
}
64+
65+
/**
66+
* Notify the user of a previous app crash by reading the crash info from the crash log file at
67+
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
68+
*
69+
* If the crash log file exists and is not empty and
70+
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
71+
* enabled, then a notification will be shown for the crash on the
72+
* {@link #NOTIFICATION_CHANNEL_NAME_CRASH_REPORT_ERRORS} channel, otherwise nothing will be done.
73+
*
74+
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
75+
*
76+
* @param context The {@link Context} for operations.
77+
* @param logTagParam The log tag to use for logging.
78+
*/
79+
public static void notifyCrash(final Context context, final String logTagParam) {
80+
if(context == null) return;
81+
82+
83+
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
84+
// If user has disabled notifications for crashes
85+
if (!preferences.getCrashReportNotificationsEnabled())
86+
return;
87+
88+
new Thread() {
89+
@Override
90+
public void run() {
91+
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
92+
93+
if(!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
94+
return;
95+
96+
String errmsg;
97+
StringBuilder reportStringBuilder = new StringBuilder();
98+
99+
// Read report string from crash log file
100+
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
101+
if(errmsg != null) {
102+
Logger.logError(logTag, errmsg);
103+
return;
104+
}
105+
106+
// Move crash log file to backup location if it exists
107+
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
108+
if(errmsg != null) {
109+
Logger.logError(logTag, errmsg);
110+
}
111+
112+
String reportString = reportStringBuilder.toString();
113+
114+
if(reportString == null || reportString.isEmpty())
115+
return;
116+
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";
120+
121+
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
122+
123+
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, 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());
138+
}
139+
}.start();
140+
}
141+
142+
/**
143+
* Get {@link Notification.Builder} for {@link #NOTIFICATION_CHANNEL_ID_CRASH_REPORT_ERRORS}
144+
* and {@link #NOTIFICATION_CHANNEL_NAME_CRASH_REPORT_ERRORS}.
145+
*
146+
* @param context The {@link Context} for operations.
147+
* @param title The title for the notification.
148+
* @param notifiationText The second line text of the notification.
149+
* @param notificationBigText The full text of the notification that may optionally be styled.
150+
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
151+
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
152+
* @return Returns the {@link Notification.Builder}.
153+
*/
154+
@Nullable
155+
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
156+
157+
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
158+
NOTIFICATION_CHANNEL_ID_CRASH_REPORT_ERRORS, Notification.PRIORITY_HIGH,
159+
title, notifiationText, notificationBigText, pendingIntent, notificationMode);
160+
161+
if(builder == null) return null;
162+
163+
// Enable timestamp
164+
builder.setShowWhen(true);
165+
166+
// Set notification icon
167+
builder.setSmallIcon(R.drawable.ic_error_notification);
168+
169+
// Set background color for small notification icon
170+
builder.setColor(0xFF607D8B);
171+
172+
// Dismiss on click
173+
builder.setAutoCancel(true);
174+
175+
return builder;
176+
}
177+
178+
/**
179+
* Setup the notification channel for {@link #NOTIFICATION_CHANNEL_ID_CRASH_REPORT_ERRORS} and
180+
* {@link #NOTIFICATION_CHANNEL_NAME_CRASH_REPORT_ERRORS}.
181+
*
182+
* @param context The {@link Context} for operations.
183+
*/
184+
public static void setupCrashReportsNotificationChannel(final Context context) {
185+
NotificationUtils.setupNotificationChannel(context, NOTIFICATION_CHANNEL_ID_CRASH_REPORT_ERRORS,
186+
NOTIFICATION_CHANNEL_NAME_CRASH_REPORT_ERRORS, NotificationManager.IMPORTANCE_HIGH);
187+
}
188+
189+
}

app/src/main/java/com/termux/app/models/ExecutionCommand.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ public String geStackTracesLogString() {
415415
}
416416

417417
public String geStackTracesMarkdownString() {
418-
return Logger.getStackTracesMarkdownString("StackTraces:", Logger.getStackTraceStringArray(throwableList));
418+
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTraceStringArray(throwableList));
419419
}
420420

421421

0 commit comments

Comments
 (0)