Why Capture Crash Screenshots and How to Implement Them on Android
Capturing a screenshot right before an Android app crashes provides visual context that speeds reproduction, clarifies UI/UX conditions, saves debugging time, and bridges communication between developers, QA, and support, and this guide shows how to implement it using UncaughtExceptionHandler, ActivityLifecycleCallbacks, PixelCopy, and Kotlin code.
Why Capture Crash Screenshots?
Have you considered that a screenshot taken just before an app crashes can provide critical visual information that a crash log alone cannot?
Faster issue reproduction : No need to guess the app state from logs; you see the exact screen that caused the crash.
Understanding UI/UX context : Determine whether a dialog, navigation error, or an edge case in the UI triggered the crash.
Saving debugging time : Developers often spend hours recreating conditions; a visual snapshot speeds this up.
Bridging development and QA : Testers and support can attach screenshots, allowing developers to grasp the situation instantly.
Catching user‑specific edge cases : Some crashes stem from particular data, configurations, or user flows that are invisible in logs; screenshots reveal these clues.
In short, combining crash reports with screenshots is like adding surveillance video to a police report – the text tells what happened, the image shows exactly how it happened.
How to Implement It
We can achieve this by leveraging Android native APIs.
UncaughtExceptionHandler : Listener invoked before the app crashes.
PixelCopy : Modern API for programmatic screenshot capture.
ActivityLifecycleCallback : Listener tracking Activity lifecycle to keep a weak reference to the current Activity.
Step 1: Set UncaughtExceptionHandler and ActivityLifecycleCallbacks
In your Application class, add the following listeners:
class Application : Application(), Application.ActivityLifecycleCallbacks {
// Custom class will be created
private lateinit var crashHandler: CrashHandler
override fun onCreate() {
super.onCreate()
initializeCrashHandler()
registerActivityLifecycleCallbacks(this)
}
private fun initializeCrashHandler() {
crashHandler = CrashHandler(application = this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {
Log.d("BACKGROUND", "onActivityResumed: ${activity::class.java.name}")
crashHandler.setCurrentActivity(activity)
}
override fun onActivityPaused(activity: Activity) {
Log.d("BACKGROUND", "onActivityPaused: ${activity::class.java.name}")
crashHandler.setCurrentActivity(null)
}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}Step 2: Create a Custom CrashHandler Class
Create a class that implements Thread.UncaughtExceptionHandler:
class CrashHandler(
private val application: Application,
) : Thread.UncaughtExceptionHandler {
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
private var currentActivity: WeakReference<Activity>? = null
companion object {
private const val TAG = "CrashHandler"
private const val SCREENSHOTS_DIR = "crash_screenshots"
private const val SCREENSHOT_TIMEOUT_MS = 5000L
}
fun setCurrentActivity(activity: Activity?) {
currentActivity = activity?.let { WeakReference(it) }
}
override fun uncaughtException(thread: Thread, throwable: Throwable) {
Log.d(TAG, "Uncaught exception detected on thread: ${thread.name}")
try {
/** Screenshot logic goes here */
defaultHandler?.uncaughtException(thread, throwable)
} catch (e: Exception) {
Log.d(TAG, "Failed to handle crash with screenshot", e)
}
defaultHandler?.uncaughtException(thread, throwable)
}
}Step 3: Implement Screenshot Logic
Use PixelCopy together with the weak reference to the current Activity to capture the final screen. The code checks the view hierarchy, captures the bitmap, and saves it to a file.
class CrashHandler(
private val application: Application,
) : Thread.UncaughtExceptionHandler {
// ... previous members ...
private val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
override fun uncaughtException(thread: Thread, throwable: Throwable) {
Log.d(TAG, "Uncaught exception detected on thread: ${thread.name}")
try {
runBlocking {
withTimeout(SCREENSHOT_TIMEOUT_MS) {
captureScreenshot(throwable)
}
}
defaultHandler?.uncaughtException(thread, throwable)
} catch (e: Exception) {
Log.d(TAG, "Failed to handle crash with screenshot", e)
}
defaultHandler?.uncaughtException(thread, throwable)
}
private suspend fun captureScreenshot(throwable: Throwable) {
val activity = currentActivity?.get() ?: run {
Log.d(TAG, "No current activity, not capturing screenshot")
return
}
val root = activity.window.decorView.rootView
if (root == null || root.width <= 0 || root.height <= 0 || !root.isShown) {
Log.d(TAG, "Root view is invalid, not capturing screenshot")
return
}
val window = root.phoneWindow ?: run {
Log.d(TAG, "Phone window is null, not capturing screenshot")
return
}
val screenshot = Bitmap.createBitmap(root.width, root.height, Bitmap.Config.ARGB_8888)
try {
PixelCopy.request(window, screenshot) { copyResult ->
if (copyResult == PixelCopy.SUCCESS) {
Log.d(TAG, "Screenshot captured successfully")
} else {
Log.d(TAG, "Failed to capture screenshot")
}
}
} catch (e: Exception) {
Log.d(TAG, "Failed to capture screenshot", e)
}
saveScreenshotToFile(screenshot, throwable)
}
private fun saveScreenshotToFile(bitmap: Bitmap, throwable: Throwable) {
executor.execute {
try {
val screenshotsDir = File(application.filesDir, SCREENSHOTS_DIR)
if (!screenshotsDir.exists()) screenshotsDir.mkdirs()
val timestamp = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val filename = "screenshot_${timestamp}_${throwable.javaClass.simpleName}.jpg"
val screenshotFile = File(screenshotsDir, filename)
FileOutputStream(screenshotFile).use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
}
Log.d(TAG, "Screenshot saved to: ${screenshotFile.absolutePath}")
} catch (e: Exception) {
Log.d(TAG, "Failed to save screenshot", e)
}
}
}
}AndroidPub
Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
