Add a foreground service with a wakelock for fetching push notifications (#6321)
* Create `PushHandlingWakeLock` to start a foreground service: When receiving a push and scheduling the notification fetching, several problems can happen: 1. Some async operation is waiting for a timeout and it takes way longer than that to finish (i.e. timeout of 10s but it took 30s to advance). 2. The same, but when starting new coroutines. I've seen the time between scheduling a coroutine and it running sometimes take up to 1 minute. 3. Notification fetching can be scheduled immediately, but it can take a while to actually run because the OS understands the app is now in Doze. Having a wakelock that runs as soon as the push handling starts fixes these: it continues the previous wakelock held by either Firebase or the UnifiedPush distributor. * Acquire the wakelock as soon as we received the pushes in both receivers * Also release the wakelock ahead of time if possible
This commit is contained in:
committed by
GitHub
parent
ea561d3702
commit
96e2f882a2
@@ -15,6 +15,7 @@ import android.net.ConnectivityManager
|
|||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
@@ -83,9 +84,11 @@ class DefaultNetworkMonitor(
|
|||||||
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
|
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
|
||||||
// If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet
|
// If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet
|
||||||
// (according to Google), which is a common case in air-gapped environments.
|
// (according to Google), which is a common case in air-gapped environments.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
if (activeNetworksCount.incrementAndGet() > 0) {
|
if (activeNetworksCount.incrementAndGet() > 0) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
<service android:name=".push.FetchPushForegroundService"
|
||||||
|
android:foregroundServiceType="shortService"
|
||||||
|
android:exported="false"
|
||||||
|
android:enabled="true" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".notifications.TestNotificationReceiver"
|
android:name=".notifications.TestNotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@ interface NotificationChannels {
|
|||||||
* Get the channel for test notifications.
|
* Get the channel for test notifications.
|
||||||
*/
|
*/
|
||||||
fun getChannelIdForTest(): String
|
fun getChannelIdForTest(): String
|
||||||
|
|
||||||
|
fun getSilentChannelId(): String = SILENT_NOTIFICATION_CHANNEL_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
|||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
|
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
|
||||||
@@ -71,7 +74,12 @@ class DefaultPushHandler(
|
|||||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||||
Timber.tag(loggerTag.value).d("## pushData: $pushData")
|
Timber.tag(loggerTag.value).d("## pushData: $pushData")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the push counter without blocking the coroutine execution, as it is not critical to be updated before handling the push
|
||||||
|
CoroutineScope(currentCoroutineContext()).launch {
|
||||||
incrementPushDataStore.incrementPushCounter()
|
incrementPushDataStore.incrementPushCounter()
|
||||||
|
}
|
||||||
|
|
||||||
// Diagnostic Push
|
// Diagnostic Push
|
||||||
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
|
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
|
||||||
pushHistoryService.onDiagnosticPush(providerInfo)
|
pushHistoryService.onDiagnosticPush(providerInfo)
|
||||||
@@ -130,6 +138,7 @@ class DefaultPushHandler(
|
|||||||
|
|
||||||
Timber.d("Queueing notification: $pushRequest")
|
Timber.d("Queueing notification: $pushRequest")
|
||||||
pushHistoryService.insertOrUpdatePushRequest(pushRequest)
|
pushHistoryService.insertOrUpdatePushRequest(pushRequest)
|
||||||
|
Timber.d("Queueing notification finished")
|
||||||
|
|
||||||
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
|
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
|
||||||
Timber.d("No pending worker for push notifications found")
|
Timber.d("No pending worker for push notifications found")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.push
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
|
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||||
|
import io.element.android.libraries.push.impl.di.PushBindings
|
||||||
|
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
|
||||||
|
// This kind of foreground service can only last up to 3 minutes before onTimeout is called
|
||||||
|
private val wakelockTimeout = 3.minutes.inWholeMilliseconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foreground service used to ensure the device stays awake while we handle the pushes and schedule and run the work to fetch the notification content.
|
||||||
|
*/
|
||||||
|
class FetchPushForegroundService : Service() {
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject lateinit var notificationChannels: NotificationChannels
|
||||||
|
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
||||||
|
@Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope
|
||||||
|
|
||||||
|
private val wakelock: PowerManager.WakeLock by lazy {
|
||||||
|
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "FetchPushService:WakeLock").apply {
|
||||||
|
setReferenceCounted(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
bindings<PushBindings>().inject(this)
|
||||||
|
|
||||||
|
wakelock.acquire(wakelockTimeout)
|
||||||
|
|
||||||
|
val notificationCompat = NotificationCompat.Builder(this, notificationChannels.getSilentChannelId())
|
||||||
|
.setSmallIcon(CommonDrawables.ic_notification)
|
||||||
|
.setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title))
|
||||||
|
.setProgress(0, 0, true)
|
||||||
|
.setVibrate(longArrayOf(0))
|
||||||
|
.setSound(null)
|
||||||
|
.build()
|
||||||
|
startForeground(NOTIFICATION_ID, notificationCompat)
|
||||||
|
|
||||||
|
// The timeout is not automatic before Android 15, so we need to schedule it ourselves
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
delay(wakelockTimeout)
|
||||||
|
onTimeout(startId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService(intent: Intent?): Boolean {
|
||||||
|
wakelock.release()
|
||||||
|
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
return super.stopService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimeout(startId: Int) {
|
||||||
|
super.onTimeout(startId)
|
||||||
|
|
||||||
|
pushHandlingWakeLock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun startIfNeeded(context: Context) {
|
||||||
|
// Don't start the foreground service if the device is already awake
|
||||||
|
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
if (powerManager.isInteractive) return
|
||||||
|
|
||||||
|
start(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(context: Context) {
|
||||||
|
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||||
|
context.stopService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.auth.SessionRestorationException
|
|||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||||
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
||||||
|
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||||
import io.element.android.libraries.push.impl.db.PushRequest
|
import io.element.android.libraries.push.impl.db.PushRequest
|
||||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||||
@@ -57,6 +58,7 @@ class FetchPendingNotificationsWorker(
|
|||||||
private val resultProcessor: NotificationResultProcessor,
|
private val resultProcessor: NotificationResultProcessor,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val systemClock: SystemClock,
|
private val systemClock: SystemClock,
|
||||||
|
private val pushHandlingWakeLock: PushHandlingWakeLock,
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.d("FetchNotificationsWorker started")
|
Timber.d("FetchNotificationsWorker started")
|
||||||
@@ -65,6 +67,8 @@ class FetchPendingNotificationsWorker(
|
|||||||
inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId)
|
inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId)
|
||||||
}.getOrNull() ?: return Result.failure()
|
}.getOrNull() ?: return Result.failure()
|
||||||
|
|
||||||
|
pushHandlingWakeLock.unlock()
|
||||||
|
|
||||||
// Fetch pending requests in the last 24 hours
|
// Fetch pending requests in the last 24 hours
|
||||||
val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days)
|
val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days)
|
||||||
val requests = pushHistoryService.getPendingPushRequests(sessionId, fetchSince).getOrNull() ?: return Result.failure()
|
val requests = pushHistoryService.getPendingPushRequests(sessionId, fetchSince).getOrNull() ?: return Result.failure()
|
||||||
@@ -101,9 +105,9 @@ class FetchPendingNotificationsWorker(
|
|||||||
|
|
||||||
results
|
results
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = { throwable ->
|
||||||
// This is a failure at the fetch notification setup, not a failure for a single fetch notification operation
|
// This is a failure at the fetch notification setup, not a failure for a single fetch notification operation
|
||||||
return handleSetupError(sessionId, requests, pendingAnalyticTransactions, it)
|
return handleSetupError(sessionId, requests, pendingAnalyticTransactions, throwable)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -173,6 +174,10 @@ class DefaultPushHandlerTest {
|
|||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
|
|
||||||
|
// Give it enough time to increment the push counter
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
submitWorkLambda.assertions()
|
submitWorkLambda.assertions()
|
||||||
.isNeverCalled()
|
.isNeverCalled()
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvid
|
|||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@Config(sdk = [33])
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class DefaultSyncPendingNotificationsRequestBuilderTest {
|
class DefaultSyncPendingNotificationsRequestBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import io.element.android.libraries.push.impl.notifications.FakeNotificationResu
|
|||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aPushRequest
|
import io.element.android.libraries.push.impl.notifications.fixtures.aPushRequest
|
||||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
||||||
|
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
|
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
|
||||||
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
@@ -238,6 +239,7 @@ class FetchPendingNotificationWorkerTest {
|
|||||||
pushHistoryService: FakePushHistoryService = FakePushHistoryService(),
|
pushHistoryService: FakePushHistoryService = FakePushHistoryService(),
|
||||||
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(),
|
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(),
|
||||||
systemClock: FakeSystemClock = FakeSystemClock(),
|
systemClock: FakeSystemClock = FakeSystemClock(),
|
||||||
|
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
|
||||||
) = FetchPendingNotificationsWorker(
|
) = FetchPendingNotificationsWorker(
|
||||||
params = createWorkerParams(workDataOf("session_id" to input)),
|
params = createWorkerParams(workDataOf("session_id" to input)),
|
||||||
context = InstrumentationRegistry.getInstrumentation().context,
|
context = InstrumentationRegistry.getInstrumentation().context,
|
||||||
@@ -248,6 +250,7 @@ class FetchPendingNotificationWorkerTest {
|
|||||||
pushHistoryService = pushHistoryService,
|
pushHistoryService = pushHistoryService,
|
||||||
resultProcessor = resultProcessor,
|
resultProcessor = resultProcessor,
|
||||||
systemClock = systemClock,
|
systemClock = systemClock,
|
||||||
|
pushHandlingWakeLock = pushHandlingWakeLock,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun TestScope.createWorkerParams(
|
private fun TestScope.createWorkerParams(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.push.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.troubleshoot.api)
|
implementation(projects.libraries.troubleshoot.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import dev.zacsweers.metro.Inject
|
|||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
|
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -25,6 +26,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
||||||
@Inject lateinit var pushParser: FirebasePushParser
|
@Inject lateinit var pushParser: FirebasePushParser
|
||||||
@Inject lateinit var pushHandler: PushHandler
|
@Inject lateinit var pushHandler: PushHandler
|
||||||
|
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
@Inject lateinit var coroutineScope: CoroutineScope
|
@Inject lateinit var coroutineScope: CoroutineScope
|
||||||
|
|
||||||
@@ -42,6 +44,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
|
|
||||||
override fun onMessageReceived(message: RemoteMessage) {
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}")
|
Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}")
|
||||||
|
|
||||||
|
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
|
||||||
|
pushHandlingWakeLock.lock()
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val pushData = pushParser.parse(message.data)
|
val pushData = pushParser.parse(message.data)
|
||||||
if (pushData == null) {
|
if (pushData == null) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.google.firebase.messaging.RemoteMessage
|
|||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SECRET
|
import io.element.android.libraries.matrix.test.A_SECRET
|
||||||
|
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock
|
||||||
import io.element.android.libraries.push.test.test.FakePushHandler
|
import io.element.android.libraries.push.test.test.FakePushHandler
|
||||||
import io.element.android.libraries.pushproviders.api.PushData
|
import io.element.android.libraries.pushproviders.api.PushData
|
||||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||||
@@ -93,12 +94,14 @@ class VectorFirebaseMessagingServiceTest {
|
|||||||
private fun TestScope.createVectorFirebaseMessagingService(
|
private fun TestScope.createVectorFirebaseMessagingService(
|
||||||
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
|
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
|
||||||
pushHandler: PushHandler = FakePushHandler(),
|
pushHandler: PushHandler = FakePushHandler(),
|
||||||
|
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
|
||||||
): VectorFirebaseMessagingService {
|
): VectorFirebaseMessagingService {
|
||||||
return VectorFirebaseMessagingService().apply {
|
return VectorFirebaseMessagingService().apply {
|
||||||
this.firebaseNewTokenHandler = firebaseNewTokenHandler
|
this.firebaseNewTokenHandler = firebaseNewTokenHandler
|
||||||
this.pushParser = FirebasePushParser()
|
this.pushParser = FirebasePushParser()
|
||||||
this.pushHandler = pushHandler
|
this.pushHandler = pushHandler
|
||||||
this.coroutineScope = this@createVectorFirebaseMessagingService
|
this.coroutineScope = this@createVectorFirebaseMessagingService
|
||||||
|
this.pushHandlingWakeLock = pushHandlingWakeLock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import dev.zacsweers.metro.Inject
|
|||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
|
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
||||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
|
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
|
||||||
@@ -37,12 +38,16 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||||||
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
||||||
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
|
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
|
||||||
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
|
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
|
||||||
|
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
||||||
|
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
@Inject lateinit var coroutineScope: CoroutineScope
|
@Inject lateinit var coroutineScope: CoroutineScope
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// We only need to inject this object once
|
||||||
|
if (!this::pushParser.isInitialized) {
|
||||||
context.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
|
context.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
|
||||||
|
}
|
||||||
super.onReceive(context, intent)
|
super.onReceive(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +59,9 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||||||
* @param instance connection, for multi-account
|
* @param instance connection, for multi-account
|
||||||
*/
|
*/
|
||||||
override fun onMessage(context: Context, message: PushMessage, instance: String) {
|
override fun onMessage(context: Context, message: PushMessage, instance: String) {
|
||||||
|
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
|
||||||
|
pushHandlingWakeLock.lock()
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val pushData = pushParser.parse(message.content, instance)
|
val pushData = pushParser.parse(message.content, instance)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
|||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SECRET
|
import io.element.android.libraries.matrix.test.A_SECRET
|
||||||
|
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock
|
||||||
import io.element.android.libraries.push.test.test.FakePushHandler
|
import io.element.android.libraries.push.test.test.FakePushHandler
|
||||||
import io.element.android.libraries.pushproviders.api.PushData
|
import io.element.android.libraries.pushproviders.api.PushData
|
||||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||||
@@ -44,7 +45,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `onReceive does the binding`() = runTest {
|
fun `onReceive does the binding`() = runTest {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().context
|
val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver()
|
val vectorUnifiedPushMessagingReceiver = VectorUnifiedPushMessagingReceiver()
|
||||||
// The binding is not found in the test env.
|
// The binding is not found in the test env.
|
||||||
assertThrows(IllegalStateException::class.java) {
|
assertThrows(IllegalStateException::class.java) {
|
||||||
vectorUnifiedPushMessagingReceiver.onReceive(context, Intent())
|
vectorUnifiedPushMessagingReceiver.onReceive(context, Intent())
|
||||||
@@ -208,6 +209,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||||||
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
|
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
|
||||||
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
||||||
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
|
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
|
||||||
|
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
|
||||||
): VectorUnifiedPushMessagingReceiver {
|
): VectorUnifiedPushMessagingReceiver {
|
||||||
return VectorUnifiedPushMessagingReceiver().apply {
|
return VectorUnifiedPushMessagingReceiver().apply {
|
||||||
this.pushParser = unifiedPushParser
|
this.pushParser = unifiedPushParser
|
||||||
@@ -220,6 +222,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||||||
this.removedGatewayHandler = removedGatewayHandler
|
this.removedGatewayHandler = removedGatewayHandler
|
||||||
this.endpointRegistrationHandler = endpointRegistrationHandler
|
this.endpointRegistrationHandler = endpointRegistrationHandler
|
||||||
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
|
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
|
||||||
|
this.pushHandlingWakeLock = pushHandlingWakeLock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user