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. * 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() count.incrementAndGet()
} }
override fun unlock() { override suspend fun unlock() {
Timber.d("Releasing wakelock used for push handling.") Timber.d("Releasing wakelock used for push handling.")
FetchPushForegroundService.stop(context) FetchPushForegroundService.stop(context)
if (count.decrementAndGet() <= 0) { if (count.decrementAndGet() <= 0) {

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.push.impl.push package io.element.android.libraries.push.impl.push
import android.app.ActivityManager
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -25,7 +26,12 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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.minutes
import kotlin.time.Duration.Companion.seconds
private const val NOTIFICATION_ID = 1001 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) 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()) val notificationCompat = NotificationCompat.Builder(this, notificationChannels.getSilentChannelId())
.setSmallIcon(CommonDrawables.ic_notification) .setSmallIcon(CommonDrawables.ic_notification)
.setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title)) .setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title))
@ -65,6 +73,12 @@ class FetchPushForegroundService : Service() {
.build() .build()
startForeground(NOTIFICATION_ID, notificationCompat) 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 // 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) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
coroutineScope.launch { coroutineScope.launch {
@ -86,10 +100,14 @@ class FetchPushForegroundService : Service() {
override fun onTimeout(startId: Int) { override fun onTimeout(startId: Int) {
super.onTimeout(startId) super.onTimeout(startId)
pushHandlingWakeLock.unlock() Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService")
coroutineScope.launch { pushHandlingWakeLock.unlock() }
} }
companion object { companion object {
private val stopMutex = Mutex()
fun startIfNeeded(context: Context) { fun startIfNeeded(context: Context) {
// Don't start the foreground service if the device is already awake // Don't start the foreground service if the device is already awake
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
@ -107,9 +125,34 @@ class FetchPushForegroundService : Service() {
} }
} }
fun stop(context: Context) { suspend fun stop(context: Context) = stopMutex.withLock {
val intent = Intent(context, FetchPushForegroundService::class.java) val runningServiceInfo = getRunningServiceInfo(context)
context.stopService(intent) 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) lock.invoke(time)
} }
override fun unlock() { override suspend fun unlock() {
unlock.invoke() unlock.invoke()
} }
} }