mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
fix(monitor): Prevent crash when OS denies foreground service start
Add promoteToForeground() helper that catches ForegroundServiceStartNotAllowedException (Android 12+) and SecurityException, allowing graceful service exit instead of crashing.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package eu.darken.capod.monitor.core.worker
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ForegroundServiceStartNotAllowedException
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
@@ -70,44 +73,63 @@ class MonitorService : Service() {
|
||||
private val monitorScope = MonitorCoroutineScope()
|
||||
private var monitoringJob: Job? = null
|
||||
@Volatile private var monitorGeneration = 0
|
||||
private var foregroundStartFailed = false
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun promoteToForeground(notification: Notification): Boolean {
|
||||
return try {
|
||||
if (hasApiLevel(29)) {
|
||||
startForeground(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
||||
)
|
||||
} else {
|
||||
startForeground(MonitorNotifications.NOTIFICATION_ID, notification)
|
||||
}
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
@Suppress("NewApi")
|
||||
if (Build.VERSION.SDK_INT >= 31 && e is ForegroundServiceStartNotAllowedException) {
|
||||
log(TAG, WARN) { "Foreground service start denied by OS: ${e.message}" }
|
||||
false
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
log(TAG, WARN) { "Foreground service start denied (security): ${e.message}" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
// Promote to foreground BEFORE Hilt DI (triggered by super.onCreate()) to avoid
|
||||
// ForegroundServiceDidNotStartInTimeException when DI is slow on backgrounded cold starts.
|
||||
MonitorNotifications.ensureChannel(this)
|
||||
val earlyNotification = MonitorNotifications.createEarlyNotification(this)
|
||||
if (hasApiLevel(29)) {
|
||||
startForeground(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
earlyNotification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
||||
)
|
||||
} else {
|
||||
startForeground(MonitorNotifications.NOTIFICATION_ID, earlyNotification)
|
||||
if (!promoteToForeground(MonitorNotifications.createEarlyNotification(this))) {
|
||||
foregroundStartFailed = true
|
||||
stopSelf()
|
||||
super.onCreate()
|
||||
return
|
||||
}
|
||||
|
||||
super.onCreate()
|
||||
log(TAG, VERBOSE) { "onCreate()" }
|
||||
|
||||
// Replace early notification with the full one from injected MonitorNotifications.
|
||||
// Second startForeground() with the same ID updates the notification in place and is
|
||||
// preferred over notify() for robust foreground state on OEM variants.
|
||||
val notification = notifications.getStartupNotification()
|
||||
if (hasApiLevel(29)) {
|
||||
startForeground(
|
||||
MonitorNotifications.NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
||||
)
|
||||
} else {
|
||||
startForeground(MonitorNotifications.NOTIFICATION_ID, notification)
|
||||
}
|
||||
// Failure here is non-fatal — the service is already foreground from the early call.
|
||||
promoteToForeground(notifications.getStartupNotification())
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
log(TAG, VERBOSE) { "onStartCommand(intent=$intent, flags=$flags, startId=$startId)" }
|
||||
|
||||
if (foregroundStartFailed) {
|
||||
log(TAG, WARN) { "Skipping monitor start, foreground promotion was denied." }
|
||||
stopSelf(startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
val forceStart = intent?.getBooleanExtra(EXTRA_FORCE_START, false) ?: false
|
||||
|
||||
if (monitoringJob?.isActive == true && !forceStart) {
|
||||
|
||||
@@ -184,13 +184,13 @@ class MonitorNotifications @Inject constructor(
|
||||
Intent(context, MainActivity::class.java),
|
||||
PendingIntentCompat.FLAG_IMMUTABLE,
|
||||
)
|
||||
return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentIntent(openPi)
|
||||
.setSmallIcon(R.drawable.device_earbuds_generic_both)
|
||||
.setContentTitle(context.getString(R.string.app_name))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply {
|
||||
setContentIntent(openPi)
|
||||
setSmallIcon(R.drawable.device_earbuds_generic_both)
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
setOngoing(true)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user