Symptom
Recurring background ANR on the FOSS build (observed on 1.0.4 AND 1.0.7). exit-info: reason=6 (ANR), subject Broadcast of Intent ... UnifiedPushReceiver (MESSAGE), ~60s timeout, not user-perceptible. The wake broadcast is the victim, not the cause — the main thread is already stuck.
Root cause (deobfuscated 1.0.7 trace)
FutureTask.get() ← blocks the MAIN thread
org.acra.data.CrashReportDataFactory.collect(:81) ← ACRA gathers report data (blocking)
org.acra.builder.ReportExecutor.execute
org.acra.reporter.ErrorReporterImpl.handleSilentException
com.rousecontext.app.support.AcraCrashReporter.logCaughtException(AcraCrashReporter.kt:26)
com.rousecontext.work.TunnelForegroundService$collectIncomingSessions...(:257)
kotlinx.coroutines.DispatchedTask.run ← Dispatchers.Main
TunnelForegroundService.collectIncomingSessions does lifecycleScope.launch { try { sessionHandler.handleStream(stream) } catch (e) { crashReporter.logCaughtException(e) } }. lifecycleScope = Dispatchers.Main.immediate, so the catch runs on Main. AcraCrashReporter.logCaughtException → ACRA.errorReporter.handleSilentException, and ACRA's CrashReportDataFactory.collect fans collectors into a thread pool and blocks the calling thread on FutureTask.get until collection finishes/times out. On Main that's a ~60s freeze → ANR. Any incoming-session error (which can repeat) re-triggers it — the exit-info showed two stacked 60s timeouts.
(The google/Firebase reporter is immune — recordException is async.)
Fix
Make AcraCrashReporter.logCaughtException (and any other blocking ACRA call) never run on the caller's thread: dispatch handleSilentException onto a background dispatcher, fire-and-forget (inject the app scope; appScope.launch(Dispatchers.IO) { ACRA.errorReporter.handleSilentException(throwable) }). Silent (non-fatal) reports don't need to block, so async collection is correct. This protects ALL call sites, not just collectIncomingSessions.
Secondary hardening: collectIncomingSessions runs handleStream on lifecycleScope (Main) too — consider a background dispatcher there (session/tool work shouldn't sit on Main). Not required once the reporter is off-main, but cleaner.
Test
logCaughtException returns immediately and does not block the calling thread even if ACRA collection is slow (fake/inject a slow handleSilentException; assert the call site returns before it completes / runs off the caller thread).
Notes
Deobfuscated via a reproducible rebuild of the v1.0.7 tag (same R8 map-id 13eced08…). This is the ANR Jason has hit repeatedly on-device; supersedes the earlier vague #506-era suspicion — the goAsync fix was fine; the real blocker is ACRA-on-Main.
Symptom
Recurring background ANR on the FOSS build (observed on 1.0.4 AND 1.0.7). exit-info:
reason=6 (ANR), subjectBroadcast of Intent ... UnifiedPushReceiver (MESSAGE), ~60s timeout, not user-perceptible. The wake broadcast is the victim, not the cause — the main thread is already stuck.Root cause (deobfuscated 1.0.7 trace)
TunnelForegroundService.collectIncomingSessionsdoeslifecycleScope.launch { try { sessionHandler.handleStream(stream) } catch (e) { crashReporter.logCaughtException(e) } }.lifecycleScope=Dispatchers.Main.immediate, so the catch runs on Main.AcraCrashReporter.logCaughtException→ACRA.errorReporter.handleSilentException, and ACRA'sCrashReportDataFactory.collectfans collectors into a thread pool and blocks the calling thread onFutureTask.getuntil collection finishes/times out. On Main that's a ~60s freeze → ANR. Any incoming-session error (which can repeat) re-triggers it — the exit-info showed two stacked 60s timeouts.(The
google/Firebase reporter is immune —recordExceptionis async.)Fix
Make
AcraCrashReporter.logCaughtException(and any other blocking ACRA call) never run on the caller's thread: dispatchhandleSilentExceptiononto a background dispatcher, fire-and-forget (inject the app scope;appScope.launch(Dispatchers.IO) { ACRA.errorReporter.handleSilentException(throwable) }). Silent (non-fatal) reports don't need to block, so async collection is correct. This protects ALL call sites, not justcollectIncomingSessions.Secondary hardening:
collectIncomingSessionsrunshandleStreamonlifecycleScope(Main) too — consider a background dispatcher there (session/tool work shouldn't sit on Main). Not required once the reporter is off-main, but cleaner.Test
logCaughtExceptionreturns immediately and does not block the calling thread even if ACRA collection is slow (fake/inject a slowhandleSilentException; assert the call site returns before it completes / runs off the caller thread).Notes
Deobfuscated via a reproducible rebuild of the
v1.0.7tag (same R8 map-id 13eced08…). This is the ANR Jason has hit repeatedly on-device; supersedes the earlier vague #506-era suspicion — the goAsync fix was fine; the real blocker is ACRA-on-Main.