diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt index 2b19ed9225..5c76eb1864 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt @@ -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() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt index 1388aac19b..9713110042 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt @@ -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) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt index f0f4ebb2de..d7cc950b99 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt @@ -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().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 } } } } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt index 925581db9b..077c8f661e 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt @@ -18,7 +18,7 @@ class FakePushHandlingWakeLock( lock.invoke(time) } - override fun unlock() { + override suspend fun unlock() { unlock.invoke() } }