To handle uncaught exceptions globally in an Android app, you can set up a custom UncaughtExceptionHandler that collects crash data and persists it before the app terminates.
Registering the Handler
Create a custom Application subclass and override onCreate(). Assign your custom handler as the default uncaught expection handler for the main thread.
import android.app.Application;
public class CrashShield extends Application {
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(
CrashCollector.getInstance(getApplicationContext()));
}
}
Declare this class in the manifest under the <application> element:
<application
android:name=".CrashShield"
... >
...
</application>
Building the Crash Collector
Implement Thread.UncaughtExceptionHandler as a thread-safe singleton that stores the origianl system handler and collects relevant information when a crash occurs.
import android.content.Context;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.Process;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
public class CrashCollector implements Thread.UncaughtExceptionHandler {
private static CrashCollector instance;
private final Context appContext;
private final Thread.UncaughtExceptionHandler systemHandler;
private CrashCollector(Context ctx) {
appContext = ctx.getApplicationContext();
systemHandler = Thread.getDefaultUncaughtExceptionHandler();
}
public static synchronized CrashCollector getInstance(Context ctx) {
if (instance == null) {
instance = new CrashCollector(ctx);
}
return instance;
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
StringWriter stackTraceWriter = new StringWriter();
ex.printStackTrace(new PrintWriter(stackTraceWriter));
StringBuilder logEntry = new StringBuilder();
try {
PackageInfo packageInfo = appContext.getPackageManager()
.getPackageInfo(appContext.getPackageName(), 0);
logEntry.append("App version: ").append(packageInfo.versionName).append("\n");
} catch (Exception ignored) {}
logEntry.append("Thread: ").append(thread.getName()).append("\n");
logEntry.append("Stack trace:\n").append(stackTraceWriter.toString()).append("\n");
// Append device metadata
logEntry.append("Device info:\n");
logEntry.append("Manufacturer: ").append(Build.MANUFACTURER).append("\n");
logEntry.append("Model: ").append(Build.MODEL).append("\n");
logEntry.append("OS version: ").append(Build.VERSION.RELEASE).append("\n");
logEntry.append("SDK level: ").append(Build.VERSION.SDK_INT).append("\n");
logEntry.append("Timestamp: ").append(System.currentTimeMillis()).append("\n");
writeToFile(logEntry.toString());
android.os.Process.killProcess(Process.myPid());
if (systemHandler != null) {
systemHandler.uncaughtException(thread, ex);
}
}
private void writeToFile(String content) {
File logDir = appContext.getExternalFilesDir(null);
if (logDir == null) return;
if (!logDir.exists()) logDir.mkdirs();
File crashLog = new File(logDir, "crash_report.log");
try (FileOutputStream fos = new FileOutputStream(crashLog, true)) {
fos.write(content.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}
When an exception reaches the handler, it writes the app version, thread name, full stack trace, device properties, and a timestamp to a log file in the app's private external storage directory. Afterwards, the process is killed and the original system handler is invoked.