Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,10 +33,12 @@ internal fun aRoomsContentState(
securityBannerState: SecurityBannerState = SecurityBannerState.None,
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
seenRoomInvites: Set<RoomId> = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
batteryOptimizationState = batteryOptimizationState,
summaries = summaries,
seenRoomInvites = seenRoomInvites.toPersistentSet(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
private val notificationCleaner: NotificationCleaner,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val appPreferencesStore: AppPreferencesStore,
Expand Down Expand Up @@ -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(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -78,6 +79,7 @@ sealed interface RoomListContentState {
data class Rooms(
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val batteryOptimizationState: BatteryOptimizationState,
val summaries: ImmutableList<RoomListRoomSummary>,
val seenRoomInvites: ImmutableSet<RoomId>,
) : RoomListContentState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,6 +46,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState(contentState = aSkeletonContentState()),
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.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
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

@Composable
internal fun BatteryOptimizationBanner(
state: BatteryOptimizationState,
modifier: Modifier = Modifier,
) {
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_battery_optimization_title_android),
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.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) },
),
)
}

@PreviewsDayNight
@Composable
internal fun BatteryOptimizationBannerPreview() = ElementPreview {
BatteryOptimizationBanner(
state = aBatteryOptimizationState(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ private fun EmptyView(
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
)
}
else -> Unit
SecurityBannerState.None -> Unit
}
}
}
Expand Down Expand Up @@ -234,6 +234,10 @@ private fun RoomsViewList(
item {
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
}
} else if (state.batteryOptimizationState.shouldDisplayBanner) {
item {
BatteryOptimizationBanner(state = state.batteryOptimizationState)
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions features/roomlist/impl/src/main/res/values/localazy.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Disable battery optimization for this app, to make sure all notifications are received."</string>
<string name="banner_battery_optimization_submit_android">"Disable optimization"</string>
<string name="banner_battery_optimization_title_android">"Notifications not arriving?"</string>
<string name="banner_set_up_recovery_content">"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."</string>
<string name="banner_set_up_recovery_submit">"Set up recovery"</string>
<string name="banner_set_up_recovery_title">"Set up recovery to protect your account"</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -712,6 +713,7 @@ class RoomListPresenterTest {
analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
batteryOptimizationPresenter = { aBatteryOptimizationState() },
notificationCleaner = notificationCleaner,
logoutPresenter = { aDirectLogoutState() },
appPreferencesStore = appPreferencesStore,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,10 +77,8 @@ class FullScreenIntentPermissionsPresenterTest {
}.test {
skipItems(1)
val loadedItem = awaitItem()
loadedItem.dismissFullScreenIntentBanner()

loadedItem.eventSink(FullScreenIntentPermissionsEvents.Dismiss)
runCurrent()

assertThat(awaitItem().shouldDisplayBanner).isFalse()
}
}
Expand All @@ -94,10 +93,8 @@ class FullScreenIntentPermissionsPresenterTest {
}.test {
skipItems(1)
val loadedItem = awaitItem()
loadedItem.openFullScreenIntentSettings()

loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings)
launchLambda.assertions().isCalledOnce()

cancelAndIgnoreRemainingEvents()
}
}
Expand All @@ -115,10 +112,8 @@ class FullScreenIntentPermissionsPresenterTest {
}.test {
skipItems(1)
val loadedItem = awaitItem()
loadedItem.openFullScreenIntentSettings()

loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings)
launchLambda.assertions().isNeverCalled()

cancelAndIgnoreRemainingEvents()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 RequestDisableOptimizations : BatteryOptimizationEvents
}
Original file line number Diff line number Diff line change
@@ -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

data class BatteryOptimizationState(
val shouldDisplayBanner: Boolean,
val eventSink: (BatteryOptimizationEvents) -> Unit,
)
Original file line number Diff line number Diff line change
@@ -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

fun aBatteryOptimizationState(
shouldDisplayBanner: Boolean = false,
eventSink: (BatteryOptimizationEvents) -> Unit = {},
) = BatteryOptimizationState(
shouldDisplayBanner = shouldDisplayBanner,
eventSink = eventSink,
)
3 changes: 3 additions & 0 deletions libraries/push/impl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

<application>
<receiver
android:name=".notifications.TestNotificationReceiver"
Expand Down
Loading
Loading