Cleanup FetchPushForegroundService (#6577)
* Rename `PushHandlingWakeLock` to `FetchPushForegroundServiceManager`. Move the start/stop logic from `FetchPushForegroundService.Companion` to it. * Add more tests using Robolectric. * Remove `FeatureFlags.SyncNotificationsWithWorkManager` and associated code: this should have been removed in one of the previous refactors, since we don't have the 2 ways to sync notifications anymore, everything uses the `WorkManager` --------- Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
parent
e14d3bb312
commit
db100281d8
17 changed files with 329 additions and 215 deletions
|
|
@ -104,14 +104,6 @@ enum class FeatureFlags(
|
||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
SyncNotificationsWithWorkManager(
|
|
||||||
key = "feature.sync_notifications_with_workmanager",
|
|
||||||
title = "Sync notifications with WorkManager",
|
|
||||||
description = "Use WorkManager to schedule notification sync tasks when a push is received." +
|
|
||||||
" This should improve reliability and battery usage.",
|
|
||||||
defaultValue = { true },
|
|
||||||
isFinished = false,
|
|
||||||
),
|
|
||||||
QrCodeLogin(
|
QrCodeLogin(
|
||||||
key = "feature.qr_code_login",
|
key = "feature.qr_code_login",
|
||||||
title = "QR Code Login",
|
title = "QR Code Login",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper to manage the foreground service used to keep the device awake while we schedule and wait for the work to fetch the notification content to run.
|
||||||
|
*/
|
||||||
|
interface FetchPushForegroundServiceManager {
|
||||||
|
/**
|
||||||
|
* Start the foreground service to acquire the wakelock. If the device is already awake, this method does nothing.
|
||||||
|
*
|
||||||
|
* @return true if the service was started, false otherwise (e.g. if the device was already awake or if starting the service failed).
|
||||||
|
*/
|
||||||
|
fun start(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the foreground service to release the wakelock. If the service is not running, this method does nothing.
|
||||||
|
*
|
||||||
|
* @return true if the service was stopped, false otherwise (e.g. if the service was not running or if stopping the service failed).
|
||||||
|
*/
|
||||||
|
suspend fun stop(): Boolean
|
||||||
|
}
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
suspend fun unlock()
|
|
||||||
}
|
|
||||||
|
|
@ -13,8 +13,6 @@ import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
@ -32,7 +30,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
||||||
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
|
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
|
||||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||||
import io.element.android.libraries.push.impl.push.OnRedactedEventReceived
|
import io.element.android.libraries.push.impl.push.OnRedactedEventReceived
|
||||||
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
|
||||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -60,8 +57,6 @@ class DefaultNotificationResultProcessor(
|
||||||
private val userPushStoreFactory: UserPushStoreFactory,
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
private val onRedactedEventReceived: OnRedactedEventReceived,
|
private val onRedactedEventReceived: OnRedactedEventReceived,
|
||||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||||
private val featureFlagService: FeatureFlagService,
|
|
||||||
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
|
||||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val notificationChannels: NotificationChannels,
|
private val notificationChannels: NotificationChannels,
|
||||||
@AppCoroutineScope private val coroutineScope: CoroutineScope,
|
@AppCoroutineScope private val coroutineScope: CoroutineScope,
|
||||||
|
|
@ -215,10 +210,6 @@ class DefaultNotificationResultProcessor(
|
||||||
if (nonRingingCallEvents.isNotEmpty()) {
|
if (nonRingingCallEvents.isNotEmpty()) {
|
||||||
onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents)
|
onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncNotificationsWithWorkManager)) {
|
|
||||||
syncOnNotifiableEvent(results.keys.toList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
|
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
|
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
class DefaultFetchPushForegroundServiceManager(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : FetchPushForegroundServiceManager {
|
||||||
|
private val stopMutex = Mutex()
|
||||||
|
|
||||||
|
override fun start(): Boolean {
|
||||||
|
Timber.d("Acquiring wakelock for push handling, starting service.")
|
||||||
|
|
||||||
|
// Don't start the foreground service if the device is already awake
|
||||||
|
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
if (powerManager.isInteractive) {
|
||||||
|
Timber.d("Device is already in an interactive state, no need to start FetchPushForegroundService")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
runCatchingExceptions { ContextCompat.startForegroundService(context, intent) }
|
||||||
|
.onFailure { throwable ->
|
||||||
|
Timber.e(throwable, "Failed to start FetchPushForegroundService, notifications may take longer than usual to sync")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stop(): Boolean {
|
||||||
|
Timber.d("Releasing wakelock used for push handling, stopping service.")
|
||||||
|
return stopMutex.withLock {
|
||||||
|
val runningServiceInfo = getRunningServiceInfo(context)
|
||||||
|
if (runningServiceInfo != null) {
|
||||||
|
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||||
|
// If it's still not running in foreground, it means the service is still starting,
|
||||||
|
// so we delay the stop to give it time to start and be set as foreground, otherwise we can crash
|
||||||
|
// with `ForegroundServiceDidNotStartInTimeException`.
|
||||||
|
var isInForeground = runningServiceInfo.foreground
|
||||||
|
withTimeoutOrNull(5.seconds) {
|
||||||
|
while (!isInForeground) {
|
||||||
|
delay(50)
|
||||||
|
val updatedServiceInfo = getRunningServiceInfo(context)
|
||||||
|
if (updatedServiceInfo == null) {
|
||||||
|
Timber.d("FetchPushForegroundService is no longer running, no need to stop it.")
|
||||||
|
return@withTimeoutOrNull
|
||||||
|
}
|
||||||
|
isInForeground = updatedServiceInfo.foreground == true
|
||||||
|
}
|
||||||
|
} ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.")
|
||||||
|
context.stopService(intent)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? {
|
||||||
|
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
return activityManager.getRunningServices(Int.MAX_VALUE)
|
||||||
|
.firstOrNull { it.service.className == FetchPushForegroundService::class.java.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 kotlin.time.Duration
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
@SingleIn(AppScope::class)
|
|
||||||
class DefaultPushHandlingWakeLock(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
) : PushHandlingWakeLock {
|
|
||||||
override fun lock(time: Duration) {
|
|
||||||
Timber.d("Acquiring wakelock for push handling, starting service.")
|
|
||||||
FetchPushForegroundService.startIfNeeded(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun unlock() {
|
|
||||||
Timber.d("Releasing wakelock used for push handling.")
|
|
||||||
FetchPushForegroundService.stop(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,32 +7,27 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.push
|
package io.element.android.libraries.push.impl.push
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
import dev.zacsweers.metro.Inject
|
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.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
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.push.impl.di.PushBindings
|
import io.element.android.libraries.push.impl.di.PushBindings
|
||||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
private const val NOTIFICATION_ID = 1001
|
private const val NOTIFICATION_ID = 1001
|
||||||
|
|
||||||
|
|
@ -48,7 +43,6 @@ class FetchPushForegroundService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var notificationChannels: NotificationChannels
|
@Inject lateinit var notificationChannels: NotificationChannels
|
||||||
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
|
||||||
@Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope
|
@Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope
|
||||||
|
|
||||||
private val wakelock: PowerManager.WakeLock by lazy {
|
private val wakelock: PowerManager.WakeLock by lazy {
|
||||||
|
|
@ -78,8 +72,13 @@ class FetchPushForegroundService : Service() {
|
||||||
// Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs.
|
// Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs.
|
||||||
// In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service
|
// In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service
|
||||||
// from running in foreground later.
|
// from running in foreground later.
|
||||||
|
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
startForeground(NOTIFICATION_ID, notificationCompat)
|
ServiceCompat.startForeground(this, NOTIFICATION_ID, notificationCompat, serviceType)
|
||||||
}
|
}
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
isOnForeground = true
|
isOnForeground = true
|
||||||
|
|
@ -116,7 +115,7 @@ class FetchPushForegroundService : Service() {
|
||||||
override fun stopService(intent: Intent?): Boolean {
|
override fun stopService(intent: Intent?): Boolean {
|
||||||
if (isOnForeground) {
|
if (isOnForeground) {
|
||||||
wakelock.release()
|
wakelock.release()
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.stopService(intent)
|
return super.stopService(intent)
|
||||||
|
|
@ -131,64 +130,7 @@ class FetchPushForegroundService : Service() {
|
||||||
Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground")
|
Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground")
|
||||||
if (isOnForeground) {
|
if (isOnForeground) {
|
||||||
Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService")
|
Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService")
|
||||||
coroutineScope.launch { pushHandlingWakeLock.unlock() }
|
stopSelf()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val stopMutex = Mutex()
|
|
||||||
|
|
||||||
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) {
|
|
||||||
runCatchingExceptions { context.startForegroundService(intent) }
|
|
||||||
.onFailure { throwable ->
|
|
||||||
Timber.e(
|
|
||||||
throwable,
|
|
||||||
"Failed to start FetchPushForegroundService, notifications may take longer than usual to sync"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun stop(context: Context) = stopMutex.withLock {
|
|
||||||
val runningServiceInfo = getRunningServiceInfo(context)
|
|
||||||
if (runningServiceInfo != null) {
|
|
||||||
val intent = Intent(context, FetchPushForegroundService::class.java)
|
|
||||||
// If it's still not running in foreground, it means the service is still starting,
|
|
||||||
// so we delay the stop to give it time to start and be set as foreground, otherwise we can crash
|
|
||||||
// with `ForegroundServiceDidNotStartInTimeException`.
|
|
||||||
var isInForeground = runningServiceInfo.foreground
|
|
||||||
withTimeoutOrNull(5.seconds) {
|
|
||||||
while (!isInForeground) {
|
|
||||||
delay(50)
|
|
||||||
val updatedServiceInfo = getRunningServiceInfo(context)
|
|
||||||
if (updatedServiceInfo == null) {
|
|
||||||
Timber.d("FetchPushForegroundService is no longer running, no need to stop it.")
|
|
||||||
return@withTimeoutOrNull
|
|
||||||
}
|
|
||||||
isInForeground = updatedServiceInfo.foreground == true
|
|
||||||
}
|
|
||||||
} ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.")
|
|
||||||
context.stopService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? {
|
|
||||||
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
|
||||||
return activityManager.getRunningServices(Int.MAX_VALUE)
|
|
||||||
.firstOrNull { it.service.className == FetchPushForegroundService::class.java.name }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +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.api.push.FetchPushForegroundServiceManager
|
||||||
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
|
||||||
|
|
@ -58,7 +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,
|
private val fetchPushForegroundServiceManager: FetchPushForegroundServiceManager,
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.d("FetchNotificationsWorker started")
|
Timber.d("FetchNotificationsWorker started")
|
||||||
|
|
@ -67,7 +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()
|
// We can stop the foreground service and unlock the wakelock, since the work is now running and the device should be kept awake
|
||||||
|
fetchPushForegroundServiceManager.stop()
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.test.FakeElementCallEntryPoint
|
import io.element.android.features.call.test.FakeElementCallEntryPoint
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
@ -34,7 +33,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
||||||
import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore
|
import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore
|
||||||
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
||||||
import io.element.android.libraries.push.impl.push.FakeOnRedactedEventReceived
|
import io.element.android.libraries.push.impl.push.FakeOnRedactedEventReceived
|
||||||
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
|
||||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
|
@ -289,8 +287,6 @@ class DefaultNotificationResultProcessorTest {
|
||||||
userPushStoreFactory: FakeUserPushStoreFactory = FakeUserPushStoreFactory(),
|
userPushStoreFactory: FakeUserPushStoreFactory = FakeUserPushStoreFactory(),
|
||||||
onRedactedEventReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = {},
|
onRedactedEventReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = {},
|
||||||
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = {},
|
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = {},
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
|
||||||
syncOnNotifiableEvent: SyncOnNotifiableEvent = {},
|
|
||||||
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
||||||
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
||||||
coroutineScope: CoroutineScope = backgroundScope,
|
coroutineScope: CoroutineScope = backgroundScope,
|
||||||
|
|
@ -301,8 +297,6 @@ class DefaultNotificationResultProcessorTest {
|
||||||
userPushStoreFactory = userPushStoreFactory,
|
userPushStoreFactory = userPushStoreFactory,
|
||||||
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
|
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
|
||||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
|
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
|
||||||
featureFlagService = featureFlagService,
|
|
||||||
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
|
||||||
elementCallEntryPoint = elementCallEntryPoint,
|
elementCallEntryPoint = elementCallEntryPoint,
|
||||||
notificationChannels = notificationChannels,
|
notificationChannels = notificationChannels,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* 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.ActivityManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
|
import android.content.Context.POWER_SERVICE
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Shadows
|
||||||
|
import org.robolectric.shadows.ShadowActivityManager
|
||||||
|
import org.robolectric.shadows.ShadowPowerManager
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DefaultFetchPushForegroundServiceManagerTest {
|
||||||
|
@Test
|
||||||
|
fun `start should start the service if the device is not interactive`() {
|
||||||
|
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||||
|
|
||||||
|
getShadowPowerManager().turnScreenOn(false)
|
||||||
|
|
||||||
|
assertThat(manager.start()).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `start won't start the service if the device is interactive`() {
|
||||||
|
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||||
|
|
||||||
|
getShadowPowerManager().turnScreenOn(true)
|
||||||
|
|
||||||
|
assertThat(manager.start()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stop will stop the service if it's running`() = runTest {
|
||||||
|
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||||
|
|
||||||
|
// Start the service first
|
||||||
|
getShadowPowerManager().turnScreenOn(false)
|
||||||
|
manager.start()
|
||||||
|
|
||||||
|
getShadowActivityManager().setServices(
|
||||||
|
listOf(
|
||||||
|
ActivityManager.RunningServiceInfo().apply {
|
||||||
|
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||||
|
foreground = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(manager.stop()).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stop will eventually stop the service once it's on foreground`() = runTest {
|
||||||
|
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||||
|
|
||||||
|
// Start the service first
|
||||||
|
getShadowPowerManager().turnScreenOn(false)
|
||||||
|
manager.start()
|
||||||
|
|
||||||
|
// The service is started, but not yet in foreground
|
||||||
|
getShadowActivityManager().setServices(
|
||||||
|
listOf(
|
||||||
|
ActivityManager.RunningServiceInfo().apply {
|
||||||
|
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||||
|
foreground = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// We call stop, which won't stop the service yet since it's not in foreground
|
||||||
|
val future = async { manager.stop() }
|
||||||
|
|
||||||
|
// Then we set the service as running in foreground, which should allow the stop to complete
|
||||||
|
getShadowActivityManager().setServices(
|
||||||
|
listOf(
|
||||||
|
ActivityManager.RunningServiceInfo().apply {
|
||||||
|
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||||
|
foreground = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val stopped = withTimeout(5.seconds) { future.await() }
|
||||||
|
assertThat(stopped).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stop will not stop the service if it's stopped`() = runTest {
|
||||||
|
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||||
|
|
||||||
|
// Set some fake running service data, even if the service is not really running
|
||||||
|
getShadowActivityManager().setServices(
|
||||||
|
listOf(
|
||||||
|
ActivityManager.RunningServiceInfo().apply {
|
||||||
|
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||||
|
foreground = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Since the service was not really running, it was not stopped
|
||||||
|
assertThat(manager.stop()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDefaultFetchPushForegroundServiceManager() = DefaultFetchPushForegroundServiceManager(
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().context,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getShadowPowerManager(): ShadowPowerManager {
|
||||||
|
val powerManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(POWER_SERVICE) as PowerManager
|
||||||
|
return Shadows.shadowOf(powerManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShadowActivityManager(): ShadowActivityManager {
|
||||||
|
val activityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
return Shadows.shadowOf(activityManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,7 +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.push.test.push.FakeFetchPushForegroundServiceManager
|
||||||
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
|
||||||
|
|
@ -239,7 +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(),
|
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||||
) = FetchPendingNotificationsWorker(
|
) = FetchPendingNotificationsWorker(
|
||||||
params = createWorkerParams(workDataOf("session_id" to input)),
|
params = createWorkerParams(workDataOf("session_id" to input)),
|
||||||
context = InstrumentationRegistry.getInstrumentation().context,
|
context = InstrumentationRegistry.getInstrumentation().context,
|
||||||
|
|
@ -250,7 +250,7 @@ class FetchPendingNotificationWorkerTest {
|
||||||
pushHistoryService = pushHistoryService,
|
pushHistoryService = pushHistoryService,
|
||||||
resultProcessor = resultProcessor,
|
resultProcessor = resultProcessor,
|
||||||
systemClock = systemClock,
|
systemClock = systemClock,
|
||||||
pushHandlingWakeLock = pushHandlingWakeLock,
|
fetchPushForegroundServiceManager = pushHandlingWakeLock,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun TestScope.createWorkerParams(
|
private fun TestScope.createWorkerParams(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.FetchPushForegroundServiceManager
|
||||||
|
|
||||||
|
class FakeFetchPushForegroundServiceManager(
|
||||||
|
private val lock: () -> Boolean = { true },
|
||||||
|
private val unlock: () -> Boolean = { true },
|
||||||
|
) : FetchPushForegroundServiceManager {
|
||||||
|
override fun start(): Boolean {
|
||||||
|
return lock.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stop(): Boolean {
|
||||||
|
return unlock.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 suspend fun unlock() {
|
|
||||||
unlock.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,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.push.api.push.FetchPushForegroundServiceManager
|
||||||
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
|
||||||
|
|
@ -27,7 +27,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
|
@Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
@Inject lateinit var coroutineScope: CoroutineScope
|
@Inject lateinit var coroutineScope: CoroutineScope
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
val isHighPriority = message.priority == PRIORITY_HIGH
|
val isHighPriority = message.priority == PRIORITY_HIGH
|
||||||
if (isHighPriority) {
|
if (isHighPriority) {
|
||||||
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
|
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
|
||||||
pushHandlingWakeLock.lock()
|
fetchPushForegroundServiceManager.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -63,7 +63,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (isHighPriority) {
|
if (isHighPriority) {
|
||||||
pushHandlingWakeLock.unlock()
|
fetchPushForegroundServiceManager.stop()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val handled = pushHandler.handle(
|
val handled = pushHandler.handle(
|
||||||
|
|
@ -73,7 +73,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
|
|
||||||
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long.
|
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long.
|
||||||
if (!handled && isHighPriority) {
|
if (!handled && isHighPriority) {
|
||||||
pushHandlingWakeLock.unlock()
|
fetchPushForegroundServiceManager.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +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.push.FakeFetchPushForegroundServiceManager
|
||||||
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
|
||||||
|
|
@ -29,7 +29,6 @@ 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.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class VectorFirebaseMessagingServiceTest {
|
class VectorFirebaseMessagingServiceTest {
|
||||||
|
|
@ -81,11 +80,11 @@ class VectorFirebaseMessagingServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test pushHandler returning true locks and does not unlock the wakelock so it continues running`() = runTest {
|
fun `test pushHandler returning true locks and does not unlock the wakelock so it continues running`() = runTest {
|
||||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val unlockLambda = lambdaRecorder<Unit> { }
|
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||||
pushHandler = FakePushHandler(handleResult = { _, _ -> true }),
|
pushHandler = FakePushHandler(handleResult = { _, _ -> true }),
|
||||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||||
lock = lockLambda,
|
lock = lockLambda,
|
||||||
unlock = unlockLambda
|
unlock = unlockLambda
|
||||||
)
|
)
|
||||||
|
|
@ -113,11 +112,11 @@ class VectorFirebaseMessagingServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test pushHandler returning false locks and unlocks the wakelock early`() = runTest {
|
fun `test pushHandler returning false locks and unlocks the wakelock early`() = runTest {
|
||||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val unlockLambda = lambdaRecorder<Unit> { }
|
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||||
pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
|
pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
|
||||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||||
lock = lockLambda,
|
lock = lockLambda,
|
||||||
unlock = unlockLambda
|
unlock = unlockLambda
|
||||||
)
|
)
|
||||||
|
|
@ -145,11 +144,11 @@ class VectorFirebaseMessagingServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest {
|
fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest {
|
||||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val unlockLambda = lambdaRecorder<Unit> { }
|
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||||
pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
|
pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
|
||||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||||
lock = lockLambda,
|
lock = lockLambda,
|
||||||
unlock = unlockLambda
|
unlock = unlockLambda
|
||||||
)
|
)
|
||||||
|
|
@ -186,14 +185,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(),
|
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||||
): 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
|
this.fetchPushForegroundServiceManager = pushHandlingWakeLock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +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.push.api.push.FetchPushForegroundServiceManager
|
||||||
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
|
||||||
|
|
@ -38,7 +38,7 @@ 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
|
@Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
|
||||||
|
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
@Inject lateinit var coroutineScope: CoroutineScope
|
@Inject lateinit var coroutineScope: CoroutineScope
|
||||||
|
|
@ -59,8 +59,8 @@ 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
|
// Start the foreground service to ensure the device stays awake while we handle the push and schedule and run the work.
|
||||||
pushHandlingWakeLock.lock()
|
fetchPushForegroundServiceManager.start()
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -71,16 +71,16 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||||
data = String(message.content),
|
data = String(message.content),
|
||||||
)
|
)
|
||||||
pushHandlingWakeLock.unlock()
|
fetchPushForegroundServiceManager.stop()
|
||||||
} else {
|
} else {
|
||||||
val handled = pushHandler.handle(
|
val handled = pushHandler.handle(
|
||||||
pushData = pushData,
|
pushData = pushData,
|
||||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||||
)
|
)
|
||||||
|
|
||||||
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long.
|
// If we failed to handle the push, we should stop the foreground service early to avoid keeping the device awake for too long.
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
pushHandlingWakeLock.unlock()
|
fetchPushForegroundServiceManager.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +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.push.FakeFetchPushForegroundServiceManager
|
||||||
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
|
||||||
|
|
@ -39,7 +39,6 @@ import org.unifiedpush.android.connector.FailedReason
|
||||||
import org.unifiedpush.android.connector.data.PublicKeySet
|
import org.unifiedpush.android.connector.data.PublicKeySet
|
||||||
import org.unifiedpush.android.connector.data.PushEndpoint
|
import org.unifiedpush.android.connector.data.PushEndpoint
|
||||||
import org.unifiedpush.android.connector.data.PushMessage
|
import org.unifiedpush.android.connector.data.PushMessage
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class VectorUnifiedPushMessagingReceiverTest {
|
class VectorUnifiedPushMessagingReceiverTest {
|
||||||
|
|
@ -106,13 +105,13 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
fun `pushHandler returning true locks the wake lock but does not unlock it so it continues to run`() = runTest {
|
fun `pushHandler returning true locks the wake lock but does not unlock it so it continues to run`() = runTest {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().context
|
val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> true }
|
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> true }
|
||||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val unlockLambda = lambdaRecorder<Unit> { }
|
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||||
pushHandler = FakePushHandler(
|
pushHandler = FakePushHandler(
|
||||||
handleResult = pushHandlerResult
|
handleResult = pushHandlerResult
|
||||||
),
|
),
|
||||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||||
lock = lockLambda,
|
lock = lockLambda,
|
||||||
unlock = unlockLambda,
|
unlock = unlockLambda,
|
||||||
),
|
),
|
||||||
|
|
@ -133,13 +132,13 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
fun `pushHandler returning false locks and unlocks the wakelock early`() = runTest {
|
fun `pushHandler returning false locks and unlocks the wakelock early`() = runTest {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().context
|
val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> false }
|
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> false }
|
||||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val unlockLambda = lambdaRecorder<Unit> { }
|
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||||
pushHandler = FakePushHandler(
|
pushHandler = FakePushHandler(
|
||||||
handleResult = pushHandlerResult
|
handleResult = pushHandlerResult
|
||||||
),
|
),
|
||||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||||
lock = lockLambda,
|
lock = lockLambda,
|
||||||
unlock = unlockLambda,
|
unlock = unlockLambda,
|
||||||
),
|
),
|
||||||
|
|
@ -264,7 +263,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(),
|
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||||
): VectorUnifiedPushMessagingReceiver {
|
): VectorUnifiedPushMessagingReceiver {
|
||||||
return VectorUnifiedPushMessagingReceiver().apply {
|
return VectorUnifiedPushMessagingReceiver().apply {
|
||||||
this.pushParser = unifiedPushParser
|
this.pushParser = unifiedPushParser
|
||||||
|
|
@ -277,7 +276,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
|
this.fetchPushForegroundServiceManager = pushHandlingWakeLock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue