From e04c9aa11873359e1c6aa3f80ee6c13e8bba6e31 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 6 Jun 2025 17:59:52 +0200 Subject: [PATCH 01/10] Add a banner to ask the user to disable battery optimization when Event cannot be resolved from Push. --- .../impl/RoomListContentStateProvider.kt | 4 + .../roomlist/impl/RoomListPresenter.kt | 3 + .../features/roomlist/impl/RoomListState.kt | 2 + .../roomlist/impl/RoomListStateProvider.kt | 2 + .../components/BatteryOptimizationBanner.kt | 45 +++++ .../impl/components/RoomListContentView.kt | 6 +- .../roomlist/impl/RoomListPresenterTest.kt | 2 + .../api/battery/BatteryOptimizationState.kt | 16 ++ .../BatteryOptimizationStateProvider.kt | 20 +++ .../push/impl/src/main/AndroidManifest.xml | 3 + .../push/impl/battery/BatteryOptimization.kt | 72 ++++++++ .../battery/BatteryOptimizationPresenter.kt | 66 +++++++ .../libraries/push/impl/di/PushModule.kt | 17 +- .../push/impl/push/DefaultPushHandler.kt | 2 + .../push/MutableBatteryOptimizationStore.kt | 31 ++++ .../push/impl/store/DefaultPushDataStore.kt | 32 ++++ .../push/impl/store/PushDataStore.kt | 1 + .../BatteryOptimizationPresenterTest.kt | 169 ++++++++++++++++++ .../impl/battery/FakeBatteryOptimization.kt | 24 +++ .../push/impl/push/DefaultPushHandlerTest.kt | 50 +++++- .../FakeMutableBatteryOptimizationStore.kt | 23 +++ .../push/impl/store/InMemoryPushDataStore.kt | 4 + 22 files changed, 583 insertions(+), 11 deletions(-) create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt index 7435424b15..5825c0a1e5 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt @@ -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.aFullScreenIntentPermissionsState 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.persistentListOf import kotlinx.collections.immutable.toPersistentSet @@ -31,10 +33,12 @@ internal fun aRoomsContentState( securityBannerState: SecurityBannerState = SecurityBannerState.None, summaries: ImmutableList = aRoomListRoomSummaryList(), fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), + batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(), seenRoomInvites: Set = emptySet(), ) = RoomListContentState.Rooms( securityBannerState = securityBannerState, fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, + batteryOptimizationState = batteryOptimizationState, summaries = summaries, seenRoomInvites = seenRoomInvites.toPersistentSet(), ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 7746f98204..9e8199cd80 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -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.preferences.api.store.AppPreferencesStore 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.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction @@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor( private val analyticsService: AnalyticsService, private val acceptDeclineInvitePresenter: Presenter, private val fullScreenIntentPermissionsPresenter: Presenter, + private val batteryOptimizationPresenter: Presenter, private val notificationCleaner: NotificationCleaner, private val logoutPresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, @@ -248,6 +250,7 @@ class RoomListPresenter @Inject constructor( RoomListContentState.Rooms( securityBannerState = securityBannerState, fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), + batteryOptimizationState = batteryOptimizationPresenter.present(), summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(), seenRoomInvites = seenRoomInvites.toPersistentSet(), ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index f685d5f379..0cf6147df7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -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.matrix.api.core.RoomId 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.ImmutableSet @@ -78,6 +79,7 @@ sealed interface RoomListContentState { data class Rooms( val securityBannerState: SecurityBannerState, val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, + val batteryOptimizationState: BatteryOptimizationState, val summaries: ImmutableList, val seenRoomInvites: ImmutableSet, ) : RoomListContentState diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 4a221d3c97..cb4818ddd1 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -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.matrix.api.core.UserId 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -45,6 +46,7 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState(contentState = aSkeletonContentState()), aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)), + aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))), ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt new file mode 100644 index 0000000000..19f63cf20d --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt @@ -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(), + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt index 0118a4fb81..b27f21c7f6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt @@ -149,7 +149,7 @@ private fun EmptyView( onDismissClick = { eventSink(RoomListEvents.DismissBanner) }, ) } - else -> Unit + SecurityBannerState.None -> Unit } } } @@ -234,6 +234,10 @@ private fun RoomsViewList( item { FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState) } + } else if (state.batteryOptimizationState.shouldDisplayBanner) { + item { + BatteryOptimizationBanner(state = state.batteryOptimizationState) + } } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 446a864f0b..3184a44a1c 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -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.test.InMemoryAppPreferencesStore 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.test.notifications.FakeNotificationCleaner import io.element.android.services.analytics.api.AnalyticsService @@ -712,6 +713,7 @@ class RoomListPresenterTest { analyticsService = analyticsService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, + batteryOptimizationPresenter = { aBatteryOptimizationState() }, notificationCleaner = notificationCleaner, logoutPresenter = { aDirectLogoutState() }, appPreferencesStore = appPreferencesStore, diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt new file mode 100644 index 0000000000..42e65c2bcb --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt @@ -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, +) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt new file mode 100644 index 0000000000..da1639ed7a --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt @@ -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, +) diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index 49ca55decc..c08c16ed5f 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -7,6 +7,9 @@ + + + () + ?.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 + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt new file mode 100644 index 0000000000..46bd00ab3a --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt @@ -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 { + @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() + } + } + } + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt index 3e7c7ddae3..bb8b7e1624 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt @@ -10,16 +10,25 @@ package io.element.android.libraries.push.impl.di import android.content.Context import androidx.core.app.NotificationManagerCompat import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds import dagger.Module import dagger.Provides +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.AppScope 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 @ContributesTo(AppScope::class) -object PushModule { - @Provides - fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat { - return NotificationManagerCompat.from(context) +interface PushModule { + companion object { + @Provides + fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat { + return NotificationManagerCompat.from(context) + } } + + @Binds + fun bindBatteryOptimizationPresenter(presenter: BatteryOptimizationPresenter): Presenter } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index a426cdcf42..d42ca28c88 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -50,6 +50,7 @@ class DefaultPushHandler @Inject constructor( private val onNotifiableEventReceived: OnNotifiableEventReceived, private val onRedactedEventReceived: OnRedactedEventReceived, private val incrementPushDataStore: IncrementPushDataStore, + private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore, private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, private val buildMeta: BuildMeta, @@ -102,6 +103,7 @@ class DefaultPushHandler @Inject constructor( sessionId = request.sessionId, reason = exception.message ?: exception.javaClass.simpleName, ) + mutableBatteryOptimizationStore.showBatteryOptimizationBanner() } ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt new file mode 100644 index 0000000000..c5d52c55a2 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt @@ -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) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt index 2130e91c72..a4d9413cb1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt @@ -43,10 +43,24 @@ class DefaultPushDataStore @Inject constructor( ) : PushDataStore { 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 = context.dataStore.data.map { preferences -> preferences[pushCounter] ?: 0 } + @Suppress("UnnecessaryParentheses") + override val shouldDisplayBatteryOptimizationBannerFlow: Flow = context.dataStore.data.map { preferences -> + (preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW + } + suspend fun incrementPushCounter() { context.dataStore.edit { settings -> 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> { return pushDatabase.pushHistoryQueries.selectAll() .asFlow() @@ -84,4 +110,10 @@ class DefaultPushDataStore @Inject constructor( 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 + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt index 13c4e06347..de8f6dbbc1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/PushDataStore.kt @@ -11,6 +11,7 @@ import io.element.android.libraries.push.api.history.PushHistoryItem import kotlinx.coroutines.flow.Flow interface PushDataStore { + val shouldDisplayBatteryOptimizationBannerFlow: Flow val pushCounterFlow: Flow /** diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt new file mode 100644 index 0000000000..6a31b399f3 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt @@ -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 { } + 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 { } + val requestDisablingBatteryOptimizationResult = lambdaRecorder { 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 { 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 + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt new file mode 100644 index 0000000000..7bf4564bb8 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt @@ -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() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 933c830525..64a3b87aa7 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -86,7 +86,7 @@ class DefaultPushHandlerTest { fun `when classical PushData is received, the notification drawer is informed`() = runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder, Result>>> { _, _, -> + lambdaRecorder, Result>>> { _, _ -> 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)))) } @@ -268,11 +268,35 @@ class DefaultPushHandlerTest { } @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 -> + Result.success( + requests.associateWith { Result.failure(ResolvingException("Unable to resolve event")) } + ) + }, + shouldSetOptimizationBatteryBanner = true, + ) + } + + private fun `test notification resolver failure`( + notificationResolveResult: (List) -> Result>>, + shouldSetOptimizationBatteryBanner: Boolean, + ) { runTest { val notifiableEventResult = - lambdaRecorder, Result>>> { _, _ -> - Result.failure(ResolvingException("Unable to resolve")) + lambdaRecorder, Result>>> { _, requests -> + notificationResolveResult(requests) } val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val incrementPushCounterResult = lambdaRecorder {} @@ -286,6 +310,7 @@ class DefaultPushHandlerTest { val pushHistoryService = FakePushHistoryService( onPushReceivedResult = onPushReceivedResult, ) + val showBatteryOptimizationBannerResult = lambdaRecorder {} val defaultPushHandler = createDefaultPushHandler( onNotifiableEventsReceived = onNotifiableEventsReceived, notifiableEventsResult = notifiableEventResult, @@ -297,6 +322,9 @@ class DefaultPushHandlerTest { getUserIdFromSecretResult = { A_USER_ID } ), incrementPushCounterResult = incrementPushCounterResult, + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + showBatteryOptimizationBannerResult = showBatteryOptimizationBannerResult, + ), pushHistoryService = pushHistoryService, ) defaultPushHandler.handle(aPushData, A_PUSHER_INFO) @@ -313,7 +341,15 @@ class DefaultPushHandlerTest { onPushReceivedResult.assertions() .isCalledOnce() .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 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 { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder, Result>>> { _, _, -> + lambdaRecorder, Result>>> { _, _ -> 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)))) } @@ -595,8 +631,9 @@ class DefaultPushHandlerTest { onNotifiableEventsReceived: (List) -> Unit = { lambdaError() }, onRedactedEventsReceived: (List) -> Unit = { lambdaError() }, notifiableEventsResult: (SessionId, List) -> Result>> = - { _, _, -> lambdaError() }, + { _, _ -> lambdaError() }, incrementPushCounterResult: () -> Unit = { lambdaError() }, + mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(), userPushStore: UserPushStore = FakeUserPushStore(), pushClientSecret: PushClientSecret = FakePushClientSecret(), buildMeta: BuildMeta = aBuildMeta(), @@ -614,6 +651,7 @@ class DefaultPushHandlerTest { incrementPushCounterResult() } }, + mutableBatteryOptimizationStore = mutableBatteryOptimizationStore, userPushStoreFactory = FakeUserPushStoreFactory { userPushStore }, pushClientSecret = pushClientSecret, buildMeta = buildMeta, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt new file mode 100644 index 0000000000..9e526debbb --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt @@ -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() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt index 7ea038791b..4f710e09b6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt @@ -15,12 +15,16 @@ import kotlinx.coroutines.flow.asStateFlow class InMemoryPushDataStore( initialPushCounter: Int = 0, + initialShouldDisplayBatteryOptimizationBanner: Boolean = false, initialPushHistoryItems: List = emptyList(), private val resetResult: () -> Unit = { lambdaError() } ) : PushDataStore { private val mutablePushCounterFlow = MutableStateFlow(initialPushCounter) override val pushCounterFlow: Flow = mutablePushCounterFlow.asStateFlow() + private val mutableShouldDisplayBatteryOptimizationBannerFlow = MutableStateFlow(initialShouldDisplayBatteryOptimizationBanner) + override val shouldDisplayBatteryOptimizationBannerFlow: Flow = mutableShouldDisplayBatteryOptimizationBannerFlow.asStateFlow() + private val mutablePushHistoryItemsFlow = MutableStateFlow(initialPushHistoryItems) override fun getPushHistoryItemsFlow(): Flow> { From a128bfeb0a6156bb73feb406d4f89ccd6f539930 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 9 Jun 2025 13:22:22 +0000 Subject: [PATCH 02/10] Update screenshots --- ...list.impl.components_BatteryOptimizationBanner_Day_0_en.png | 3 +++ ...st.impl.components_BatteryOptimizationBanner_Night_0_en.png | 3 +++ .../images/features.roomlist.impl_RoomListView_Day_11_en.png | 3 +++ .../images/features.roomlist.impl_RoomListView_Night_11_en.png | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png new file mode 100644 index 0000000000..61e060fe3c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91bae49343942b318e7aac1446abc97c4f207d2b446ba78106c9725066c2e746 +size 26349 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png new file mode 100644 index 0000000000..c8254c4e12 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0600177d28bf58cd6f43bb7268f528bd5f4c3a1cfcc0e2793c233096c4d67c49 +size 25173 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png new file mode 100644 index 0000000000..c48bccce51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a98815701f702ee2c33111d3df57ec9467ce63f22f7b9836d0ab4e1f939b1f7b +size 100152 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png new file mode 100644 index 0000000000..c3594209a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b7c835fee75d39964ba630c31230688491980aac26f545ea8d9a51b13c77ef4 +size 105914 From a306fbd0e717512155cc6f7711a6219641d768be Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Jun 2025 15:26:26 +0200 Subject: [PATCH 03/10] Use defined const instead of magic numbers. --- .../push/impl/push/MutableBatteryOptimizationStore.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt index c5d52c55a2..1c1d9186f6 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt @@ -22,10 +22,10 @@ class DefaultMutableBatteryOptimizationStore @Inject constructor( private val defaultPushDataStore: DefaultPushDataStore, ) : MutableBatteryOptimizationStore { override suspend fun showBatteryOptimizationBanner() { - defaultPushDataStore.setBatteryOptimizationBannerState(1) + defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_SHOW) } override suspend fun onOptimizationBannerDismissed() { - defaultPushDataStore.setBatteryOptimizationBannerState(2) + defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED) } } From dc2d5a253a426b755ac9f5d47afd0367f135e807 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Jun 2025 16:01:19 +0200 Subject: [PATCH 04/10] Avoid using the Activity, and use eventSink instead of lambda in states. --- .../components/BatteryOptimizationBanner.kt | 7 +++-- .../api/battery/BatteryOptimizationEvents.kt | 13 +++++++++ .../api/battery/BatteryOptimizationState.kt | 5 +--- .../BatteryOptimizationStateProvider.kt | 8 ++---- .../push/impl/battery/BatteryOptimization.kt | 11 ++++---- .../battery/BatteryOptimizationPresenter.kt | 27 +++++++++++-------- .../BatteryOptimizationPresenterTest.kt | 7 ++--- .../impl/battery/FakeBatteryOptimization.kt | 3 +-- 8 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt index 19f63cf20d..d2dda01659 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt @@ -7,13 +7,13 @@ 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.BatteryOptimizationEvents import io.element.android.libraries.push.api.battery.BatteryOptimizationState import io.element.android.libraries.push.api.battery.aBatteryOptimizationState @@ -22,7 +22,6 @@ internal fun BatteryOptimizationBanner( state: BatteryOptimizationState, modifier: Modifier = Modifier, ) { - val activity = LocalActivity.current Announcement( modifier = modifier.roomListBannerPadding(), // TODO Localazy @@ -30,8 +29,8 @@ internal fun BatteryOptimizationBanner( 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, + onActionClick = { state.eventSink(BatteryOptimizationEvents.DoAction) }, + onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) }, ), ) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt new file mode 100644 index 0000000000..2ee8022827 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt @@ -0,0 +1,13 @@ +/* + * 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 + +sealed interface BatteryOptimizationEvents { + data object Dismiss : BatteryOptimizationEvents + data object DoAction : BatteryOptimizationEvents +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt index 42e65c2bcb..7e7c2b2bd6 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt @@ -7,10 +7,7 @@ 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, + val eventSink: (BatteryOptimizationEvents) -> Unit, ) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt index da1639ed7a..d6d4e5774b 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt @@ -7,14 +7,10 @@ package io.element.android.libraries.push.api.battery -import android.app.Activity - fun aBatteryOptimizationState( shouldDisplayBanner: Boolean = false, - dismiss: () -> Unit = {}, - openSettings: (Activity?) -> Unit = {}, + eventSink: (BatteryOptimizationEvents) -> Unit = {}, ) = BatteryOptimizationState( shouldDisplayBanner = shouldDisplayBanner, - dismiss = dismiss, - openSettings = openSettings, + eventSink = eventSink, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt index dd114aed4b..8adcbb2572 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt @@ -8,7 +8,6 @@ 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 @@ -19,6 +18,7 @@ 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 io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import timber.log.Timber import javax.inject.Inject @@ -39,16 +39,16 @@ interface BatteryOptimization { * 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 + fun requestDisablingBatteryOptimization(): Boolean } @ContributesBinding(AppScope::class) class AndroidBatteryOptimization @Inject constructor( @ApplicationContext private val context: Context, + private val externalIntentLauncher: ExternalIntentLauncher, ) : BatteryOptimization { override fun isIgnoringBatteryOptimizations(): Boolean { return context.getSystemService() @@ -56,13 +56,12 @@ class AndroidBatteryOptimization @Inject constructor( } @SuppressLint("BatteryLife") - override fun requestDisablingBatteryOptimization(activity: Activity?): Boolean { - activity ?: return false + override fun requestDisablingBatteryOptimization(): Boolean { val intent = Intent() intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent.data = ("package:" + context.packageName).toUri() return try { - activity.startActivity(intent) + externalIntentLauncher.launch(intent) true } catch (exception: ActivityNotFoundException) { Timber.w(exception, "Cannot request ignoring battery optimizations.") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt index 46bd00ab3a..857b820ad4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt @@ -16,6 +16,7 @@ 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.BatteryOptimizationEvents 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 @@ -45,22 +46,26 @@ class BatteryOptimizationPresenter @Inject constructor( onPauseOrDispose {} } - return BatteryOptimizationState( - shouldDisplayBanner = localShouldDisplayBanner && storeShouldDisplayBanner && !isSystemIgnoringBatteryOptimizations, - dismiss = { - coroutineScope.launch { + fun handleEvents(event: BatteryOptimizationEvents) { + when (event) { + BatteryOptimizationEvents.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() + BatteryOptimizationEvents.DoAction -> { + isRequestSent = true + if (batteryOptimization.requestDisablingBatteryOptimization().not()) { + // If not able to perform the request, ensure that we do not display the banner again + coroutineScope.launch { + mutableBatteryOptimizationStore.onOptimizationBannerDismissed() + } } } } + } + + return BatteryOptimizationState( + shouldDisplayBanner = localShouldDisplayBanner && storeShouldDisplayBanner && !isSystemIgnoringBatteryOptimizations, + eventSink = ::handleEvents, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt index 6a31b399f3..7188aecaa3 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt @@ -9,6 +9,7 @@ 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.api.battery.BatteryOptimizationEvents 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 @@ -96,7 +97,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.dismiss() + displayedItem.eventSink(BatteryOptimizationEvents.Dismiss) onOptimizationBannerDismissedResult.assertions().isCalledOnce() } } @@ -122,7 +123,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.openSettings(null) + displayedItem.eventSink(BatteryOptimizationEvents.DoAction) requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() onOptimizationBannerDismissedResult.assertions().isCalledOnce() } @@ -148,7 +149,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.openSettings(null) + displayedItem.eventSink(BatteryOptimizationEvents.DoAction) requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() batteryOptimization.isIgnoringBatteryOptimizationsResult = true lifeCycleOwner.givenState(Lifecycle.State.RESUMED) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt index 7bf4564bb8..0adb3d2520 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/FakeBatteryOptimization.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.push.impl.battery -import android.app.Activity import io.element.android.tests.testutils.lambda.lambdaError class FakeBatteryOptimization( @@ -18,7 +17,7 @@ class FakeBatteryOptimization( return isIgnoringBatteryOptimizationsResult } - override fun requestDisablingBatteryOptimization(activity: Activity?): Boolean { + override fun requestDisablingBatteryOptimization(): Boolean { return requestDisablingBatteryOptimizationResult() } } From fc5c811d76fbbb05a4294192528a3323b7e7274a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Jun 2025 16:01:45 +0200 Subject: [PATCH 05/10] Use eventSink instead of lambda in states. --- .../notifications/NotificationSettingsView.kt | 3 ++- .../FullScreenIntentPermissionBanner.kt | 5 +++-- .../api/FullScreenIntentPermissionsEvents.kt | 13 +++++++++++++ .../api/FullScreenIntentPermissionsState.kt | 3 +-- ...FullScreenIntentPermissionsStateProvider.kt | 6 ++---- .../FullScreenIntentPermissionsPresenter.kt | 18 ++++++++++++------ ...FullScreenIntentPermissionsPresenterTest.kt | 13 ++++--------- 7 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 8f2543c161..f73fecaf3c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList @@ -149,7 +150,7 @@ private fun NotificationSettingsContentView( Text(stringResource(R.string.full_screen_intent_banner_message)) }, onClick = { - state.fullScreenIntentPermissionsState.openFullScreenIntentSettings() + state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) } ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt index 7e84a26ab3..339b5f4fa6 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/FullScreenIntentPermissionBanner.kt @@ -15,6 +15,7 @@ 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.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.ui.strings.CommonStrings @@ -29,8 +30,8 @@ fun FullScreenIntentPermissionBanner( description = stringResource(R.string.full_screen_intent_banner_message), type = AnnouncementType.Actionable( actionText = stringResource(CommonStrings.action_continue), - onDismissClick = state.dismissFullScreenIntentBanner, - onActionClick = state.openFullScreenIntentSettings, + onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) }, + onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) }, ), modifier = modifier.roomListBannerPadding(), ) diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt new file mode 100644 index 0000000000..e0b8433749 --- /dev/null +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt @@ -0,0 +1,13 @@ +/* + * 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.fullscreenintent.api + +sealed interface FullScreenIntentPermissionsEvents { + data object Dismiss : FullScreenIntentPermissionsEvents + data object OpenSettings : FullScreenIntentPermissionsEvents +} diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt index 3d77ebb4b5..5b3f84f977 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt @@ -10,6 +10,5 @@ package io.element.android.libraries.fullscreenintent.api data class FullScreenIntentPermissionsState( val permissionGranted: Boolean, val shouldDisplayBanner: Boolean, - val dismissFullScreenIntentBanner: () -> Unit, - val openFullScreenIntentSettings: () -> Unit, + val eventSink: (FullScreenIntentPermissionsEvents) -> Unit, ) diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt index 7dbe340c39..25ab2ab522 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt @@ -10,11 +10,9 @@ package io.element.android.libraries.fullscreenintent.api fun aFullScreenIntentPermissionsState( permissionGranted: Boolean = true, shouldDisplay: Boolean = false, - openFullScreenIntentSettings: () -> Unit = {}, - dismissFullScreenIntentBanner: () -> Unit = {}, + eventSink: (FullScreenIntentPermissionsEvents) -> Unit = {}, ) = FullScreenIntentPermissionsState( permissionGranted = permissionGranted, shouldDisplayBanner = shouldDisplay, - openFullScreenIntentSettings = openFullScreenIntentSettings, - dismissFullScreenIntentBanner = dismissFullScreenIntentBanner, + eventSink = eventSink, ) diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt index 3e427b6fa6..14cc438f41 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher @@ -60,15 +61,20 @@ class FullScreenIntentPermissionsPresenter @Inject constructor( val coroutineScope = rememberCoroutineScope() val isGranted = notificationManagerCompat.canUseFullScreenIntent() val isBannerDismissed by isFullScreenIntentBannerDismissed.collectAsState(initial = true) + + fun handleEvents(event: FullScreenIntentPermissionsEvents) { + when (event) { + FullScreenIntentPermissionsEvents.Dismiss -> coroutineScope.launch { + dismissFullScreenIntentBanner() + } + FullScreenIntentPermissionsEvents.OpenSettings -> openFullScreenIntentSettings() + } + } + return FullScreenIntentPermissionsState( permissionGranted = isGranted, shouldDisplayBanner = !isBannerDismissed && !isGranted, - dismissFullScreenIntentBanner = { - coroutineScope.launch { - dismissFullScreenIntentBanner() - } - }, - openFullScreenIntentSettings = ::openFullScreenIntentSettings, + eventSink = ::handleEvents, ) } diff --git a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt index 8cf508562b..0d10032da0 100644 --- a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt +++ b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt @@ -15,6 +15,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermissionsPresenter import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory @@ -76,10 +77,8 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.dismissFullScreenIntentBanner() - + loadedItem.eventSink(FullScreenIntentPermissionsEvents.Dismiss) runCurrent() - assertThat(awaitItem().shouldDisplayBanner).isFalse() } } @@ -94,10 +93,8 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.openFullScreenIntentSettings() - + loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) launchLambda.assertions().isCalledOnce() - cancelAndIgnoreRemainingEvents() } } @@ -115,10 +112,8 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.openFullScreenIntentSettings() - + loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) launchLambda.assertions().isNeverCalled() - cancelAndIgnoreRemainingEvents() } } From 3d60bb81eef29d90c89c2418de957156721750aa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Jun 2025 16:55:06 +0200 Subject: [PATCH 06/10] Add fallback to ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS if ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS fails. --- .../push/impl/battery/BatteryOptimization.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt index 8adcbb2572..4087a2e798 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimization.kt @@ -57,14 +57,28 @@ class AndroidBatteryOptimization @Inject constructor( @SuppressLint("BatteryLife") override fun requestDisablingBatteryOptimization(): Boolean { + val ignoreBatteryOptimizationsResult = launchAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, withData = true) + if (ignoreBatteryOptimizationsResult) { + return true + } + // Open settings as a fallback if the first attempt fails + return launchAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS, withData = false) + } + + private fun launchAction( + action: String, + withData: Boolean, + ): Boolean { val intent = Intent() - intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - intent.data = ("package:" + context.packageName).toUri() + intent.action = action + if (withData) { + intent.data = ("package:" + context.packageName).toUri() + } return try { externalIntentLauncher.launch(intent) true } catch (exception: ActivityNotFoundException) { - Timber.w(exception, "Cannot request ignoring battery optimizations.") + Timber.w(exception, "Cannot launch intent with action $action.") false } } From 3a91c00a8c2d9b5da866ba20731712349ff87d17 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jun 2025 10:29:10 +0200 Subject: [PATCH 07/10] Add unit tests on AndroidBatteryOptimization --- .../battery/AndroidBatteryOptimizationTest.kt | 113 ++++++++++++++++++ services/toolbox/test/build.gradle.kts | 1 + .../test/intent/FakeExternalIntentLauncher.kt | 3 +- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt new file mode 100644 index 0000000000..e3a28550ca --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt @@ -0,0 +1,113 @@ +/* + * 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.content.ActivityNotFoundException +import android.content.Intent +import android.provider.Settings +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher +import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher +import io.element.android.tests.testutils.lambda.lambdaRecorder +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AndroidBatteryOptimizationTest { + @Test + fun `isIgnoringBatteryOptimizations should return false`() { + val sut = createAndroidBatteryOptimization() + assertThat(sut.isIgnoringBatteryOptimizations()).isFalse() + } + + @Test + fun `requestDisablingBatteryOptimization is called once with expected intent`() { + val launchLambda = lambdaRecorder { intent -> + assertThat(intent.action).isEqualTo(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + assertThat(intent.data.toString()).isEqualTo("package:${InstrumentationRegistry.getInstrumentation().context.packageName}") + } + val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) + val sut = createAndroidBatteryOptimization( + externalIntentLauncher = externalIntentLauncher, + ) + val result = sut.requestDisablingBatteryOptimization() + launchLambda.assertions().isCalledOnce() + assertThat(result).isTrue() + } + + @Test + fun `in case of 1 error, requestDisablingBatteryOptimization returns true`() { + var callNumber = 0 + val launchLambda = lambdaRecorder { intent -> + callNumber++ + when (callNumber) { + 1 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + assertThat(intent.data.toString()).isEqualTo("package:${InstrumentationRegistry.getInstrumentation().context.packageName}") + throw ActivityNotFoundException("Test exception") + } + 2 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + assertThat(intent.data).isNull() + // No error + } + else -> { + throw AssertionError("Unexpected call number: $callNumber") + } + } + } + val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) + val sut = createAndroidBatteryOptimization( + externalIntentLauncher = externalIntentLauncher, + ) + val result = sut.requestDisablingBatteryOptimization() + launchLambda.assertions().isCalledExactly(2) + assertThat(result).isTrue() + } + + @Test + fun `in case of 2 errors, requestDisablingBatteryOptimization returns false`() { + var callNumber = 0 + val launchLambda = lambdaRecorder { intent -> + callNumber++ + when (callNumber) { + 1 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + assertThat(intent.data.toString()).isEqualTo("package:${InstrumentationRegistry.getInstrumentation().context.packageName}") + throw ActivityNotFoundException("Test exception") + } + 2 -> { + assertThat(intent.action).isEqualTo(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + assertThat(intent.data).isNull() + throw ActivityNotFoundException("Test exception") + } + else -> { + throw AssertionError("Unexpected call number: $callNumber") + } + } + } + val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) + val sut = createAndroidBatteryOptimization( + externalIntentLauncher = externalIntentLauncher, + ) + val result = sut.requestDisablingBatteryOptimization() + launchLambda.assertions().isCalledExactly(2) + assertThat(result).isFalse() + } + + private fun createAndroidBatteryOptimization( + externalIntentLauncher: ExternalIntentLauncher = FakeExternalIntentLauncher(), + ): AndroidBatteryOptimization { + return AndroidBatteryOptimization( + context = InstrumentationRegistry.getInstrumentation().context, + externalIntentLauncher = externalIntentLauncher, + ) + } +} diff --git a/services/toolbox/test/build.gradle.kts b/services/toolbox/test/build.gradle.kts index 9f3f9bfee3..8d2f2b8a2d 100644 --- a/services/toolbox/test/build.gradle.kts +++ b/services/toolbox/test/build.gradle.kts @@ -14,4 +14,5 @@ android { dependencies { api(projects.services.toolbox.api) + implementation(projects.tests.testutils) } diff --git a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt index 721a6ebe1b..a8e9589b6d 100644 --- a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt +++ b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/intent/FakeExternalIntentLauncher.kt @@ -9,9 +9,10 @@ package io.element.android.services.toolbox.test.intent import android.content.Intent import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher +import io.element.android.tests.testutils.lambda.lambdaError class FakeExternalIntentLauncher( - var launchLambda: (Intent) -> Unit = {}, + var launchLambda: (Intent) -> Unit = { lambdaError() }, ) : ExternalIntentLauncher { override fun launch(intent: Intent) { launchLambda(intent) From 3cc72bee435c1c579ba4cb1953e38ca7be431df3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Jun 2025 17:40:38 +0200 Subject: [PATCH 08/10] Battery optimization banner: update wording. --- .../impl/components/BatteryOptimizationBanner.kt | 9 +++++---- features/roomlist/impl/src/main/res/values/localazy.xml | 3 +++ tools/localazy/config.json | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt index d2dda01659..f907ca6587 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt @@ -9,6 +9,8 @@ package io.element.android.features.roomlist.impl.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.roomlist.impl.R import io.element.android.libraries.designsystem.components.Announcement import io.element.android.libraries.designsystem.components.AnnouncementType import io.element.android.libraries.designsystem.preview.ElementPreview @@ -24,11 +26,10 @@ internal fun BatteryOptimizationBanner( ) { 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.", + title = stringResource(R.string.banner_battery_optimization_title_android), + description = stringResource(R.string.banner_battery_optimization_content_android), type = AnnouncementType.Actionable( - actionText = "Yes, disable", + actionText = stringResource(R.string.banner_battery_optimization_submit_android), onActionClick = { state.eventSink(BatteryOptimizationEvents.DoAction) }, onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) }, ), diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index d72af71f84..a7b7ce8047 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -1,5 +1,8 @@ + "Disable battery optimization for this app, to make sure all notifications are received." + "Disable optimization" + "Notifications not arriving?" "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices." "Set up recovery" "Set up recovery to protect your account" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b299c39ef7..b4721f8be5 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -168,6 +168,7 @@ "session_verification_banner_.*", "confirm_recovery_key_banner_.*", "banner\\.set_up_recovery\\..*", + "banner\\.battery_optimization\\..*", "full_screen_intent_banner_.*", "screen_migration_.*", "screen_invites_.*", From f2a453263c0e007f3c01ddbdf1b67ea09c6036ab Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 12 Jun 2025 16:05:30 +0000 Subject: [PATCH 09/10] Update screenshots --- ...ist.impl.components_BatteryOptimizationBanner_Day_0_en.png | 4 ++-- ...t.impl.components_BatteryOptimizationBanner_Night_0_en.png | 4 ++-- .../images/features.roomlist.impl_RoomListView_Day_11_en.png | 4 ++-- .../features.roomlist.impl_RoomListView_Night_11_en.png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png index 61e060fe3c..6b81678de0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91bae49343942b318e7aac1446abc97c4f207d2b446ba78106c9725066c2e746 -size 26349 +oid sha256:ef31a93af4f24ebada1e1436812c01d949e75d09f1259fabbe0d7a2a7eae3400 +size 26278 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png index c8254c4e12..6440604848 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_BatteryOptimizationBanner_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0600177d28bf58cd6f43bb7268f528bd5f4c3a1cfcc0e2793c233096c4d67c49 -size 25173 +oid sha256:9bf4b769592e06e707a727bae111a6ebf27ced4c272c4aaace3368049873182b +size 25324 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png index c48bccce51..b7e1278591 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a98815701f702ee2c33111d3df57ec9467ce63f22f7b9836d0ab4e1f939b1f7b -size 100152 +oid sha256:6041a7693307a30183606e7f8f2b605ba4c9bfb8c7ad5d8122837749813d6e2c +size 99928 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png index c3594209a4..6cafa75e88 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b7c835fee75d39964ba630c31230688491980aac26f545ea8d9a51b13c77ef4 -size 105914 +oid sha256:7563629f9df4012d7cc35b2805e1ac381ccdb171935b35cad0d1ea60186e3e7d +size 105496 From 066867c7c04b71665a50ac38f5bd31c177b53425 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jun 2025 10:49:26 +0200 Subject: [PATCH 10/10] Rename `DoAction` to more specific `RequestDisableOptimizations` --- .../roomlist/impl/components/BatteryOptimizationBanner.kt | 2 +- .../libraries/push/api/battery/BatteryOptimizationEvents.kt | 2 +- .../push/impl/battery/BatteryOptimizationPresenter.kt | 2 +- .../push/impl/battery/BatteryOptimizationPresenterTest.kt | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt index f907ca6587..8fde1834d2 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/BatteryOptimizationBanner.kt @@ -30,7 +30,7 @@ internal fun BatteryOptimizationBanner( description = stringResource(R.string.banner_battery_optimization_content_android), type = AnnouncementType.Actionable( actionText = stringResource(R.string.banner_battery_optimization_submit_android), - onActionClick = { state.eventSink(BatteryOptimizationEvents.DoAction) }, + onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) }, onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) }, ), ) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt index 2ee8022827..ab8d9e92e5 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt @@ -9,5 +9,5 @@ package io.element.android.libraries.push.api.battery sealed interface BatteryOptimizationEvents { data object Dismiss : BatteryOptimizationEvents - data object DoAction : BatteryOptimizationEvents + data object RequestDisableOptimizations : BatteryOptimizationEvents } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt index 857b820ad4..9fa17f0544 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt @@ -51,7 +51,7 @@ class BatteryOptimizationPresenter @Inject constructor( BatteryOptimizationEvents.Dismiss -> coroutineScope.launch { mutableBatteryOptimizationStore.onOptimizationBannerDismissed() } - BatteryOptimizationEvents.DoAction -> { + BatteryOptimizationEvents.RequestDisableOptimizations -> { isRequestSent = true if (batteryOptimization.requestDisablingBatteryOptimization().not()) { // If not able to perform the request, ensure that we do not display the banner again diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt index 7188aecaa3..d5dd7c48d4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt @@ -123,7 +123,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.eventSink(BatteryOptimizationEvents.DoAction) + displayedItem.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() onOptimizationBannerDismissedResult.assertions().isCalledOnce() } @@ -149,7 +149,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.eventSink(BatteryOptimizationEvents.DoAction) + displayedItem.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() batteryOptimization.isIgnoringBatteryOptimizationsResult = true lifeCycleOwner.givenState(Lifecycle.State.RESUMED)