From 0cb9b70f35649c6e2f793bd8111d2625d180ecb3 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 14 Apr 2026 11:07:56 +0200 Subject: [PATCH] fix: Handle ForegroundServiceDidNotStartInTimeException crash --- app/src/main/java/eu/darken/capod/App.kt | 28 +++++++++++++++++++ .../monitor/core/worker/MonitorService.kt | 23 +++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/darken/capod/App.kt b/app/src/main/java/eu/darken/capod/App.kt index 94e54202..d2aaa1dd 100644 --- a/app/src/main/java/eu/darken/capod/App.kt +++ b/app/src/main/java/eu/darken/capod/App.kt @@ -1,12 +1,16 @@ package eu.darken.capod import android.app.Application +import android.os.Looper import dagger.hilt.android.HiltAndroidApp import eu.darken.capod.common.coroutine.AppScope +import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter import eu.darken.capod.common.debug.logging.LogCatLogger import eu.darken.capod.common.debug.logging.Logging import eu.darken.capod.common.debug.logging.asLog +import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.throttleLatest @@ -23,6 +27,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.system.exitProcess @HiltAndroidApp open class App : Application() { @@ -37,6 +42,20 @@ open class App : Application() { super.onCreate() if (BuildConfig.DEBUG) Logging.install(LogCatLogger()) + var foregroundExceptionHandled = false + val oldHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + if (throwable.isForegroundServiceTimingException() && !foregroundExceptionHandled) { + foregroundExceptionHandled = true + log(TAG, WARN) { "Suppressed foreground service timing exception: ${throwable.asLog()}" } + Bugs.report(tag = TAG, "Foreground service timing exception suppressed", exception = throwable) + Looper.loop() + return@setDefaultUncaughtExceptionHandler + } + log(TAG, ERROR) { "UNCAUGHT EXCEPTION: ${throwable.asLog()}" } + if (oldHandler != null) oldHandler.uncaughtException(thread, throwable) else exitProcess(1) + } + autoReporting.setup(this) log(TAG) { "onCreate() done! ${Exception().asLog()}" } @@ -64,5 +83,14 @@ open class App : Application() { companion object { internal val TAG = logTag("CAP") + + private fun Throwable.isForegroundServiceTimingException(): Boolean { + var current: Throwable? = this + while (current != null) { + if (current.javaClass.simpleName == "ForegroundServiceDidNotStartInTimeException") return true + current = current.cause + } + return false + } } } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt index 8d56ed0f..cff0cf0c 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt @@ -79,6 +79,7 @@ class MonitorService : Service() { private var monitoringJob: Job? = null @Volatile private var monitorGeneration = 0 private var foregroundStartFailed = false + private var injectionComplete = false @SuppressLint("InlinedApi") private fun promoteToForeground(notification: Notification): Boolean { @@ -114,7 +115,12 @@ class MonitorService : Service() { if (!promoteToForeground(MonitorNotifications.createEarlyNotification(this))) { foregroundStartFailed = true stopSelf() - super.onCreate() + try { + super.onCreate() + } catch (e: Exception) { + log(TAG, WARN) { "Hilt DI failed in onCreate() (foreground denied): ${e.asLog()}" } + Bugs.report(tag = TAG, "Hilt DI failed in onCreate() (foreground denied)", exception = e) + } return } @@ -127,6 +133,7 @@ class MonitorService : Service() { stopSelf() return } + injectionComplete = true log(TAG, VERBOSE) { "onCreate()" } // Replace early notification with the full one from injected MonitorNotifications. @@ -302,12 +309,16 @@ class MonitorService : Service() { log(TAG, VERBOSE) { "onDestroy()" } monitorScope.cancel("Service destroyed") - if (generalSettings.useExtraMonitorNotification.valueBlocking && !generalSettings.keepConnectedNotificationAfterDisconnect.valueBlocking) { - try { - notificationManager.cancel(MonitorNotifications.NOTIFICATION_ID_CONNECTED) - } catch (e: Exception) { - log(TAG, WARN) { "Failed to cancel connected notification: ${e.message}" } + if (injectionComplete) { + if (generalSettings.useExtraMonitorNotification.valueBlocking && !generalSettings.keepConnectedNotificationAfterDisconnect.valueBlocking) { + try { + notificationManager.cancel(MonitorNotifications.NOTIFICATION_ID_CONNECTED) + } catch (e: Exception) { + log(TAG, WARN) { "Failed to cancel connected notification: ${e.message}" } + } } + } else { + log(TAG, WARN) { "onDestroy: Skipping notification cleanup, injection was incomplete." } } super.onDestroy()