From f9c5fa901175c4f6e7fddef48d1144814688deaa Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:46:24 +0600 Subject: [PATCH] Log app scope failures instead of crashing outside debug builds --- app/src/main/java/mihon/app/di/AppBindings.kt | 20 ++++++++++++++++++- .../mihon/core/metro/AppCoroutineScope.kt | 3 +++ .../kotlin/mihon/telemetry/TelemetryConfig.kt | 4 ++++ .../kotlin/mihon/telemetry/TelemetryConfig.kt | 2 ++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/mihon/app/di/AppBindings.kt b/app/src/main/java/mihon/app/di/AppBindings.kt index f6a9751b1..991aa7603 100644 --- a/app/src/main/java/mihon/app/di/AppBindings.kt +++ b/app/src/main/java/mihon/app/di/AppBindings.kt @@ -4,15 +4,20 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.Provides import dev.zacsweers.metro.SingleIn +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf +import logcat.LogPriority import mihon.core.metro.AppCoroutineScope +import mihon.core.metro.IsDebugBuild +import mihon.telemetry.TelemetryConfig import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML +import tachiyomi.core.common.util.system.logcat @BindingContainer object AppBindings { @@ -20,7 +25,20 @@ object AppBindings { @Provides @SingleIn(AppScope::class) @AppCoroutineScope - fun providesAppCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + fun providesAppCoroutineScope(@IsDebugBuild isDebugBuild: Boolean): CoroutineScope { + // Work here outlives the screen that started it, so a failure would crash the app on whatever screen is open + // by then. Debug builds still crash so the bug gets noticed. + val handler = CoroutineExceptionHandler { _, throwable -> + if (isDebugBuild) { + val thread = Thread.currentThread() + thread.uncaughtExceptionHandler?.uncaughtException(thread, throwable) + } else { + logcat(LogPriority.ERROR, throwable) { "Uncaught exception in the app scope" } + TelemetryConfig.recordException(throwable) + } + } + return CoroutineScope(SupervisorJob() + Dispatchers.IO + handler) + } @Provides @SingleIn(AppScope::class) diff --git a/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt b/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt index 678e56919..3f3cf745b 100644 --- a/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt +++ b/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt @@ -9,6 +9,9 @@ import dev.zacsweers.metro.Qualifier * [kotlinx.coroutines.Dispatchers.IO], so a failure in one child never takes the others down. Use it for work that is * owned by an application scoped component and must outlive any screen. Work that belongs to a screen, a view model or * another cancellable lifecycle keeps its own scope. + * + * A child that fails with an uncaught exception is logged and reported instead of crashing the app, except in debug + * builds, since by the time it fails the screen that started it may be long gone. */ @Qualifier @Target( diff --git a/telemetry/src/firebase/kotlin/mihon/telemetry/TelemetryConfig.kt b/telemetry/src/firebase/kotlin/mihon/telemetry/TelemetryConfig.kt index 324467ab2..d4018fcd7 100644 --- a/telemetry/src/firebase/kotlin/mihon/telemetry/TelemetryConfig.kt +++ b/telemetry/src/firebase/kotlin/mihon/telemetry/TelemetryConfig.kt @@ -59,6 +59,10 @@ object TelemetryConfig { .getCertificateFingerprints() .any { it == MIHON_CERTIFICATE_FINGERPRINT } } + + fun recordException(throwable: Throwable) { + crashlytics?.recordException(throwable) + } } private val MIHON_PACKAGES = hashSetOf("app.mihon", "app.mihon.debug") diff --git a/telemetry/src/noop/kotlin/mihon/telemetry/TelemetryConfig.kt b/telemetry/src/noop/kotlin/mihon/telemetry/TelemetryConfig.kt index e238db049..e7d726204 100644 --- a/telemetry/src/noop/kotlin/mihon/telemetry/TelemetryConfig.kt +++ b/telemetry/src/noop/kotlin/mihon/telemetry/TelemetryConfig.kt @@ -9,4 +9,6 @@ object TelemetryConfig { fun setAnalyticsEnabled(enabled: Boolean) = Unit fun setCrashlyticsEnabled(enabled: Boolean) = Unit + + fun recordException(throwable: Throwable) = Unit }