Fix ForegroundServiceDidNotStartInTimeException (#6470)

* Start the `FetchPushForegroundService ` in foreground ASAP. This is a first step to mitigate `ForegroundServiceDidNotStartInTimeException` being thrown.

* Don't stop the service immediately if it's running but not in foreground. Try waiting up to 5s for it to be in foreground.
This commit is contained in:
Jorge Martin Espinosa
2026-03-27 07:54:27 +01:00
committed by GitHub
parent 2c083a87d4
commit 8239a5ba0a
4 changed files with 53 additions and 10 deletions

View File

@@ -22,5 +22,5 @@ interface PushHandlingWakeLock {
/**
* Release the wakelock. If no wakelock is associated with the key, this method does nothing.
*/
fun unlock()
suspend fun unlock()
}

View File

@@ -31,7 +31,7 @@ class DefaultPushHandlingWakeLock(
count.incrementAndGet()
}
override fun unlock() {
override suspend fun unlock() {
Timber.d("Releasing wakelock used for push handling.")
FetchPushForegroundService.stop(context)
if (count.decrementAndGet() <= 0) {

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.push.impl.push
import android.app.ActivityManager
import android.app.Service
import android.content.Context
import android.content.Intent
@@ -25,7 +26,12 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
private const val NOTIFICATION_ID = 1001
@@ -51,11 +57,13 @@ class FetchPushForegroundService : Service() {
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
override fun onCreate() {
Timber.d("Creating FetchPushForegroundService")
bindings<PushBindings>().inject(this)
wakelock.acquire(wakelockTimeout)
Timber.d("Starting FetchPushForegroundService with wakelock timeout of $wakelockTimeout ms")
// Start the foreground service as soon as possible
val notificationCompat = NotificationCompat.Builder(this, notificationChannels.getSilentChannelId())
.setSmallIcon(CommonDrawables.ic_notification)
.setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title))
@@ -65,6 +73,12 @@ class FetchPushForegroundService : Service() {
.build()
startForeground(NOTIFICATION_ID, notificationCompat)
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
wakelock.acquire(wakelockTimeout)
// The timeout is not automatic before Android 15, so we need to schedule it ourselves
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
coroutineScope.launch {
@@ -86,10 +100,14 @@ class FetchPushForegroundService : Service() {
override fun onTimeout(startId: Int) {
super.onTimeout(startId)
pushHandlingWakeLock.unlock()
Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService")
coroutineScope.launch { pushHandlingWakeLock.unlock() }
}
companion object {
private val stopMutex = Mutex()
fun startIfNeeded(context: Context) {
// Don't start the foreground service if the device is already awake
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
@@ -107,9 +125,34 @@ class FetchPushForegroundService : Service() {
}
}
fun stop(context: Context) {
val intent = Intent(context, FetchPushForegroundService::class.java)
context.stopService(intent)
suspend fun stop(context: Context) = stopMutex.withLock {
val runningServiceInfo = getRunningServiceInfo(context)
if (runningServiceInfo != null) {
val intent = Intent(context, FetchPushForegroundService::class.java)
// If it's still not running in foreground, it means the service is still starting,
// so we delay the stop to give it time to start and be set as foreground, otherwise we can crash
// with `ForegroundServiceDidNotStartInTimeException`.
var isInForeground = runningServiceInfo.foreground
withTimeoutOrNull(5.seconds) {
while (!isInForeground) {
delay(50)
val updatedServiceInfo = getRunningServiceInfo(context)
if (updatedServiceInfo == null) {
Timber.d("FetchPushForegroundService is no longer running, no need to stop it.")
return@withTimeoutOrNull
}
isInForeground = updatedServiceInfo.foreground == true
}
} ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.")
context.stopService(intent)
}
}
@Suppress("DEPRECATION")
private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? {
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
return activityManager.getRunningServices(Int.MAX_VALUE)
.firstOrNull { it.service.className == FetchPushForegroundService::class.java.name }
}
}
}

View File

@@ -18,7 +18,7 @@ class FakePushHandlingWakeLock(
lock.invoke(time)
}
override fun unlock() {
override suspend fun unlock() {
unlock.invoke()
}
}