Add a banner to ask the user to disable battery optimization when Event cannot be resolved from Push.
This commit is contained in:
parent
11c2467577
commit
7deed4cc86
22 changed files with 583 additions and 11 deletions
|
|
@ -12,6 +12,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
|
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
|
|
@ -31,10 +33,12 @@ internal fun aRoomsContentState(
|
||||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||||
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
|
||||||
|
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
|
||||||
seenRoomInvites: Set<RoomId> = emptySet(),
|
seenRoomInvites: Set<RoomId> = emptySet(),
|
||||||
) = RoomListContentState.Rooms(
|
) = RoomListContentState.Rooms(
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
|
||||||
|
batteryOptimizationState = batteryOptimizationState,
|
||||||
summaries = summaries,
|
summaries = summaries,
|
||||||
seenRoomInvites = seenRoomInvites.toPersistentSet(),
|
seenRoomInvites = seenRoomInvites.toPersistentSet(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
|
|
@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
|
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
|
||||||
|
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
|
||||||
private val notificationCleaner: NotificationCleaner,
|
private val notificationCleaner: NotificationCleaner,
|
||||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
|
@ -248,6 +250,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
RoomListContentState.Rooms(
|
RoomListContentState.Rooms(
|
||||||
securityBannerState = securityBannerState,
|
securityBannerState = securityBannerState,
|
||||||
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
|
||||||
|
batteryOptimizationState = batteryOptimizationPresenter.present(),
|
||||||
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),
|
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),
|
||||||
seenRoomInvites = seenRoomInvites.toPersistentSet(),
|
seenRoomInvites = seenRoomInvites.toPersistentSet(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
|
|
@ -78,6 +79,7 @@ sealed interface RoomListContentState {
|
||||||
data class Rooms(
|
data class Rooms(
|
||||||
val securityBannerState: SecurityBannerState,
|
val securityBannerState: SecurityBannerState,
|
||||||
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
|
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
|
||||||
|
val batteryOptimizationState: BatteryOptimizationState,
|
||||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||||
val seenRoomInvites: ImmutableSet<RoomId>,
|
val seenRoomInvites: ImmutableSet<RoomId>,
|
||||||
) : RoomListContentState
|
) : RoomListContentState
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -45,6 +46,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(contentState = aSkeletonContentState()),
|
aRoomListState(contentState = aSkeletonContentState()),
|
||||||
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
|
||||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
|
||||||
|
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.features.roomlist.impl.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.libraries.designsystem.components.Announcement
|
||||||
|
import io.element.android.libraries.designsystem.components.AnnouncementType
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
|
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BatteryOptimizationBanner(
|
||||||
|
state: BatteryOptimizationState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val activity = LocalActivity.current
|
||||||
|
Announcement(
|
||||||
|
modifier = modifier.roomListBannerPadding(),
|
||||||
|
// TODO Localazy
|
||||||
|
title = "Notification tip",
|
||||||
|
description = "To be sure to receive all the notifications, it can help to disable the battery optimization for this application.",
|
||||||
|
type = AnnouncementType.Actionable(
|
||||||
|
actionText = "Yes, disable",
|
||||||
|
onActionClick = { state.openSettings(activity) },
|
||||||
|
onDismissClick = state.dismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun BatteryOptimizationBannerPreview() = ElementPreview {
|
||||||
|
BatteryOptimizationBanner(
|
||||||
|
state = aBatteryOptimizationState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -149,7 +149,7 @@ private fun EmptyView(
|
||||||
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
|
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> Unit
|
SecurityBannerState.None -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +234,10 @@ private fun RoomsViewList(
|
||||||
item {
|
item {
|
||||||
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
|
||||||
}
|
}
|
||||||
|
} else if (state.batteryOptimizationState.shouldDisplayBanner) {
|
||||||
|
item {
|
||||||
|
BatteryOptimizationBanner(state = state.batteryOptimizationState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
|
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
@ -712,6 +713,7 @@ class RoomListPresenterTest {
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
||||||
|
batteryOptimizationPresenter = { aBatteryOptimizationState() },
|
||||||
notificationCleaner = notificationCleaner,
|
notificationCleaner = notificationCleaner,
|
||||||
logoutPresenter = { aDirectLogoutState() },
|
logoutPresenter = { aDirectLogoutState() },
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.battery
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
|
||||||
|
data class BatteryOptimizationState(
|
||||||
|
val shouldDisplayBanner: Boolean,
|
||||||
|
val dismiss: () -> Unit,
|
||||||
|
val openSettings: (Activity?) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.battery
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
|
||||||
|
fun aBatteryOptimizationState(
|
||||||
|
shouldDisplayBanner: Boolean = false,
|
||||||
|
dismiss: () -> Unit = {},
|
||||||
|
openSettings: (Activity?) -> Unit = {},
|
||||||
|
) = BatteryOptimizationState(
|
||||||
|
shouldDisplayBanner = shouldDisplayBanner,
|
||||||
|
dismiss = dismiss,
|
||||||
|
openSettings = openSettings,
|
||||||
|
)
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".notifications.TestNotificationReceiver"
|
android:name=".notifications.TestNotificationReceiver"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.battery
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface BatteryOptimization {
|
||||||
|
/**
|
||||||
|
* Tells if the application ignores battery optimizations.
|
||||||
|
*
|
||||||
|
* Ignoring them allows the app to run in background to make background sync with the homeserver.
|
||||||
|
* This user option appears on Android M but Android O enforces its usage and kills apps not
|
||||||
|
* authorised by the user to run in background.
|
||||||
|
*
|
||||||
|
* @return true if battery optimisations are ignored
|
||||||
|
*/
|
||||||
|
fun isIgnoringBatteryOptimizations(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the user to disable battery optimizations for this app.
|
||||||
|
* This will open the system settings where the user can disable battery optimizations.
|
||||||
|
* See https://developer.android.com/training/monitoring-device-state/doze-standby#exemption-cases
|
||||||
|
*
|
||||||
|
* @param activity The activity from which to start the settings intent.
|
||||||
|
* @return true if the intent was successfully started, false if the activity was not found
|
||||||
|
*/
|
||||||
|
fun requestDisablingBatteryOptimization(activity: Activity?): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class AndroidBatteryOptimization @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
) : BatteryOptimization {
|
||||||
|
override fun isIgnoringBatteryOptimizations(): Boolean {
|
||||||
|
return context.getSystemService<PowerManager>()
|
||||||
|
?.isIgnoringBatteryOptimizations(context.packageName) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("BatteryLife")
|
||||||
|
override fun requestDisablingBatteryOptimization(activity: Activity?): Boolean {
|
||||||
|
activity ?: return false
|
||||||
|
val intent = Intent()
|
||||||
|
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
intent.data = ("package:" + context.packageName).toUri()
|
||||||
|
return try {
|
||||||
|
activity.startActivity(intent)
|
||||||
|
true
|
||||||
|
} catch (exception: ActivityNotFoundException) {
|
||||||
|
Timber.w(exception, "Cannot request ignoring battery optimizations.")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.battery
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
|
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
|
||||||
|
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BatteryOptimizationPresenter @Inject constructor(
|
||||||
|
private val pushDataStore: PushDataStore,
|
||||||
|
private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore,
|
||||||
|
private val batteryOptimization: BatteryOptimization,
|
||||||
|
) : Presenter<BatteryOptimizationState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): BatteryOptimizationState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var isRequestSent by remember { mutableStateOf(false) }
|
||||||
|
var localShouldDisplayBanner by remember { mutableStateOf(true) }
|
||||||
|
val storeShouldDisplayBanner by pushDataStore.shouldDisplayBatteryOptimizationBannerFlow.collectAsState(initial = false)
|
||||||
|
var isSystemIgnoringBatteryOptimizations by remember {
|
||||||
|
mutableStateOf(batteryOptimization.isIgnoringBatteryOptimizations())
|
||||||
|
}
|
||||||
|
|
||||||
|
LifecycleResumeEffect(Unit) {
|
||||||
|
isSystemIgnoringBatteryOptimizations = batteryOptimization.isIgnoringBatteryOptimizations()
|
||||||
|
if (isRequestSent) {
|
||||||
|
localShouldDisplayBanner = false
|
||||||
|
}
|
||||||
|
onPauseOrDispose {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BatteryOptimizationState(
|
||||||
|
shouldDisplayBanner = localShouldDisplayBanner && storeShouldDisplayBanner && !isSystemIgnoringBatteryOptimizations,
|
||||||
|
dismiss = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
mutableBatteryOptimizationStore.onOptimizationBannerDismissed()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSettings = { activity ->
|
||||||
|
isRequestSent = true
|
||||||
|
if (batteryOptimization.requestDisablingBatteryOptimization(activity).not()) {
|
||||||
|
// If not able to perform the request, ensure that we do not display the banner again
|
||||||
|
coroutineScope.launch {
|
||||||
|
mutableBatteryOptimizationStore.onOptimizationBannerDismissed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,16 +10,25 @@ package io.element.android.libraries.push.impl.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
|
import io.element.android.libraries.push.impl.battery.BatteryOptimizationPresenter
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
object PushModule {
|
interface PushModule {
|
||||||
@Provides
|
companion object {
|
||||||
fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat {
|
@Provides
|
||||||
return NotificationManagerCompat.from(context)
|
fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat {
|
||||||
|
return NotificationManagerCompat.from(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindBatteryOptimizationPresenter(presenter: BatteryOptimizationPresenter): Presenter<BatteryOptimizationState>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class DefaultPushHandler @Inject constructor(
|
||||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||||
private val onRedactedEventReceived: OnRedactedEventReceived,
|
private val onRedactedEventReceived: OnRedactedEventReceived,
|
||||||
private val incrementPushDataStore: IncrementPushDataStore,
|
private val incrementPushDataStore: IncrementPushDataStore,
|
||||||
|
private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore,
|
||||||
private val userPushStoreFactory: UserPushStoreFactory,
|
private val userPushStoreFactory: UserPushStoreFactory,
|
||||||
private val pushClientSecret: PushClientSecret,
|
private val pushClientSecret: PushClientSecret,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
|
@ -102,6 +103,7 @@ class DefaultPushHandler @Inject constructor(
|
||||||
sessionId = request.sessionId,
|
sessionId = request.sessionId,
|
||||||
reason = exception.message ?: exception.javaClass.simpleName,
|
reason = exception.message ?: exception.javaClass.simpleName,
|
||||||
)
|
)
|
||||||
|
mutableBatteryOptimizationStore.showBatteryOptimizationBanner()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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 com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface MutableBatteryOptimizationStore {
|
||||||
|
suspend fun showBatteryOptimizationBanner()
|
||||||
|
suspend fun onOptimizationBannerDismissed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultMutableBatteryOptimizationStore @Inject constructor(
|
||||||
|
private val defaultPushDataStore: DefaultPushDataStore,
|
||||||
|
) : MutableBatteryOptimizationStore {
|
||||||
|
override suspend fun showBatteryOptimizationBanner() {
|
||||||
|
defaultPushDataStore.setBatteryOptimizationBannerState(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onOptimizationBannerDismissed() {
|
||||||
|
defaultPushDataStore.setBatteryOptimizationBannerState(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,10 +43,24 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
) : PushDataStore {
|
) : PushDataStore {
|
||||||
private val pushCounter = intPreferencesKey("push_counter")
|
private val pushCounter = intPreferencesKey("push_counter")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer preference to track the state of the battery optimization banner.
|
||||||
|
* Possible values:
|
||||||
|
* [BATTERY_OPTIMIZATION_BANNER_STATE_INIT]: Should not show the banner
|
||||||
|
* [BATTERY_OPTIMIZATION_BANNER_STATE_SHOW]: Should show the banner
|
||||||
|
* [BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED]: Banner has been shown and user has dismissed it
|
||||||
|
*/
|
||||||
|
private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state")
|
||||||
|
|
||||||
override val pushCounterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
|
override val pushCounterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
|
||||||
preferences[pushCounter] ?: 0
|
preferences[pushCounter] ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UnnecessaryParentheses")
|
||||||
|
override val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
|
||||||
|
(preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun incrementPushCounter() {
|
suspend fun incrementPushCounter() {
|
||||||
context.dataStore.edit { settings ->
|
context.dataStore.edit { settings ->
|
||||||
val currentCounterValue = settings[pushCounter] ?: 0
|
val currentCounterValue = settings[pushCounter] ?: 0
|
||||||
|
|
@ -54,6 +68,18 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setBatteryOptimizationBannerState(newState: Int) {
|
||||||
|
context.dataStore.edit { settings ->
|
||||||
|
val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT
|
||||||
|
settings[batteryOptimizationBannerState] = when (currentValue) {
|
||||||
|
BATTERY_OPTIMIZATION_BANNER_STATE_INIT,
|
||||||
|
BATTERY_OPTIMIZATION_BANNER_STATE_SHOW -> newState
|
||||||
|
BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED -> currentValue
|
||||||
|
else -> error("Invalid value for showBatteryOptimizationBanner: $currentValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||||
return pushDatabase.pushHistoryQueries.selectAll()
|
return pushDatabase.pushHistoryQueries.selectAll()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
|
|
@ -84,4 +110,10 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
it.clear()
|
it.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BATTERY_OPTIMIZATION_BANNER_STATE_INIT = 0
|
||||||
|
const val BATTERY_OPTIMIZATION_BANNER_STATE_SHOW = 1
|
||||||
|
const val BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED = 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PushDataStore {
|
interface PushDataStore {
|
||||||
|
val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean>
|
||||||
val pushCounterFlow: Flow<Int>
|
val pushCounterFlow: Flow<Int>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.battery
|
||||||
|
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore
|
||||||
|
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
|
||||||
|
import io.element.android.libraries.push.impl.store.InMemoryPushDataStore
|
||||||
|
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||||
|
import io.element.android.tests.testutils.FakeLifecycleOwner
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.testWithLifecycleOwner
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class BatteryOptimizationPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
pushDataStore = InMemoryPushDataStore(
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner = false,
|
||||||
|
),
|
||||||
|
batteryOptimization = FakeBatteryOptimization(
|
||||||
|
isIgnoringBatteryOptimizationsResult = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val lifeCycleOwner = FakeLifecycleOwner()
|
||||||
|
presenter.testWithLifecycleOwner(lifeCycleOwner) {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.shouldDisplayBanner).isFalse()
|
||||||
|
lifeCycleOwner.givenState(Lifecycle.State.RESUMED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - should display banner`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
pushDataStore = InMemoryPushDataStore(
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner = true,
|
||||||
|
),
|
||||||
|
batteryOptimization = FakeBatteryOptimization(
|
||||||
|
isIgnoringBatteryOptimizationsResult = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.shouldDisplayBanner).isFalse()
|
||||||
|
assertThat(awaitItem().shouldDisplayBanner).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - should display banner, but setting already performed`() = runTest {
|
||||||
|
val presenter = createPresenter(
|
||||||
|
pushDataStore = InMemoryPushDataStore(
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner = true,
|
||||||
|
),
|
||||||
|
batteryOptimization = FakeBatteryOptimization(
|
||||||
|
isIgnoringBatteryOptimizationsResult = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.shouldDisplayBanner).isFalse()
|
||||||
|
assertThat(awaitItem().shouldDisplayBanner).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - should display banner, user dismisses`() = runTest {
|
||||||
|
val onOptimizationBannerDismissedResult = lambdaRecorder<Unit> { }
|
||||||
|
val presenter = createPresenter(
|
||||||
|
pushDataStore = InMemoryPushDataStore(
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner = true,
|
||||||
|
),
|
||||||
|
batteryOptimization = FakeBatteryOptimization(
|
||||||
|
isIgnoringBatteryOptimizationsResult = false,
|
||||||
|
),
|
||||||
|
mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(
|
||||||
|
onOptimizationBannerDismissedResult = onOptimizationBannerDismissedResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.shouldDisplayBanner).isFalse()
|
||||||
|
val displayedItem = awaitItem()
|
||||||
|
assertThat(displayedItem.shouldDisplayBanner).isTrue()
|
||||||
|
displayedItem.dismiss()
|
||||||
|
onOptimizationBannerDismissedResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - should display banner, user continue, error case`() = runTest {
|
||||||
|
val onOptimizationBannerDismissedResult = lambdaRecorder<Unit> { }
|
||||||
|
val requestDisablingBatteryOptimizationResult = lambdaRecorder<Boolean> { false }
|
||||||
|
val presenter = createPresenter(
|
||||||
|
pushDataStore = InMemoryPushDataStore(
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner = true,
|
||||||
|
),
|
||||||
|
batteryOptimization = FakeBatteryOptimization(
|
||||||
|
isIgnoringBatteryOptimizationsResult = false,
|
||||||
|
requestDisablingBatteryOptimizationResult = requestDisablingBatteryOptimizationResult
|
||||||
|
),
|
||||||
|
mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(
|
||||||
|
onOptimizationBannerDismissedResult = onOptimizationBannerDismissedResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.shouldDisplayBanner).isFalse()
|
||||||
|
val displayedItem = awaitItem()
|
||||||
|
assertThat(displayedItem.shouldDisplayBanner).isTrue()
|
||||||
|
displayedItem.openSettings(null)
|
||||||
|
requestDisablingBatteryOptimizationResult.assertions().isCalledOnce()
|
||||||
|
onOptimizationBannerDismissedResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - should display banner, user continue, nominal case`() = runTest {
|
||||||
|
val requestDisablingBatteryOptimizationResult = lambdaRecorder<Boolean> { true }
|
||||||
|
val batteryOptimization = FakeBatteryOptimization(
|
||||||
|
isIgnoringBatteryOptimizationsResult = false,
|
||||||
|
requestDisablingBatteryOptimizationResult = requestDisablingBatteryOptimizationResult
|
||||||
|
)
|
||||||
|
val presenter = createPresenter(
|
||||||
|
pushDataStore = InMemoryPushDataStore(
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner = true,
|
||||||
|
),
|
||||||
|
batteryOptimization = batteryOptimization,
|
||||||
|
mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(),
|
||||||
|
)
|
||||||
|
val lifeCycleOwner = FakeLifecycleOwner()
|
||||||
|
presenter.testWithLifecycleOwner(lifeCycleOwner) {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.shouldDisplayBanner).isFalse()
|
||||||
|
val displayedItem = awaitItem()
|
||||||
|
assertThat(displayedItem.shouldDisplayBanner).isTrue()
|
||||||
|
displayedItem.openSettings(null)
|
||||||
|
requestDisablingBatteryOptimizationResult.assertions().isCalledOnce()
|
||||||
|
batteryOptimization.isIgnoringBatteryOptimizationsResult = true
|
||||||
|
lifeCycleOwner.givenState(Lifecycle.State.RESUMED)
|
||||||
|
assertThat(awaitItem().shouldDisplayBanner).isFalse()
|
||||||
|
assertThat(awaitItem().shouldDisplayBanner).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
pushDataStore: PushDataStore = InMemoryPushDataStore(),
|
||||||
|
mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(),
|
||||||
|
batteryOptimization: BatteryOptimization = FakeBatteryOptimization(),
|
||||||
|
) = BatteryOptimizationPresenter(
|
||||||
|
pushDataStore = pushDataStore,
|
||||||
|
mutableBatteryOptimizationStore = mutableBatteryOptimizationStore,
|
||||||
|
batteryOptimization = batteryOptimization
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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.battery
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeBatteryOptimization(
|
||||||
|
var isIgnoringBatteryOptimizationsResult: Boolean = false,
|
||||||
|
private val requestDisablingBatteryOptimizationResult: () -> Boolean = { lambdaError() }
|
||||||
|
) : BatteryOptimization {
|
||||||
|
override fun isIgnoringBatteryOptimizations(): Boolean {
|
||||||
|
return isIgnoringBatteryOptimizationsResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestDisablingBatteryOptimization(activity: Activity?): Boolean {
|
||||||
|
return requestDisablingBatteryOptimizationResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ class DefaultPushHandlerTest {
|
||||||
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
||||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _, ->
|
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||||
}
|
}
|
||||||
|
|
@ -268,11 +268,35 @@ class DefaultPushHandlerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
|
fun `when classical PushData is received, but a failure occurs (session not found), nothing happen`() {
|
||||||
|
`test notification resolver failure`(
|
||||||
|
notificationResolveResult = { _ ->
|
||||||
|
Result.failure(ResolvingException("Unable to restore session"))
|
||||||
|
},
|
||||||
|
shouldSetOptimizationBatteryBanner = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when classical PushData is received, but not able to resolve the event, the banner to disable battery optimization will be displayed`() {
|
||||||
|
`test notification resolver failure`(
|
||||||
|
notificationResolveResult = { requests: List<NotificationEventRequest> ->
|
||||||
|
Result.success(
|
||||||
|
requests.associateWith { Result.failure(ResolvingException("Unable to resolve event")) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shouldSetOptimizationBatteryBanner = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun `test notification resolver failure`(
|
||||||
|
notificationResolveResult: (List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>,
|
||||||
|
shouldSetOptimizationBatteryBanner: Boolean,
|
||||||
|
) {
|
||||||
runTest {
|
runTest {
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, requests ->
|
||||||
Result.failure(ResolvingException("Unable to resolve"))
|
notificationResolveResult(requests)
|
||||||
}
|
}
|
||||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
|
|
@ -286,6 +310,7 @@ class DefaultPushHandlerTest {
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
val showBatteryOptimizationBannerResult = lambdaRecorder<Unit> {}
|
||||||
val defaultPushHandler = createDefaultPushHandler(
|
val defaultPushHandler = createDefaultPushHandler(
|
||||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||||
notifiableEventsResult = notifiableEventResult,
|
notifiableEventsResult = notifiableEventResult,
|
||||||
|
|
@ -297,6 +322,9 @@ class DefaultPushHandlerTest {
|
||||||
getUserIdFromSecretResult = { A_USER_ID }
|
getUserIdFromSecretResult = { A_USER_ID }
|
||||||
),
|
),
|
||||||
incrementPushCounterResult = incrementPushCounterResult,
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
|
mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(
|
||||||
|
showBatteryOptimizationBannerResult = showBatteryOptimizationBannerResult,
|
||||||
|
),
|
||||||
pushHistoryService = pushHistoryService,
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||||
|
|
@ -313,7 +341,15 @@ class DefaultPushHandlerTest {
|
||||||
onPushReceivedResult.assertions()
|
onPushReceivedResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), value(true), any())
|
.with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), value(true), any())
|
||||||
|
showBatteryOptimizationBannerResult.assertions().let {
|
||||||
|
if (shouldSetOptimizationBatteryBanner) {
|
||||||
|
it.isCalledOnce()
|
||||||
|
} else {
|
||||||
|
it.isNeverCalled()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when ringing call PushData is received, the incoming call will be handled`() = runTest {
|
fun `when ringing call PushData is received, the incoming call will be handled`() = runTest {
|
||||||
|
|
@ -542,7 +578,7 @@ class DefaultPushHandlerTest {
|
||||||
fun `when receiving several push notifications at the same time, those are batched before being processed`() = runTest {
|
fun `when receiving several push notifications at the same time, those are batched before being processed`() = runTest {
|
||||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||||
val notifiableEventResult =
|
val notifiableEventResult =
|
||||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _, ->
|
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||||
}
|
}
|
||||||
|
|
@ -595,8 +631,9 @@ class DefaultPushHandlerTest {
|
||||||
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = { lambdaError() },
|
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = { lambdaError() },
|
||||||
onRedactedEventsReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = { lambdaError() },
|
onRedactedEventsReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = { lambdaError() },
|
||||||
notifiableEventsResult: (SessionId, List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> =
|
notifiableEventsResult: (SessionId, List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> =
|
||||||
{ _, _, -> lambdaError() },
|
{ _, _ -> lambdaError() },
|
||||||
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
||||||
|
mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(),
|
||||||
userPushStore: UserPushStore = FakeUserPushStore(),
|
userPushStore: UserPushStore = FakeUserPushStore(),
|
||||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
|
|
@ -614,6 +651,7 @@ class DefaultPushHandlerTest {
|
||||||
incrementPushCounterResult()
|
incrementPushCounterResult()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mutableBatteryOptimizationStore = mutableBatteryOptimizationStore,
|
||||||
userPushStoreFactory = FakeUserPushStoreFactory { userPushStore },
|
userPushStoreFactory = FakeUserPushStoreFactory { userPushStore },
|
||||||
pushClientSecret = pushClientSecret,
|
pushClientSecret = pushClientSecret,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector 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 io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
|
class FakeMutableBatteryOptimizationStore(
|
||||||
|
private val showBatteryOptimizationBannerResult: () -> Unit = { lambdaError() },
|
||||||
|
private val onOptimizationBannerDismissedResult: () -> Unit = { lambdaError() },
|
||||||
|
) : MutableBatteryOptimizationStore {
|
||||||
|
override suspend fun showBatteryOptimizationBanner() {
|
||||||
|
showBatteryOptimizationBannerResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onOptimizationBannerDismissed() {
|
||||||
|
onOptimizationBannerDismissedResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,12 +15,16 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class InMemoryPushDataStore(
|
class InMemoryPushDataStore(
|
||||||
initialPushCounter: Int = 0,
|
initialPushCounter: Int = 0,
|
||||||
|
initialShouldDisplayBatteryOptimizationBanner: Boolean = false,
|
||||||
initialPushHistoryItems: List<PushHistoryItem> = emptyList(),
|
initialPushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||||
private val resetResult: () -> Unit = { lambdaError() }
|
private val resetResult: () -> Unit = { lambdaError() }
|
||||||
) : PushDataStore {
|
) : PushDataStore {
|
||||||
private val mutablePushCounterFlow = MutableStateFlow(initialPushCounter)
|
private val mutablePushCounterFlow = MutableStateFlow(initialPushCounter)
|
||||||
override val pushCounterFlow: Flow<Int> = mutablePushCounterFlow.asStateFlow()
|
override val pushCounterFlow: Flow<Int> = mutablePushCounterFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val mutableShouldDisplayBatteryOptimizationBannerFlow = MutableStateFlow(initialShouldDisplayBatteryOptimizationBanner)
|
||||||
|
override val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean> = mutableShouldDisplayBatteryOptimizationBannerFlow.asStateFlow()
|
||||||
|
|
||||||
private val mutablePushHistoryItemsFlow = MutableStateFlow(initialPushHistoryItems)
|
private val mutablePushHistoryItemsFlow = MutableStateFlow(initialPushHistoryItems)
|
||||||
|
|
||||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue