🛡️ Android Library

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.

Maven Central API 22+ Apache 2.0 Kotlin
💥

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

androidx.activity is a transitive dependency. Make sure android.useAndroidX=true is set in your gradle.properties.

Quick Start

Two lines in your Application class are all you need:

1

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()
        )
    }
}
2

Register in AndroidManifest (already handled)

The library's AndroidManifest.xml declares both crash activities automatically via manifest merge — no extra entries needed.

3

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

MethodDefaultDescription
debugMode(Boolean)falseShow detailed developer screen instead of the user-facing screen.
enableLogging(Boolean)truePersist crash data to device storage as serialized files.
maxCrashLogs(Int)50Maximum number of crash files kept on disk. Oldest are pruned. Must be > 0.
customUserActivity(KClass)built-inReplace the user-facing crash screen with your own AppCompatActivity.
customDeveloperActivity(KClass)built-inReplace the developer crash screen with your own AppCompatActivity.
enableAutoRestart(Boolean, delayMs)false, 1 sAutomatically restart the app after the crash screen is shown.
showNotification(Boolean, NotificationConfig?)falsePost a system notification when a crash is captured.
enableAnalytics(Boolean, AnalyticsProvider?)falseForward crash events to a custom analytics provider.
enableSecureMode(Boolean)falseRedact emails, card numbers, SSNs and phone numbers from all string data.
customDataProvider(CustomDataProvider)nullAttach additional key-value pairs to every crash report.
crashInterceptor(CrashInterceptor)nullPre-process crashes; returning true suppresses the CrashGuard screen.
excludeException(KClass<Throwable>)nonePass matching exceptions to the default handler instead of CrashGuard.
logStoragePath(String)filesDir/crash_logsOverride 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:

ScreenWhen shownContent
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:

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:

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.

PropertyTypeDescription
idStringUUID uniquely identifying this crash.
timestampLongUnix epoch ms when the crash occurred.
exceptionTypeStringFully-qualified class name of the exception.
exceptionMessageStringMessage from the exception (or "No message").
stackTraceStringFull formatted stack trace string.
threadNameStringName of the thread that threw the exception.
appVersionStringApp version name at crash time.
appPackageStringApplication package name.
deviceInfoDeviceInfoManufacturer, model, Android version, CPU ABI, screen resolution/density.
availableMemoryLongAvailable RAM in bytes.
totalMemoryLongTotal device RAM in bytes.
diskSpaceLongFree disk space (filesDir) in bytes.
batteryLevelFloatBattery charge level 0.0–1.0.
isChargingBooleanWhether the device was charging at crash time.
networkTypeString"WiFi" / "Cellular" / "Ethernet" / "No Connection" / "Unknown".
orientationString"Portrait" / "Landscape" / "Unknown".
activityStackList<String>Activity class names in the back-stack at crash time.
customDataMap<String, String>Key-value pairs from your CustomDataProvider.
formattedTimestampStringHuman-readable date/time: yyyy-MM-dd HH:mm:ss.SSS.
shortTimestampStringCompact form: MMM dd, HH:mm.
getFullReport()StringComplete formatted report with all fields.
toJson()StringAll 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.