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 } } }