diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaver.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaver.kt index 1ad1d0ca..07b116d6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaver.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaver.kt @@ -20,6 +20,7 @@ */ package com.greenart7c3.nostrsigner.service.crashreports +import java.util.concurrent.TimeoutException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -33,12 +34,29 @@ class UnexpectedCrashSaver( t: Thread, e: Throwable, ) { - if (e !is OutOfMemoryError) { - // OOM reports are junk + if (!isJunkReport(e)) { scope.launch { cache.writeReport(ReportAssembler().buildReport(e)) } } defaultUEH!!.uncaughtException(t, e) } + + companion object { + /** + * Crashes we never prompt the user to report because nobody can act on them: + * - OutOfMemoryError: junk reports. + * - Platform finalizer-watchdog timeouts (e.g. + * `BinderInternal$GcWatcher.finalize() timed out after 10 seconds`): a known + * AOSP issue raised on the FinalizerDaemon thread with no app frames. The OS + * kills the process regardless; app code cannot catch, prevent, or fix it. + */ + fun isJunkReport(e: Throwable): Boolean = e is OutOfMemoryError || isPlatformFinalizerTimeout(e) + + private fun isPlatformFinalizerTimeout(e: Throwable): Boolean = e is TimeoutException && + ( + e.message?.contains("finalize() timed out") == true || + e.stackTrace.any { it.className.startsWith("java.lang.Daemons\$FinalizerDaemon") } + ) + } } diff --git a/app/src/test/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaverTest.kt b/app/src/test/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaverTest.kt new file mode 100644 index 00000000..aa8fbfb7 --- /dev/null +++ b/app/src/test/java/com/greenart7c3/nostrsigner/service/crashreports/UnexpectedCrashSaverTest.kt @@ -0,0 +1,53 @@ +package com.greenart7c3.nostrsigner.service.crashreports + +import java.util.concurrent.TimeoutException +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class UnexpectedCrashSaverTest { + private fun finalizerWatchdogCrash(): TimeoutException { + val e = TimeoutException("com.android.internal.os.BinderInternal\$GcWatcher.finalize() timed out after 10 seconds") + e.stackTrace = + arrayOf( + StackTraceElement("com.android.internal.os.BinderInternal\$GcWatcher", "finalize", "BinderInternal.java", 64), + StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "doFinalize", "Daemons.java", 389), + StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "processReference", "Daemons.java", 369), + StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "runInternal", "Daemons.java", 354), + StackTraceElement("java.lang.Daemons\$Daemon", "run", "Daemons.java", 135), + StackTraceElement("java.lang.Thread", "run", "Thread.java", 1564), + ) + return e + } + + @Test + fun `binder gc watcher finalizer timeout is junk`() { + assertTrue(UnexpectedCrashSaver.isJunkReport(finalizerWatchdogCrash())) + } + + @Test + fun `finalizer daemon stack is junk even without the message`() { + val e = TimeoutException(null as String?) + e.stackTrace = + arrayOf( + StackTraceElement("android.os.BinderProxy", "finalize", "BinderProxy.java", 100), + StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "doFinalize", "Daemons.java", 389), + ) + assertTrue(UnexpectedCrashSaver.isJunkReport(e)) + } + + @Test + fun `out of memory is junk`() { + assertTrue(UnexpectedCrashSaver.isJunkReport(OutOfMemoryError())) + } + + @Test + fun `app timeout exception is not junk`() { + assertFalse(UnexpectedCrashSaver.isJunkReport(TimeoutException("socket timed out"))) + } + + @Test + fun `regular crash is not junk`() { + assertFalse(UnexpectedCrashSaver.isJunkReport(IllegalStateException("boom"))) + } +}