Add a foreground service with a wakelock for fetching push notifications (#6321)

* Create `PushHandlingWakeLock` to start a foreground service:

When receiving a push and scheduling the notification fetching, several problems can happen:

1. Some async operation is waiting for a timeout and it takes way longer than that to finish (i.e. timeout of 10s but it took 30s to advance).
2. The same, but when starting new coroutines. I've seen the time between scheduling a coroutine and it running sometimes take up to 1 minute.
3. Notification fetching can be scheduled immediately, but it can take a while to actually run because the OS understands the app is now in Doze.

Having a wakelock that runs as soon as the push handling starts fixes these: it continues the previous wakelock held by either Firebase or the UnifiedPush distributor.

* Acquire the wakelock as soon as we received the pushes in both receivers

* Also release the wakelock ahead of time if possible
This commit is contained in:
Jorge Martin Espinosa
2026-03-17 14:24:26 +01:00
committed by GitHub
parent ea561d3702
commit 96e2f882a2
18 changed files with 287 additions and 6 deletions

View File

@@ -15,6 +15,7 @@ import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
@@ -83,9 +84,11 @@ class DefaultNetworkMonitor(
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) { if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
// If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet // If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet
// (according to Google), which is a common case in air-gapped environments. // (according to Google), which is a common case in air-gapped environments.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED) isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} }
} }
}
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) { if (activeNetworksCount.incrementAndGet() > 0) {

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
fun unlock()
}

View File

@@ -11,6 +11,11 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application> <application>
<service android:name=".push.FetchPushForegroundService"
android:foregroundServiceType="shortService"
android:exported="false"
android:enabled="true" />
<receiver <receiver
android:name=".notifications.TestNotificationReceiver" android:name=".notifications.TestNotificationReceiver"
android:exported="false" /> android:exported="false" />

View File

@@ -0,0 +1,17 @@
/*
* 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.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import io.element.android.libraries.push.impl.push.FetchPushForegroundService
@ContributesTo(AppScope::class)
interface PushBindings {
fun inject(fetchPushForegroundService: FetchPushForegroundService)
}

View File

@@ -58,6 +58,8 @@ interface NotificationChannels {
* Get the channel for test notifications. * Get the channel for test notifications.
*/ */
fun getChannelIdForTest(): String fun getChannelIdForTest(): String
fun getSilentChannelId(): String = SILENT_NOTIFICATION_CHANNEL_ID
} }
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)

View File

@@ -31,7 +31,10 @@ import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@@ -71,7 +74,12 @@ class DefaultPushHandler(
if (buildMeta.lowPrivacyLoggingEnabled) { if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## pushData: $pushData") Timber.tag(loggerTag.value).d("## pushData: $pushData")
} }
// Update the push counter without blocking the coroutine execution, as it is not critical to be updated before handling the push
CoroutineScope(currentCoroutineContext()).launch {
incrementPushDataStore.incrementPushCounter() incrementPushDataStore.incrementPushCounter()
}
// Diagnostic Push // Diagnostic Push
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
pushHistoryService.onDiagnosticPush(providerInfo) pushHistoryService.onDiagnosticPush(providerInfo)
@@ -130,6 +138,7 @@ class DefaultPushHandler(
Timber.d("Queueing notification: $pushRequest") Timber.d("Queueing notification: $pushRequest")
pushHistoryService.insertOrUpdatePushRequest(pushRequest) pushHistoryService.insertOrUpdatePushRequest(pushRequest)
Timber.d("Queueing notification finished")
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) { if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
Timber.d("No pending worker for push notifications found") Timber.d("No pending worker for push notifications found")

View File

@@ -0,0 +1,45 @@
/*
* 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 java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Duration
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultPushHandlingWakeLock(
@ApplicationContext private val context: Context,
) : PushHandlingWakeLock {
private val count = AtomicInteger(0)
override fun lock(time: Duration) {
Timber.d("Acquiring wakelock for push handling, starting service.")
FetchPushForegroundService.startIfNeeded(context)
count.incrementAndGet()
}
override fun unlock() {
Timber.d("Releasing wakelock used for push handling.")
FetchPushForegroundService.stop(context)
if (count.decrementAndGet() <= 0) {
Timber.d("No more wakelock needed for push handling, stopping service.")
count.set(0)
} else {
Timber.d("Wakelock still needed for push handling, restarting service | count: ${count.get()}.")
FetchPushForegroundService.startIfNeeded(context)
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.utils.CommonDrawables
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.notifications.channels.NotificationChannels
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.minutes
private const val NOTIFICATION_ID = 1001
// This kind of foreground service can only last up to 3 minutes before onTimeout is called
private val wakelockTimeout = 3.minutes.inWholeMilliseconds
/**
* Foreground service used to ensure the device stays awake while we handle the pushes and schedule and run the work to fetch the notification content.
*/
class FetchPushForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
@Inject lateinit var notificationChannels: NotificationChannels
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
@Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope
private val wakelock: PowerManager.WakeLock by lazy {
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "FetchPushService:WakeLock").apply {
setReferenceCounted(false)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
bindings<PushBindings>().inject(this)
wakelock.acquire(wakelockTimeout)
val notificationCompat = NotificationCompat.Builder(this, notificationChannels.getSilentChannelId())
.setSmallIcon(CommonDrawables.ic_notification)
.setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title))
.setProgress(0, 0, true)
.setVibrate(longArrayOf(0))
.setSound(null)
.build()
startForeground(NOTIFICATION_ID, notificationCompat)
// 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 {
delay(wakelockTimeout)
onTimeout(startId)
}
}
return START_NOT_STICKY
}
override fun stopService(intent: Intent?): Boolean {
wakelock.release()
stopForeground(STOP_FOREGROUND_REMOVE)
return super.stopService(intent)
}
override fun onTimeout(startId: Int) {
super.onTimeout(startId)
pushHandlingWakeLock.unlock()
}
companion object {
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) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
val intent = Intent(context, FetchPushForegroundService::class.java)
context.stopService(intent)
}
}
}

