fix(notif): always stopSelf when foreground promotion fails

ForegroundServiceDidNotStartInTimeException crashes the whole app when a
service started via startForegroundService() never successfully calls
startForeground() within Android's ~10s window.

NotificationRelayService.initializeForeground() only called stopSelf() on
ForegroundServiceStartNotAllowedException. Any other failure to promote to
the foreground (OEM-specific RemoteException/IllegalStateException, a
resource lookup failure while building the notification, etc.) was logged
but left the service in a "started but not foregrounded" zombie state,
guaranteeing the timeout crash.

Now stopSelf() runs on every failure path, which clears the OS's fgRequired
flag and cancels the pending timeout. onStartCommand also bails early
(START_NOT_STICKY) when foreground promotion failed, so we don't spin up
relay coroutines on a service that's tearing itself down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hCJDhJmCNkzqb7SaMWQgB
This commit is contained in:
Claude
2026-06-19 20:59:27 +00:00
parent a99e67d8be
commit a9439d014f
@@ -152,6 +152,10 @@ class NotificationRelayService : Service() {
// Safety: also call startForeground from onStartCommand in case
// onCreate didn't complete before onStartCommand fired (ntfy #1520)
initializeForeground()
// If we couldn't promote to the foreground, initializeForeground() already
// called stopSelf(); don't spin up relay coroutines on a service that's
// tearing itself down.
if (!foregroundStarted) return START_NOT_STICKY
startRelayConnection()
return START_STICKY
}
@@ -222,14 +226,23 @@ class NotificationRelayService : Service() {
)
foregroundStarted = true
} catch (e: Exception) {
// Any failure to promote to the foreground leaves the service in a
// "started but not foregrounded" zombie state. Once startForegroundService()
// has been called, Android expects startForeground() within ~10s; if it never
// lands it fires ForegroundServiceDidNotStartInTimeException and crashes the
// whole app. Tearing the service down clears the OS's fgRequired flag, which
// cancels that pending timeout, so we must stopSelf() on EVERY failure path —
// not just ForegroundServiceStartNotAllowedException. Other causes include
// OEM-specific RemoteException/IllegalStateException and resource lookup
// failures while building the notification.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
e is ForegroundServiceStartNotAllowedException
) {
Log.w(TAG, "Foreground service start not allowed, stopping self")
stopSelf()
} else {
Log.e(TAG, "Failed to start foreground", e)
Log.e(TAG, "Failed to start foreground, stopping self", e)
}
stopSelf()
}
}