Cleanup FetchPushForegroundService (#6577)

* Rename `PushHandlingWakeLock` to `FetchPushForegroundServiceManager`. Move the start/stop logic from `FetchPushForegroundService.Companion` to it.

* Add more tests using Robolectric.

* Remove `FeatureFlags.SyncNotificationsWithWorkManager` and associated code: this should have been removed in one of the previous refactors, since we don't have the 2 ways to sync notifications anymore, everything uses the `WorkManager`

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
Jorge Martin Espinosa 2026-04-20 16:03:12 +02:00 committed by GitHub
parent e14d3bb312
commit db100281d8
17 changed files with 329 additions and 215 deletions

View file

@ -104,14 +104,6 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
SyncNotificationsWithWorkManager(
key = "feature.sync_notifications_with_workmanager",
title = "Sync notifications with WorkManager",
description = "Use WorkManager to schedule notification sync tasks when a push is received." +
" This should improve reliability and battery usage.",
defaultValue = { true },
isFinished = false,
),
QrCodeLogin( QrCodeLogin(
key = "feature.qr_code_login", key = "feature.qr_code_login",
title = "QR Code Login", title = "QR Code Login",

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.api.push
/**
* A helper to manage the foreground service used to keep the device awake while we schedule and wait for the work to fetch the notification content to run.
*/
interface FetchPushForegroundServiceManager {
/**
* Start the foreground service to acquire the wakelock. If the device is already awake, this method does nothing.
*
* @return true if the service was started, false otherwise (e.g. if the device was already awake or if starting the service failed).
*/
fun start(): Boolean
/**
* Stop the foreground service to release the wakelock. If the service is not running, this method does nothing.
*
* @return true if the service was stopped, false otherwise (e.g. if the service was not running or if stopping the service failed).
*/
suspend fun stop(): Boolean
}

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.api.push
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
/**
* Abstraction over wakelocks used for push handling to ensure the device stays awake while we handle the push and schedule and run the work.
*/
interface PushHandlingWakeLock {
/**
* Acquire a wakelock. The wakelock will be held for the given [time] or until [unlock] is called, whichever happens first.
*/
fun lock(time: Duration = 1.minutes)
/**
* Release the wakelock. If no wakelock is associated with the key, this method does nothing.
*/
suspend fun unlock()
}

View file

@ -13,8 +13,6 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.features.call.api.CallType import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -32,7 +30,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
import io.element.android.libraries.push.impl.push.OnRedactedEventReceived import io.element.android.libraries.push.impl.push.OnRedactedEventReceived
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -60,8 +57,6 @@ class DefaultNotificationResultProcessor(
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
private val onRedactedEventReceived: OnRedactedEventReceived, private val onRedactedEventReceived: OnRedactedEventReceived,
private val onNotifiableEventReceived: OnNotifiableEventReceived, private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val featureFlagService: FeatureFlagService,
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
private val elementCallEntryPoint: ElementCallEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint,
private val notificationChannels: NotificationChannels, private val notificationChannels: NotificationChannels,
@AppCoroutineScope private val coroutineScope: CoroutineScope, @AppCoroutineScope private val coroutineScope: CoroutineScope,
@ -215,10 +210,6 @@ class DefaultNotificationResultProcessor(
if (nonRingingCallEvents.isNotEmpty()) { if (nonRingingCallEvents.isNotEmpty()) {
onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents) onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents)
} }
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncNotificationsWithWorkManager)) {
syncOnNotifiableEvent(results.keys.toList())
}
} }
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.push
import android.app.ActivityManager
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.content.ContextCompat
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultFetchPushForegroundServiceManager(
@ApplicationContext private val context: Context,
) : FetchPushForegroundServiceManager {
private val stopMutex = Mutex()
override fun start(): Boolean {
Timber.d("Acquiring wakelock for push handling, starting service.")
// Don't start the foreground service if the device is already awake
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
if (powerManager.isInteractive) {
Timber.d("Device is already in an interactive state, no need to start FetchPushForegroundService")
return false
}
val intent = Intent(context, FetchPushForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
runCatchingExceptions { ContextCompat.startForegroundService(context, intent) }
.onFailure { throwable ->
Timber.e(throwable, "Failed to start FetchPushForegroundService, notifications may take longer than usual to sync")
}
} else {
context.startService(intent)
}
return true
}
override suspend fun stop(): Boolean {
Timber.d("Releasing wakelock used for push handling, stopping service.")
return 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)
} else {
false
}
}
}
@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

