CrashGuard
Industry-grade crash handling for Android. Customizable crash screens, persistent log storage, analytics hooks, and clean-architecture internals — all in one zero-boilerplate library.
Smart Crash Screens
Separate debug (full stack trace) and user-facing (friendly message) screens, fully replaceable.
Persistent Logging
Crash reports are stored on-device as serialized files and queryable via use-case APIs.
Analytics Hooks
Plug in your own Firebase / Sentry / Datadog provider with a single interface.
Secure Mode
Automatically redacts emails, credit-card numbers, SSNs and phone numbers from reports.
Clean Architecture
Domain/data layers, repository pattern, and use cases — no god class, fully testable.
Fluent Builder
Every behaviour is opt-in via a readable builder. Sane defaults out of the box.
Installation
CrashGuard is published on Maven Central. Add the dependency to your app module:
Kotlin DSL (build.gradle.kts)
ktsdependencies { implementation("io.github.alims-repo:crash-guard:1.0.1") }
Groovy (build.gradle)
groovydependencies { implementation "io.github.alims-repo:crash-guard:1.0.1" }
Minimum SDK: API 22 · Compile SDK: 37
android.useAndroidX=true is set in your gradle.properties.
Quick Start
Two lines in your Application class are all you need:
Initialize in Application.onCreate()
kotlinclass MyApp : Application() { override fun onCreate() { super.onCreate() CrashGuard.install( application = this, config = CrashGuardConfig.Builder(this) .debugMode(BuildConfig.DEBUG) // developer screen in debug builds .build() ) } }
Register in AndroidManifest (already handled)
The library's AndroidManifest.xml declares both crash activities automatically
via manifest merge — no extra entries needed.
Done! 🎉
CrashGuard now intercepts all uncaught exceptions. In debug builds it shows a full-detail developer screen; in release builds it shows a friendly user screen.
Builder API
All settings are optional — use only what you need.
kotlinCrashGuard.install( application = this, config = CrashGuardConfig.Builder(this) // ── Modes ──────────────────────────────────────────────────────── .debugMode(BuildConfig.DEBUG) // true → DeveloperCrashActivity .enableLogging(true) // persist crashes to device storage .maxCrashLogs(50) // max stored crash files (default 50) // ── Custom screens ─────────────────────────────────────────────── .customUserActivity(MyCrashActivity::class) .customDeveloperActivity(MyDevCrashActivity::class) // ── Auto restart ───────────────────────────────────────────────── .enableAutoRestart(true, delayMs = 1500L) // ── Analytics ──────────────────────────────────────────────────── .enableAnalytics(true, provider = MyAnalyticsProvider()) // ── Secure mode (redacts PII) ──────────────────────────────────── .enableSecureMode(true) // ── Custom context data ────────────────────────────────────────── .customDataProvider(MyDataProvider()) // ── Crash interceptor ──────────────────────────────────────────── .crashInterceptor(MyCrashInterceptor()) // ── Exception filter ───────────────────────────────────────────── .excludeException(IllegalArgumentException::class) // ── Storage path (default: filesDir/crash_logs) ────────────────── .logStoragePath("/custom/path") .build() )
Full Reference
| Method | Default | Description |
|---|---|---|
debugMode(Boolean) | false | Show detailed developer screen instead of the user-facing screen. |
enableLogging(Boolean) | true | Persist crash data to device storage as serialized files. |
maxCrashLogs(Int) | 50 | Maximum number of crash files kept on disk. Oldest are pruned. Must be > 0. |
customUserActivity(KClass) | built-in | Replace the user-facing crash screen with your own AppCompatActivity. |
customDeveloperActivity(KClass) | built-in | Replace the developer crash screen with your own AppCompatActivity. |
enableAutoRestart(Boolean, delayMs) | false, 1 s | Automatically restart the app after the crash screen is shown. |
showNotification(Boolean, NotificationConfig?) | false | Post a system notification when a crash is captured. |
enableAnalytics(Boolean, AnalyticsProvider?) | false | Forward crash events to a custom analytics provider. |
enableSecureMode(Boolean) | false | Redact emails, card numbers, SSNs and phone numbers from all string data. |
customDataProvider(CustomDataProvider) | null | Attach additional key-value pairs to every crash report. |
crashInterceptor(CrashInterceptor) | null | Pre-process crashes; returning true suppresses the CrashGuard screen. |
excludeException(KClass<Throwable>) | none | Pass matching exceptions to the default handler instead of CrashGuard. |
logStoragePath(String) | filesDir/crash_logs | Override the directory where .crash files are stored. |
Callbacks
Four lifecycle callbacks let you react to crash events without coupling to library internals.
kotlinCrashGuardConfig.Builder(this) // Fired immediately when a crash is captured. .onCrashDetected { crashData -> Log.e("App", "Crash: ${crashData.exceptionType}") } // Fired before the crash screen is shown. // Return false to suppress the screen entirely. .onBeforeCrashScreen { crashData -> val shouldShow = crashData.exceptionType != "java.lang.OutOfMemoryError" shouldShow } // Fired after the crash flow completes (just before killProcess). .onAfterCrashScreen { crashData -> // Upload report, flush logs, etc. uploadCrashReport(crashData) } // Fired once, when CrashGuard.install() completes successfully. .onInitialized { Log.d("App", "CrashGuard is active") } .build()
Crash Interceptor
Implement CrashInterceptor to pre-process crashes. Returning true
lets CrashGuard know you have handled the crash; it then forwards to the default system handler
and returns without showing the crash screen.
kotlinclass MyCrashInterceptor : CrashInterceptor { override fun onCrashIntercepted(throwable: Throwable, thread: Thread): Boolean { // Return true = you handled it (still forwarded to default handler) // Return false = CrashGuard proceeds normally if (throwable is SocketTimeoutException) { showNetworkErrorDialog() return true } return false } }
Excluding Exceptions
Use excludeException() to bypass CrashGuard for specific types. The exception is
forwarded directly to the system's default uncaught-exception handler.
kotlinCrashGuardConfig.Builder(this) .excludeException(CancellationException::class) .excludeException(InterruptedException::class) .build()
Built-in Crash Screens
CrashGuard ships two screens out of the box, selected automatically by debugMode:
| Screen | When shown | Content |
|---|---|---|
| UserCrashActivity | debugMode = false (release) |
Friendly error message, timestamp, Restart / Close buttons. |
| DeveloperCrashActivity | debugMode = true (debug) |
Exception type, full stack trace, device info, memory, battery, custom data. Copy / Share buttons. |
Custom Crash Screen
Replace either built-in screen with your own AppCompatActivity. The crash data is
delivered via the activity's Intent extra.
1 — Create the Activity
kotlinclass MyCrashActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Retrieve crash data from the Intent extra val crashData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getSerializableExtra( CrashGuardConstants.EXTRA_CRASH_DATA, CrashData::class.java ) } else { @Suppress("DEPRECATION") intent.getSerializableExtra(CrashGuardConstants.EXTRA_CRASH_DATA) as? CrashData } setContentView(R.layout.activity_my_crash) findViewById<TextView>(R.id.tv_error).text = crashData?.exceptionMessage } }
2 — Register in the Builder
kotlinCrashGuardConfig.Builder(this) .customUserActivity(MyCrashActivity::class) // for release .customDeveloperActivity(MyDevActivity::class) // for debug .build()
3 — Declare in AndroidManifest
xml<activity android:name=".MyCrashActivity" android:exported="false" />
Crash History
When enableLogging(true) (default), every crash is persisted to
filesDir/crash_logs/. Query stored crashes using the repository directly:
kotlin// Build the same storage/repository CrashGuard uses internally val storage = CrashLogStorage(context, CrashGuard.getConfig()) val repository = CrashRepositoryImpl(storage) // Retrieve crashes via use cases val allCrashesUseCase = GetAllCrashesUseCase(repository) val countUseCase = GetCrashCountUseCase(repository) val deleteAllUseCase = DeleteAllCrashesUseCase(repository) lifecycleScope.launch { val crashes = allCrashesUseCase.execute().getOrElse { emptyList() } crashes.forEach { crash -> Log.d("Crashes", crash.getFullReport()) } val count = countUseCase.execute().getOrDefault(0) Log.d("Crashes", "Total stored: $count") // Delete a specific crash DeleteCrashUseCase(repository).execute(crashes.first().id) // Wipe all deleteAllUseCase.execute() }
Export
kotlin// Export all crashes as a JSON string val json = storage.exportCrashesAsJson() // Export a single crash as a .txt file shared to external storage val file = storage.exportCrashAsText(crashData) Log.d("Export", "Saved to: ${file.absolutePath}")
Analytics Integration
Implement AnalyticsProvider to forward crash events to any backend:
kotlinclass FirebaseAnalyticsProvider : AnalyticsProvider { override fun logCrash(crashData: CrashData) { FirebaseCrashlytics.getInstance().apply { setCustomKey("crash_id", crashData.id) setCustomKey("thread", crashData.threadName) recordException(crashData.exception ?: RuntimeException(crashData.exceptionMessage)) } } override fun logEvent(eventName: String, params: Map<String, Any>) { val bundle = Bundle() params.forEach { (k, v) -> bundle.putString(k, v.toString()) } Firebase.analytics.logEvent(eventName, bundle) } } // Register in config: CrashGuardConfig.Builder(this) .enableAnalytics(true, provider = FirebaseAnalyticsProvider()) .build()
Custom Data & Secure Mode
Custom Data Provider
Attach extra context to every crash report — user ID, feature flags, network state, etc.:
kotlinclass AppContextProvider : CustomDataProvider { override fun provideCustomData(context: Context): Map<String, String> = mapOf( "user_id" to Session.userId, "feature_flags" to FeatureFlags.dump(), "build_flavor" to BuildConfig.FLAVOR ) }
Secure Mode
When enableSecureMode(true), CrashGuard automatically scrubs the following patterns
from all string fields in the crash report:
- Email addresses →
[EMAIL] - Credit/debit card numbers (16-digit groups) →
[CARD] - Social Security Numbers (US format) →
[SSN] - Phone numbers (E.164 and NANP) →
[PHONE]
Testing
Simulating a Crash
kotlin// Dispatches through the installed UncaughtExceptionHandler — // the full crash flow (save → screen → terminate) runs. CrashGuard.triggerCrash(RuntimeException("Test crash"))
Unit Testing
CrashGuard.uninstall() resets all state between tests:
kotlin@Before fun setUp() { CrashGuard.uninstall() // reset if a previous test called install() CrashGuard.install(mockApplication, testConfig) } @After fun tearDown() { CrashGuard.uninstall() }
ProGuard / R8
CrashGuard ships with consumer ProGuard rules that are applied automatically
to any app that depends on the library — you don't need to add anything to your own
proguard-rules.pro.
The bundled rules:
- Keep all Serializable domain model classes (
CrashData,DeviceInfo) with original class names and non-transient fields, preventingInvalidClassExceptionbetween builds. - Keep the public CrashGuard API surface for apps using R8 full mode.
proguard# (Auto-included — excerpt from crash-guard's consumer-rules.pro) -keep class io.github.alimsrepo.crashguard.domain.model.** implements java.io.Serializable { !static !transient <fields>; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); } -keep public class io.github.alimsrepo.crashguard.CrashGuard { public *; } -keep public class io.github.alimsrepo.crashguard.domain.config.** { public *; }
CrashData Reference
Every crash report is an instance of CrashData, which is fully serializable and queryable after the fact.
| Property | Type | Description |
|---|---|---|
id | String | UUID uniquely identifying this crash. |
timestamp | Long | Unix epoch ms when the crash occurred. |
exceptionType | String | Fully-qualified class name of the exception. |
exceptionMessage | String | Message from the exception (or "No message"). |
stackTrace | String | Full formatted stack trace string. |
threadName | String | Name of the thread that threw the exception. |
appVersion | String | App version name at crash time. |
appPackage | String | Application package name. |
deviceInfo | DeviceInfo | Manufacturer, model, Android version, CPU ABI, screen resolution/density. |
availableMemory | Long | Available RAM in bytes. |
totalMemory | Long | Total device RAM in bytes. |
diskSpace | Long | Free disk space (filesDir) in bytes. |
batteryLevel | Float | Battery charge level 0.0–1.0. |
isCharging | Boolean | Whether the device was charging at crash time. |
networkType | String | "WiFi" / "Cellular" / "Ethernet" / "No Connection" / "Unknown". |
orientation | String | "Portrait" / "Landscape" / "Unknown". |
activityStack | List<String> | Activity class names in the back-stack at crash time. |
customData | Map<String, String> | Key-value pairs from your CustomDataProvider. |
formattedTimestamp | String | Human-readable date/time: yyyy-MM-dd HH:mm:ss.SSS. |
shortTimestamp | String | Compact form: MMM dd, HH:mm. |
getFullReport() | String | Complete formatted report with all fields. |
toJson() | String | All fields as a valid JSON object string. |
License
CrashGuard is released under the Apache License 2.0.
textCopyright 2025 Alim Sourav
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.