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 ef041baeb4
commit bba2e6df3f
4 changed files with 53 additions and 10 deletions
@@ -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()
} }
@@ -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) {
@@ -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 }
} }
} }
} }
@@ -18,7 +18,7 @@ class FakePushHandlingWakeLock(
lock.invoke(time) lock.invoke(time)
} }
override fun unlock() { override suspend fun unlock() {
unlock.invoke() unlock.invoke()
} }
} }