View File

@@ -25,6 +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.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
@@ -57,6 +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,
) : CoroutineWorker(context, params) { ) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.d("FetchNotificationsWorker started") Timber.d("FetchNotificationsWorker started")
@@ -65,6 +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()
// 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)
val requests = pushHistoryService.getPendingPushRequests(sessionId, fetchSince).getOrNull() ?: return Result.failure() val requests = pushHistoryService.getPendingPushRequests(sessionId, fetchSince).getOrNull() ?: return Result.failure()
@@ -101,9 +105,9 @@ class FetchPendingNotificationsWorker(
results results
}, },
onFailure = { onFailure = { throwable ->
// This is a failure at the fetch notification setup, not a failure for a single fetch notification operation // This is a failure at the fetch notification setup, not a failure for a single fetch notification operation
return handleSetupError(sessionId, requests, pendingAnalyticTransactions, it) return handleSetupError(sessionId, requests, pendingAnalyticTransactions, throwable)
} }
) )

View File

@@ -42,6 +42,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -173,6 +174,10 @@ class DefaultPushHandlerTest {
workManagerScheduler = workManagerScheduler, workManagerScheduler = workManagerScheduler,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
// Give it enough time to increment the push counter
runCurrent()
submitWorkLambda.assertions() submitWorkLambda.assertions()
.isNeverCalled() .isNeverCalled()
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()

View File

@@ -24,7 +24,9 @@ import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvid
import kotlinx.coroutines.test.runTest 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.annotation.Config
@Config(sdk = [33])
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DefaultSyncPendingNotificationsRequestBuilderTest { class DefaultSyncPendingNotificationsRequestBuilderTest {
@Test @Test

View File

@@ -28,6 +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.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
@@ -238,6 +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(),
) = FetchPendingNotificationsWorker( ) = FetchPendingNotificationsWorker(
params = createWorkerParams(workDataOf("session_id" to input)), params = createWorkerParams(workDataOf("session_id" to input)),
context = InstrumentationRegistry.getInstrumentation().context, context = InstrumentationRegistry.getInstrumentation().context,
@@ -248,6 +250,7 @@ class FetchPendingNotificationWorkerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
resultProcessor = resultProcessor, resultProcessor = resultProcessor,
systemClock = systemClock, systemClock = systemClock,
pushHandlingWakeLock = pushHandlingWakeLock,
) )
private fun TestScope.createWorkerParams( private fun TestScope.createWorkerParams(

View File

@@ -0,0 +1,24 @@
/*
* 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 fun unlock() {
unlock.invoke()
}
}

View File

@@ -56,6 +56,7 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.push.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.troubleshoot.api) implementation(projects.libraries.troubleshoot.api)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)

View File

@@ -14,6 +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.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
@@ -25,6 +26,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
@AppCoroutineScope @AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope @Inject lateinit var coroutineScope: CoroutineScope
@@ -42,6 +44,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) { override fun onMessageReceived(message: RemoteMessage) {
Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}") Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}")
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
pushHandlingWakeLock.lock()
coroutineScope.launch { coroutineScope.launch {
val pushData = pushParser.parse(message.data) val pushData = pushParser.parse(message.data)
if (pushData == null) { if (pushData == null) {

View File

@@ -15,6 +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.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
@@ -93,12 +94,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(),
): 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
} }
} }
} }

View File

@@ -14,6 +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.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
@@ -37,12 +38,16 @@ 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
@AppCoroutineScope @AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope @Inject lateinit var coroutineScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
// We only need to inject this object once
if (!this::pushParser.isInitialized) {
context.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this) context.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
}
super.onReceive(context, intent) super.onReceive(context, intent)
} }
@@ -54,6 +59,9 @@ 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
pushHandlingWakeLock.lock()
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}") Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
coroutineScope.launch { coroutineScope.launch {
val pushData = pushParser.parse(message.content, instance) val pushData = pushParser.parse(message.content, instance)

View File

@@ -18,6 +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.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
@@ -44,7 +45,7 @@ class VectorUnifiedPushMessagingReceiverTest {
@Test @Test
fun `onReceive does the binding`() = runTest { fun `onReceive does the binding`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context val context = InstrumentationRegistry.getInstrumentation().context
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver() val vectorUnifiedPushMessagingReceiver = VectorUnifiedPushMessagingReceiver()
// The binding is not found in the test env. // The binding is not found in the test env.
assertThrows(IllegalStateException::class.java) { assertThrows(IllegalStateException::class.java) {
vectorUnifiedPushMessagingReceiver.onReceive(context, Intent()) vectorUnifiedPushMessagingReceiver.onReceive(context, Intent())
@@ -208,6 +209,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(),
): VectorUnifiedPushMessagingReceiver { ): VectorUnifiedPushMessagingReceiver {
return VectorUnifiedPushMessagingReceiver().apply { return VectorUnifiedPushMessagingReceiver().apply {
this.pushParser = unifiedPushParser this.pushParser = unifiedPushParser
@@ -220,6 +222,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
} }
} }
} }