mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16: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
|
package eu.darken.capod.monitor.core.worker
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.ForegroundServiceStartNotAllowedException
|
||||||
|
import android.app.Notification
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||||
@@ -70,44 +73,63 @@ class MonitorService : Service() {
|
|||||||
private val monitorScope = MonitorCoroutineScope()
|
private val monitorScope = MonitorCoroutineScope()
|
||||||
private var monitoringJob: Job? = null
|
private var monitoringJob: Job? = null
|
||||||
@Volatile private var monitorGeneration = 0
|
@Volatile private var monitorGeneration = 0
|
||||||
|
private var foregroundStartFailed = false
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@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() {
|
override fun onCreate() {
|
||||||
// Promote to foreground BEFORE Hilt DI (triggered by super.onCreate()) to avoid
|
// Promote to foreground BEFORE Hilt DI (triggered by super.onCreate()) to avoid
|
||||||
// ForegroundServiceDidNotStartInTimeException when DI is slow on backgrounded cold starts.
|
// ForegroundServiceDidNotStartInTimeException when DI is slow on backgrounded cold starts.
|
||||||
MonitorNotifications.ensureChannel(this)
|
MonitorNotifications.ensureChannel(this)
|
||||||
val earlyNotification = MonitorNotifications.createEarlyNotification(this)
|
if (!promoteToForeground(MonitorNotifications.createEarlyNotification(this))) {
|
||||||
if (hasApiLevel(29)) {
|
foregroundStartFailed = true
|
||||||
startForeground(
|
stopSelf()
|
||||||
MonitorNotifications.NOTIFICATION_ID,
|
super.onCreate()
|
||||||
earlyNotification,
|
return
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
startForeground(MonitorNotifications.NOTIFICATION_ID, earlyNotification)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
log(TAG, VERBOSE) { "onCreate()" }
|
log(TAG, VERBOSE) { "onCreate()" }
|
||||||
|
|
||||||
// Replace early notification with the full one from injected MonitorNotifications.
|
// Replace early notification with the full one from injected MonitorNotifications.
|
||||||
// Second startForeground() with the same ID updates the notification in place and is
|
// Failure here is non-fatal — the service is already foreground from the early call.
|
||||||
// preferred over notify() for robust foreground state on OEM variants.
|
promoteToForeground(notifications.getStartupNotification())
|
||||||
val notification = notifications.getStartupNotification()
|
|
||||||
if (hasApiLevel(29)) {
|
|
||||||
startForeground(
|
|
||||||
MonitorNotifications.NOTIFICATION_ID,
|
|
||||||
notification,
|
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
startForeground(MonitorNotifications.NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
log(TAG, VERBOSE) { "onStartCommand(intent=$intent, flags=$flags, startId=$startId)" }
|
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
|
val forceStart = intent?.getBooleanExtra(EXTRA_FORCE_START, false) ?: false
|
||||||
|
|
||||||
if (monitoringJob?.isActive == true && !forceStart) {
|
if (monitoringJob?.isActive == true && !forceStart) {
|
||||||
|
|||||||
@@ -184,13 +184,13 @@ class MonitorNotifications @Inject constructor(
|
|||||||
Intent(context, MainActivity::class.java),
|
Intent(context, MainActivity::class.java),
|
||||||
PendingIntentCompat.FLAG_IMMUTABLE,
|
PendingIntentCompat.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply {
|
||||||
.setContentIntent(openPi)
|
setContentIntent(openPi)
|
||||||
.setSmallIcon(R.drawable.device_earbuds_generic_both)
|
setSmallIcon(R.drawable.device_earbuds_generic_both)
|
||||||
.setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setOngoing(true)
|
setOngoing(true)
|
||||||
.build()
|
}.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user