From 96e2f882a213451ba067a902f84997d67b6d9047 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 17 Mar 2026 14:24:26 +0100 Subject: [PATCH] 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 --- .../impl/DefaultNetworkMonitor.kt | 5 +- .../push/api/push/PushHandlingWakeLock.kt | 26 ++++ .../push/impl/src/main/AndroidManifest.xml | 5 + .../libraries/push/impl/di/PushBindings.kt | 17 +++ .../channels/NotificationChannels.kt | 2 + .../push/impl/push/DefaultPushHandler.kt | 11 +- .../impl/push/DefaultPushHandlingWakeLock.kt | 45 +++++++ .../impl/push/FetchPushForegroundService.kt | 115 ++++++++++++++++++ .../FetchPendingNotificationsWorker.kt | 8 +- .../push/impl/push/DefaultPushHandlerTest.kt | 5 + ...cPendingNotificationsRequestBuilderTest.kt | 2 + .../FetchPendingNotificationWorkerTest.kt | 3 + .../test/push/FakePushHandlingWakeLock.kt | 24 ++++ .../pushproviders/firebase/build.gradle.kts | 1 + .../VectorFirebaseMessagingService.kt | 6 + .../VectorFirebaseMessagingServiceTest.kt | 3 + .../VectorUnifiedPushMessagingReceiver.kt | 10 +- .../VectorUnifiedPushMessagingReceiverTest.kt | 5 +- 18 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt create mode 100644 libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt 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 } } }