@ -1,33 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.push
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
import timber.log.Timber
import kotlin.time.Duration
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultPushHandlingWakeLock(
@ApplicationContext private val context: Context,
) : PushHandlingWakeLock {
override fun lock(time: Duration) {
Timber.d("Acquiring wakelock for push handling, starting service.")
FetchPushForegroundService.startIfNeeded(context)
}
override suspend fun unlock() {
Timber.d("Releasing wakelock used for push handling.")
FetchPushForegroundService.stop(context)
}
}

View file

@ -7,32 +7,27 @@
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.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
import io.element.android.libraries.push.impl.di.PushBindings import io.element.android.libraries.push.impl.di.PushBindings
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
import io.element.android.libraries.ui.strings.CommonStrings 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 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
@ -48,7 +43,6 @@ class FetchPushForegroundService : Service() {
} }
@Inject lateinit var notificationChannels: NotificationChannels @Inject lateinit var notificationChannels: NotificationChannels
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
@Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope @Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope
private val wakelock: PowerManager.WakeLock by lazy { private val wakelock: PowerManager.WakeLock by lazy {
@ -78,8 +72,13 @@ class FetchPushForegroundService : Service() {
// Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs. // Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs.
// In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service // In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service
// from running in foreground later. // from running in foreground later.
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
} else {
0
}
runCatchingExceptions { runCatchingExceptions {
startForeground(NOTIFICATION_ID, notificationCompat) ServiceCompat.startForeground(this, NOTIFICATION_ID, notificationCompat, serviceType)
} }
.onSuccess { .onSuccess {
isOnForeground = true isOnForeground = true
@ -116,7 +115,7 @@ class FetchPushForegroundService : Service() {
override fun stopService(intent: Intent?): Boolean { override fun stopService(intent: Intent?): Boolean {
if (isOnForeground) { if (isOnForeground) {
wakelock.release() wakelock.release()
stopForeground(STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
} }
return super.stopService(intent) return super.stopService(intent)
@ -131,64 +130,7 @@ class FetchPushForegroundService : Service() {
Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground")
if (isOnForeground) { if (isOnForeground) {
Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService") Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService")
coroutineScope.launch { pushHandlingWakeLock.unlock() } stopSelf()
}
}
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
if (powerManager.isInteractive) return
start(context)
}
fun start(context: Context) {
val intent = Intent(context, FetchPushForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
runCatchingExceptions { context.startForegroundService(intent) }
.onFailure { throwable ->
Timber.e(
throwable,
"Failed to start FetchPushForegroundService, notifications may take longer than usual to sync"
)
}
} else {
context.startService(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

@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.auth.SessionRestorationException
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.isNetworkError import io.element.android.libraries.matrix.api.exception.isNetworkError
import io.element.android.libraries.push.api.push.PushHandlingWakeLock import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
import io.element.android.libraries.push.impl.db.PushRequest import io.element.android.libraries.push.impl.db.PushRequest
import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.PushHistoryService
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
@ -58,7 +58,7 @@ class FetchPendingNotificationsWorker(
private val resultProcessor: NotificationResultProcessor, private val resultProcessor: NotificationResultProcessor,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val systemClock: SystemClock, private val systemClock: SystemClock,
private val pushHandlingWakeLock: PushHandlingWakeLock, private val fetchPushForegroundServiceManager: FetchPushForegroundServiceManager,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.d("FetchNotificationsWorker started") Timber.d("FetchNotificationsWorker started")
@ -67,7 +67,8 @@ class FetchPendingNotificationsWorker(
inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId) inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId)
}.getOrNull() ?: return Result.failure() }.getOrNull() ?: return Result.failure()
pushHandlingWakeLock.unlock() // We can stop the foreground service and unlock the wakelock, since the work is now running and the device should be kept awake
fetchPushForegroundServiceManager.stop()
// Fetch pending requests in the last 24 hours // Fetch pending requests in the last 24 hours
val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days) val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days)

View file

@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.api.CallType import io.element.android.features.call.api.CallType
import io.element.android.features.call.test.FakeElementCallEntryPoint import io.element.android.features.call.test.FakeElementCallEntryPoint
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -34,7 +33,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
import io.element.android.libraries.push.impl.push.FakeOnRedactedEventReceived import io.element.android.libraries.push.impl.push.FakeOnRedactedEventReceived
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.any
@ -289,8 +287,6 @@ class DefaultNotificationResultProcessorTest {
userPushStoreFactory: FakeUserPushStoreFactory = FakeUserPushStoreFactory(), userPushStoreFactory: FakeUserPushStoreFactory = FakeUserPushStoreFactory(),
onRedactedEventReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = {}, onRedactedEventReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = {},
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = {}, onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = {},
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
syncOnNotifiableEvent: SyncOnNotifiableEvent = {},
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(), elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(), notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
coroutineScope: CoroutineScope = backgroundScope, coroutineScope: CoroutineScope = backgroundScope,
@ -301,8 +297,6 @@ class DefaultNotificationResultProcessorTest {
userPushStoreFactory = userPushStoreFactory, userPushStoreFactory = userPushStoreFactory,
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived), onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived), onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
featureFlagService = featureFlagService,
syncOnNotifiableEvent = syncOnNotifiableEvent,
elementCallEntryPoint = elementCallEntryPoint, elementCallEntryPoint = elementCallEntryPoint,
notificationChannels = notificationChannels, notificationChannels = notificationChannels,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.push
import android.app.ActivityManager
import android.content.ComponentName
import android.content.Context.ACTIVITY_SERVICE
import android.content.Context.POWER_SERVICE
import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Shadows
import org.robolectric.shadows.ShadowActivityManager
import org.robolectric.shadows.ShadowPowerManager
import kotlin.time.Duration.Companion.seconds
@RunWith(AndroidJUnit4::class)
class DefaultFetchPushForegroundServiceManagerTest {
@Test
fun `start should start the service if the device is not interactive`() {
val manager = createDefaultFetchPushForegroundServiceManager()
getShadowPowerManager().turnScreenOn(false)
assertThat(manager.start()).isTrue()
}
@Test
fun `start won't start the service if the device is interactive`() {
val manager = createDefaultFetchPushForegroundServiceManager()
getShadowPowerManager().turnScreenOn(true)
assertThat(manager.start()).isFalse()
}
@Test
fun `stop will stop the service if it's running`() = runTest {
val manager = createDefaultFetchPushForegroundServiceManager()
// Start the service first
getShadowPowerManager().turnScreenOn(false)
manager.start()
getShadowActivityManager().setServices(
listOf(
ActivityManager.RunningServiceInfo().apply {
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
foreground = true
}
)
)
assertThat(manager.stop()).isTrue()
}
@Test
fun `stop will eventually stop the service once it's on foreground`() = runTest {
val manager = createDefaultFetchPushForegroundServiceManager()
// Start the service first
getShadowPowerManager().turnScreenOn(false)
manager.start()
// The service is started, but not yet in foreground
getShadowActivityManager().setServices(
listOf(
ActivityManager.RunningServiceInfo().apply {
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
foreground = false
}
)
)
// We call stop, which won't stop the service yet since it's not in foreground
val future = async { manager.stop() }
// Then we set the service as running in foreground, which should allow the stop to complete
getShadowActivityManager().setServices(
listOf(
ActivityManager.RunningServiceInfo().apply {
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
foreground = true
}
)
)
val stopped = withTimeout(5.seconds) { future.await() }
assertThat(stopped).isTrue()
}
@Test
fun `stop will not stop the service if it's stopped`() = runTest {
val manager = createDefaultFetchPushForegroundServiceManager()
// Set some fake running service data, even if the service is not really running
getShadowActivityManager().setServices(
listOf(
ActivityManager.RunningServiceInfo().apply {
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
foreground = true
}
)
)
// Since the service was not really running, it was not stopped
assertThat(manager.stop()).isFalse()
}
private fun createDefaultFetchPushForegroundServiceManager() = DefaultFetchPushForegroundServiceManager(
context = InstrumentationRegistry.getInstrumentation().context,
)
private fun getShadowPowerManager(): ShadowPowerManager {
val powerManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(POWER_SERVICE) as PowerManager
return Shadows.shadowOf(powerManager)
}
private fun getShadowActivityManager(): ShadowActivityManager {
val activityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
return Shadows.shadowOf(activityManager)
}
}

View file

@ -28,7 +28,7 @@ import io.element.android.libraries.push.impl.notifications.FakeNotificationResu
import io.element.android.libraries.push.impl.notifications.fixtures.aPushRequest import io.element.android.libraries.push.impl.notifications.fixtures.aPushRequest
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
@ -239,7 +239,7 @@ class FetchPendingNotificationWorkerTest {
pushHistoryService: FakePushHistoryService = FakePushHistoryService(), pushHistoryService: FakePushHistoryService = FakePushHistoryService(),
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(), resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(),
systemClock: FakeSystemClock = FakeSystemClock(), systemClock: FakeSystemClock = FakeSystemClock(),
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(), pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
) = FetchPendingNotificationsWorker( ) = FetchPendingNotificationsWorker(
params = createWorkerParams(workDataOf("session_id" to input)), params = createWorkerParams(workDataOf("session_id" to input)),
context = InstrumentationRegistry.getInstrumentation().context, context = InstrumentationRegistry.getInstrumentation().context,
@ -250,7 +250,7 @@ class FetchPendingNotificationWorkerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
resultProcessor = resultProcessor, resultProcessor = resultProcessor,
systemClock = systemClock, systemClock = systemClock,
pushHandlingWakeLock = pushHandlingWakeLock, fetchPushForegroundServiceManager = pushHandlingWakeLock,
) )
private fun TestScope.createWorkerParams( private fun TestScope.createWorkerParams(

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.test.push
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
class FakeFetchPushForegroundServiceManager(
private val lock: () -> Boolean = { true },
private val unlock: () -> Boolean = { true },
) : FetchPushForegroundServiceManager {
override fun start(): Boolean {
return lock.invoke()
}
override suspend fun stop(): Boolean {
return unlock.invoke()
}
}

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.test.push
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
import kotlin.time.Duration
class FakePushHandlingWakeLock(
private val lock: (time: Duration) -> Unit = {},
private val unlock: () -> Unit = {},
) : PushHandlingWakeLock {
override fun lock(time: Duration) {
lock.invoke(time)
}
override suspend fun unlock() {
unlock.invoke()
}
}

View file

@ -15,7 +15,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.push.api.push.PushHandlingWakeLock import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -27,7 +27,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
@Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushParser: FirebasePushParser
@Inject lateinit var pushHandler: PushHandler @Inject lateinit var pushHandler: PushHandler
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock @Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
@AppCoroutineScope @AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope @Inject lateinit var coroutineScope: CoroutineScope
@ -49,7 +49,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
val isHighPriority = message.priority == PRIORITY_HIGH val isHighPriority = message.priority == PRIORITY_HIGH
if (isHighPriority) { if (isHighPriority) {
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work // Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
pushHandlingWakeLock.lock() fetchPushForegroundServiceManager.start()
} }
coroutineScope.launch { coroutineScope.launch {
@ -63,7 +63,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
}, },
) )
if (isHighPriority) { if (isHighPriority) {
pushHandlingWakeLock.unlock() fetchPushForegroundServiceManager.stop()
} }
} else { } else {
val handled = pushHandler.handle( val handled = pushHandler.handle(
@ -73,7 +73,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long. // If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long.
if (!handled && isHighPriority) { if (!handled && isHighPriority) {
pushHandlingWakeLock.unlock() fetchPushForegroundServiceManager.stop()
} }
} }
} }

View file

@ -15,7 +15,7 @@ import com.google.firebase.messaging.RemoteMessage
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SECRET import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager
import io.element.android.libraries.push.test.test.FakePushHandler import io.element.android.libraries.push.test.test.FakePushHandler
import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
@ -29,7 +29,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import kotlin.time.Duration
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class VectorFirebaseMessagingServiceTest { class VectorFirebaseMessagingServiceTest {
@ -81,11 +80,11 @@ class VectorFirebaseMessagingServiceTest {
@Test @Test
fun `test pushHandler returning true locks and does not unlock the wakelock so it continues running`() = runTest { fun `test pushHandler returning true locks and does not unlock the wakelock so it continues running`() = runTest {
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> } val lockLambda = lambdaRecorder<Boolean> { true }
val unlockLambda = lambdaRecorder<Unit> { } val unlockLambda = lambdaRecorder<Boolean> { true }
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
pushHandler = FakePushHandler(handleResult = { _, _ -> true }), pushHandler = FakePushHandler(handleResult = { _, _ -> true }),
pushHandlingWakeLock = FakePushHandlingWakeLock( pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
lock = lockLambda, lock = lockLambda,
unlock = unlockLambda unlock = unlockLambda
) )
@ -113,11 +112,11 @@ class VectorFirebaseMessagingServiceTest {
@Test @Test
fun `test pushHandler returning false locks and unlocks the wakelock early`() = runTest { fun `test pushHandler returning false locks and unlocks the wakelock early`() = runTest {
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> } val lockLambda = lambdaRecorder<Boolean> { true }
val unlockLambda = lambdaRecorder<Unit> { } val unlockLambda = lambdaRecorder<Boolean> { true }
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
pushHandler = FakePushHandler(handleResult = { _, _ -> false }), pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
pushHandlingWakeLock = FakePushHandlingWakeLock( pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
lock = lockLambda, lock = lockLambda,
unlock = unlockLambda unlock = unlockLambda
) )
@ -145,11 +144,11 @@ class VectorFirebaseMessagingServiceTest {
@Test @Test
fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest { fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest {
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> } val lockLambda = lambdaRecorder<Boolean> { true }
val unlockLambda = lambdaRecorder<Unit> { } val unlockLambda = lambdaRecorder<Boolean> { true }
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
pushHandler = FakePushHandler(handleResult = { _, _ -> false }), pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
pushHandlingWakeLock = FakePushHandlingWakeLock( pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
lock = lockLambda, lock = lockLambda,
unlock = unlockLambda unlock = unlockLambda
) )
@ -186,14 +185,14 @@ class VectorFirebaseMessagingServiceTest {
private fun TestScope.createVectorFirebaseMessagingService( private fun TestScope.createVectorFirebaseMessagingService(
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(), firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
pushHandler: PushHandler = FakePushHandler(), pushHandler: PushHandler = FakePushHandler(),
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(), pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
): VectorFirebaseMessagingService { ): VectorFirebaseMessagingService {
return VectorFirebaseMessagingService().apply { return VectorFirebaseMessagingService().apply {
this.firebaseNewTokenHandler = firebaseNewTokenHandler this.firebaseNewTokenHandler = firebaseNewTokenHandler
this.pushParser = FirebasePushParser() this.pushParser = FirebasePushParser()
this.pushHandler = pushHandler this.pushHandler = pushHandler
this.coroutineScope = this@createVectorFirebaseMessagingService this.coroutineScope = this@createVectorFirebaseMessagingService
this.pushHandlingWakeLock = pushHandlingWakeLock this.fetchPushForegroundServiceManager = pushHandlingWakeLock
} }
} }
} }

View file

@ -14,7 +14,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.push.api.push.PushHandlingWakeLock import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
@ -38,7 +38,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler @Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler @Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock @Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
@AppCoroutineScope @AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope @Inject lateinit var coroutineScope: CoroutineScope
@ -59,8 +59,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
* @param instance connection, for multi-account * @param instance connection, for multi-account
*/ */
override fun onMessage(context: Context, message: PushMessage, instance: String) { override fun onMessage(context: Context, message: PushMessage, instance: String) {
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work // Start the foreground service to ensure the device stays awake while we handle the push and schedule and run the work.
pushHandlingWakeLock.lock() fetchPushForegroundServiceManager.start()
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}") Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
coroutineScope.launch { coroutineScope.launch {
@ -71,16 +71,16 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
providerInfo = "${UnifiedPushConfig.NAME} - $instance", providerInfo = "${UnifiedPushConfig.NAME} - $instance",
data = String(message.content), data = String(message.content),
) )
pushHandlingWakeLock.unlock() fetchPushForegroundServiceManager.stop()
} else { } else {
val handled = pushHandler.handle( val handled = pushHandler.handle(
pushData = pushData, pushData = pushData,
providerInfo = "${UnifiedPushConfig.NAME} - $instance", providerInfo = "${UnifiedPushConfig.NAME} - $instance",
) )
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long. // If we failed to handle the push, we should stop the foreground service early to avoid keeping the device awake for too long.
if (!handled) { if (!handled) {
pushHandlingWakeLock.unlock() fetchPushForegroundServiceManager.stop()
} }
} }
} }

View file

@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SECRET import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager
import io.element.android.libraries.push.test.test.FakePushHandler import io.element.android.libraries.push.test.test.FakePushHandler
import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
@ -39,7 +39,6 @@ import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.data.PublicKeySet import org.unifiedpush.android.connector.data.PublicKeySet
import org.unifiedpush.android.connector.data.PushEndpoint import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage import org.unifiedpush.android.connector.data.PushMessage
import kotlin.time.Duration
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class VectorUnifiedPushMessagingReceiverTest { class VectorUnifiedPushMessagingReceiverTest {
@ -106,13 +105,13 @@ class VectorUnifiedPushMessagingReceiverTest {
fun `pushHandler returning true locks the wake lock but does not unlock it so it continues to run`() = runTest { fun `pushHandler returning true locks the wake lock but does not unlock it so it continues to run`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context val context = InstrumentationRegistry.getInstrumentation().context
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> true } val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> true }
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> } val lockLambda = lambdaRecorder<Boolean> { true }
val unlockLambda = lambdaRecorder<Unit> { } val unlockLambda = lambdaRecorder<Boolean> { true }
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
pushHandler = FakePushHandler( pushHandler = FakePushHandler(
handleResult = pushHandlerResult handleResult = pushHandlerResult
), ),
pushHandlingWakeLock = FakePushHandlingWakeLock( pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
lock = lockLambda, lock = lockLambda,
unlock = unlockLambda, unlock = unlockLambda,
), ),
@ -133,13 +132,13 @@ class VectorUnifiedPushMessagingReceiverTest {
fun `pushHandler returning false locks and unlocks the wakelock early`() = runTest { fun `pushHandler returning false locks and unlocks the wakelock early`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context val context = InstrumentationRegistry.getInstrumentation().context
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> false } val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> false }
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> } val lockLambda = lambdaRecorder<Boolean> { true }
val unlockLambda = lambdaRecorder<Unit> { } val unlockLambda = lambdaRecorder<Boolean> { true }
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
pushHandler = FakePushHandler( pushHandler = FakePushHandler(
handleResult = pushHandlerResult handleResult = pushHandlerResult
), ),
pushHandlingWakeLock = FakePushHandlingWakeLock( pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
lock = lockLambda, lock = lockLambda,
unlock = unlockLambda, unlock = unlockLambda,
), ),
@ -264,7 +263,7 @@ class VectorUnifiedPushMessagingReceiverTest {
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(), unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(), endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() }, removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(), pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
): VectorUnifiedPushMessagingReceiver { ): VectorUnifiedPushMessagingReceiver {
return VectorUnifiedPushMessagingReceiver().apply { return VectorUnifiedPushMessagingReceiver().apply {
this.pushParser = unifiedPushParser this.pushParser = unifiedPushParser
@ -277,7 +276,7 @@ class VectorUnifiedPushMessagingReceiverTest {
this.removedGatewayHandler = removedGatewayHandler this.removedGatewayHandler = removedGatewayHandler
this.endpointRegistrationHandler = endpointRegistrationHandler this.endpointRegistrationHandler = endpointRegistrationHandler
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
this.pushHandlingWakeLock = pushHandlingWakeLock this.fetchPushForegroundServiceManager = pushHandlingWakeLock
} }
} }
} }