diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt
index 949db720ce..7cffa057bc 100644
--- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt
+++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt
@@ -15,6 +15,7 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
+import android.os.Build
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
@@ -83,7 +84,9 @@ class DefaultNetworkMonitor(
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
// (according to Google), which is a common case in air-gapped environments.
- isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ }
}
}
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
new file mode 100644
index 0000000000..2b19ed9225
--- /dev/null
+++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt
@@ -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()
+}
diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml
index a15bb34715..32caefa8f5 100644
--- a/libraries/push/impl/src/main/AndroidManifest.xml
+++ b/libraries/push/impl/src/main/AndroidManifest.xml
@@ -11,6 +11,11 @@
+
+
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt
new file mode 100644
index 0000000000..49b2c43bc7
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt
@@ -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)
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt
index 120d48cf4b..dc009c0b7d 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt
@@ -58,6 +58,8 @@ interface NotificationChannels {
* Get the channel for test notifications.
*/
fun getChannelIdForTest(): String
+
+ fun getSilentChannelId(): String = SILENT_NOTIFICATION_CHANNEL_ID
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
index 0c43480bd6..c39caa5781 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
@@ -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.AnalyticsService
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.launch
import timber.log.Timber
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@@ -71,7 +74,12 @@ class DefaultPushHandler(
if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## pushData: $pushData")
}
- incrementPushDataStore.incrementPushCounter()
+
+ // 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()
+ }
+
// Diagnostic Push
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
pushHistoryService.onDiagnosticPush(providerInfo)
@@ -130,6 +138,7 @@ class DefaultPushHandler(
Timber.d("Queueing notification: $pushRequest")
pushHistoryService.insertOrUpdatePushRequest(pushRequest)
+ Timber.d("Queueing notification finished")
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
Timber.d("No pending worker for push notifications found")
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
new file mode 100644
index 0000000000..1388aac19b
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt
@@ -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)
+ }
+ }
+}
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
new file mode 100644
index 0000000000..f0f4ebb2de
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt
@@ -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().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)
+ }
+ }
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt
index a14c20d53b..ec57582529 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt
@@ -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.exception.ClientException
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.history.PushHistoryService
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
@@ -57,6 +58,7 @@ class FetchPendingNotificationsWorker(
private val resultProcessor: NotificationResultProcessor,
private val analyticsService: AnalyticsService,
private val systemClock: SystemClock,
+ private val pushHandlingWakeLock: PushHandlingWakeLock,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
Timber.d("FetchNotificationsWorker started")
@@ -65,6 +67,8 @@ class FetchPendingNotificationsWorker(
inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId)
}.getOrNull() ?: return Result.failure()
+ pushHandlingWakeLock.unlock()
+
// Fetch pending requests in the last 24 hours
val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days)
val requests = pushHistoryService.getPendingPushRequests(sessionId, fetchSince).getOrNull() ?: return Result.failure()
@@ -101,9 +105,9 @@ class FetchPendingNotificationsWorker(
results
},
- onFailure = {
+ onFailure = { throwable ->
// 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)
}
)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt
index 1f7f64bf90..cc6e4674f9 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt
@@ -42,6 +42,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds
@@ -173,6 +174,10 @@ class DefaultPushHandlerTest {
workManagerScheduler = workManagerScheduler,
)
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
+
+ // Give it enough time to increment the push counter
+ runCurrent()
+
submitWorkLambda.assertions()
.isNeverCalled()
incrementPushCounterResult.assertions()
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt
index a8daf5bcff..796d5d192f 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt
@@ -24,7 +24,9 @@ import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvid
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+@Config(sdk = [33])
@RunWith(AndroidJUnit4::class)
class DefaultSyncPendingNotificationsRequestBuilderTest {
@Test
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt
index b39c3d3f05..8168019a99 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt
@@ -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.model.ResolvedPushEvent
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.di.MetroWorkerFactory
import io.element.android.services.analytics.test.FakeAnalyticsService
@@ -238,6 +239,7 @@ class FetchPendingNotificationWorkerTest {
pushHistoryService: FakePushHistoryService = FakePushHistoryService(),
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(),
systemClock: FakeSystemClock = FakeSystemClock(),
+ pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
) = FetchPendingNotificationsWorker(
params = createWorkerParams(workDataOf("session_id" to input)),
context = InstrumentationRegistry.getInstrumentation().context,
@@ -248,6 +250,7 @@ class FetchPendingNotificationWorkerTest {
pushHistoryService = pushHistoryService,
resultProcessor = resultProcessor,
systemClock = systemClock,
+ pushHandlingWakeLock = pushHandlingWakeLock,
)
private fun TestScope.createWorkerParams(
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
new file mode 100644
index 0000000000..925581db9b
--- /dev/null
+++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt
@@ -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()
+ }
+}
diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts
index ee5bd942ff..49ce7135d5 100644
--- a/libraries/pushproviders/firebase/build.gradle.kts
+++ b/libraries/pushproviders/firebase/build.gradle.kts
@@ -56,6 +56,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.push.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.troubleshoot.api)
implementation(projects.services.toolbox.api)
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt
index 532ee8a4a1..6c479b92c1 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt
@@ -14,6 +14,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -25,6 +26,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
@Inject lateinit var pushParser: FirebasePushParser
@Inject lateinit var pushHandler: PushHandler
+ @Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
@AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope
@@ -42,6 +44,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
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 {
val pushData = pushParser.parse(message.data)
if (pushData == null) {
diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt
index 1140d6f45e..81bf19e666 100644
--- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt
+++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt
@@ -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.A_ROOM_ID
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.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler
@@ -93,12 +94,14 @@ class VectorFirebaseMessagingServiceTest {
private fun TestScope.createVectorFirebaseMessagingService(
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
pushHandler: PushHandler = FakePushHandler(),
+ pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
): VectorFirebaseMessagingService {
return VectorFirebaseMessagingService().apply {
this.firebaseNewTokenHandler = firebaseNewTokenHandler
this.pushParser = FirebasePushParser()
this.pushHandler = pushHandler
this.coroutineScope = this@createVectorFirebaseMessagingService
+ this.pushHandlingWakeLock = pushHandlingWakeLock
}
}
}
diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt
index 05f6969fc5..59a2f654dd 100644
--- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt
+++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt
@@ -14,6 +14,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
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.unifiedpush.registration.EndpointRegistrationHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
@@ -37,12 +38,16 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
+ @Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
@AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {
- context.bindings().inject(this)
+ // We only need to inject this object once
+ if (!this::pushParser.isInitialized) {
+ context.bindings().inject(this)
+ }
super.onReceive(context, intent)
}
@@ -54,6 +59,9 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
* @param instance connection, for multi-account
*/
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}")
coroutineScope.launch {
val pushData = pushParser.parse(message.content, instance)
diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt
index f10f6430f0..10de44d4ff 100644
--- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt
+++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt
@@ -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.A_ROOM_ID
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.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler
@@ -44,7 +45,7 @@ class VectorUnifiedPushMessagingReceiverTest {
@Test
fun `onReceive does the binding`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
- val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver()
+ val vectorUnifiedPushMessagingReceiver = VectorUnifiedPushMessagingReceiver()
// The binding is not found in the test env.
assertThrows(IllegalStateException::class.java) {
vectorUnifiedPushMessagingReceiver.onReceive(context, Intent())
@@ -208,6 +209,7 @@ class VectorUnifiedPushMessagingReceiverTest {
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
+ pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
): VectorUnifiedPushMessagingReceiver {
return VectorUnifiedPushMessagingReceiver().apply {
this.pushParser = unifiedPushParser
@@ -220,6 +222,7 @@ class VectorUnifiedPushMessagingReceiverTest {
this.removedGatewayHandler = removedGatewayHandler
this.endpointRegistrationHandler = endpointRegistrationHandler
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
+ this.pushHandlingWakeLock = pushHandlingWakeLock
}
}
}