diff options
10 files changed, 370 insertions, 3 deletions
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeOverlayModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeOverlayModule.kt new file mode 100644 index 000000000000..e55520a09103 --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeOverlayModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene + +import com.android.systemui.notifications.ui.composable.NotificationsShadeOverlay +import com.android.systemui.scene.ui.composable.Overlay +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet + +@Module +interface NotificationsShadeOverlayModule { + + @Binds @IntoSet fun notificationsShade(overlay: NotificationsShadeOverlay): Overlay +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt new file mode 100644 index 000000000000..37888f29ab6b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notifications.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ContentScope +import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel +import com.android.systemui.scene.session.ui.composable.SaveableSession +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.composable.ExpandedShadeHeader +import com.android.systemui.shade.ui.composable.OverlayShade +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager +import dagger.Lazy +import java.util.Optional +import javax.inject.Inject + +@SysUISingleton +class NotificationsShadeOverlay +@Inject +constructor( + private val actionsViewModelFactory: NotificationsShadeOverlayActionsViewModel.Factory, + private val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, + private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, + private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, + private val tintedIconManagerFactory: TintedIconManager.Factory, + private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, + private val statusBarIconController: StatusBarIconController, + private val shadeSession: SaveableSession, + private val stackScrollView: Lazy<NotificationScrollView>, +) : Overlay { + + override val key = Overlays.NotificationsShade + + private val actionsViewModel: NotificationsShadeOverlayActionsViewModel by lazy { + actionsViewModelFactory.create() + } + + override suspend fun activate(): Nothing { + actionsViewModel.activate() + } + + @Composable + override fun ContentScope.Content( + modifier: Modifier, + ) { + OverlayShade( + modifier = modifier, + viewModelFactory = overlayShadeViewModelFactory, + lockscreenContent = { Optional.empty() }, + ) { + Column { + val placeholderViewModel = + rememberViewModel("NotificationsShadeOverlay") { + notificationsPlaceholderViewModelFactory.create() + } + + ExpandedShadeHeader( + viewModelFactory = shadeHeaderViewModelFactory, + createTintedIconManager = tintedIconManagerFactory::create, + createBatteryMeterViewController = batteryMeterViewControllerFactory::create, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + NotificationScrollingStack( + shadeSession = shadeSession, + stackScrollView = stackScrollView.get(), + viewModel = placeholderViewModel, + maxScrimTop = { 0f }, + shouldPunchHoleBehindScrim = false, + shouldFillMaxSize = false, + shouldReserveSpaceForNavBar = false, + shadeMode = ShadeMode.Dual, + modifier = Modifier.fillMaxWidth(), + ) + + // Communicates the bottom position of the drawable area within the shade to NSSL. + NotificationStackCutoffGuideline( + stackScrollView = stackScrollView.get(), + viewModel = placeholderViewModel, + ) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt new file mode 100644 index 000000000000..cdba4dbfe921 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notifications.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository } + + private val underTest by lazy { kosmos.notificationsShadeOverlayActionsViewModel } + + @Test + fun upTransitionSceneKey_topAligned_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + fakeShadeRepository.setDualShadeAlignedToBottom(false) + underTest.activateIn(this) + + assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.NotificationsShade) + assertThat(actions?.get(Swipe.Down)).isNull() + } + + @Test + fun upTransitionSceneKey_bottomAligned_doesNothing() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + fakeShadeRepository.setDualShadeAlignedToBottom(true) + underTest.activateIn(this) + + assertThat(actions?.get(Swipe.Up)).isNull() + assertThat((actions?.get(Swipe.Down) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.NotificationsShade) + } + + @Test + fun back_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(this) + + assertThat((actions?.get(Back) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.NotificationsShade) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt new file mode 100644 index 000000000000..db988f62b99d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notifications.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.TransitionKeys +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeAlignment +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Models the UI state for the user actions for navigating to other scenes or overlays. */ +class NotificationsShadeOverlayActionsViewModel +@AssistedInject +constructor( + private val shadeInteractor: ShadeInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + setActions( + mapOf( + if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { + Swipe.Up to UserActionResult.HideOverlay(Overlays.NotificationsShade) + } else { + Swipe.Down to + UserActionResult.HideOverlay( + overlay = Overlays.NotificationsShade, + transitionKey = TransitionKeys.OpenBottomShade, + ) + }, + Back to UserActionResult.HideOverlay(Overlays.NotificationsShade), + ) + ) + } + + @AssistedFactory + interface Factory { + fun create(): NotificationsShadeOverlayActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index 6e8997346fc4..98cf941d2c85 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -27,6 +27,7 @@ import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartabl import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.domain.startable.ScrimStartable import com.android.systemui.scene.domain.startable.StatusBarStartable +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.flag.DualShade @@ -42,6 +43,7 @@ import dagger.multibindings.IntoMap [ EmptySceneModule::class, GoneSceneModule::class, + NotificationsShadeOverlayModule::class, NotificationsShadeSceneModule::class, NotificationsShadeSessionModule::class, QuickSettingsSceneModule::class, @@ -99,6 +101,10 @@ interface KeyguardlessSceneContainerFrameworkModule { Scenes.Shade.takeUnless { DualShade.isEnabled }, ), initialSceneKey = Scenes.Gone, + overlayKeys = + listOfNotNull( + Overlays.NotificationsShade.takeIf { DualShade.isEnabled }, + ), navigationDistances = mapOf( Scenes.Gone to 0, diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 7d63b4ce0044..86b242782308 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -28,6 +28,7 @@ import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartabl import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.domain.startable.ScrimStartable import com.android.systemui.scene.domain.startable.StatusBarStartable +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.flag.DualShade @@ -50,6 +51,7 @@ import dagger.multibindings.IntoMap QuickSettingsSceneModule::class, ShadeSceneModule::class, QuickSettingsShadeSceneModule::class, + NotificationsShadeOverlayModule::class, NotificationsShadeSceneModule::class, NotificationsShadeSessionModule::class, SceneDomainModule::class, @@ -108,6 +110,10 @@ interface SceneContainerFrameworkModule { Scenes.Shade.takeUnless { DualShade.isEnabled }, ), initialSceneKey = Scenes.Lockscreen, + overlayKeys = + listOfNotNull( + Overlays.NotificationsShade.takeIf { DualShade.isEnabled }, + ), navigationDistances = mapOf( Scenes.Gone to 0, diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt new file mode 100644 index 000000000000..0bb02e92c9ba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.shared.model + +import com.android.compose.animation.scene.OverlayKey + +/** + * Keys of all known overlays. + * + * PLEASE KEEP THE KEYS SORTED ALPHABETICALLY. + */ +object Overlays { + /** + * The notifications shade overlay primarily shows a scrollable list of notifications. + * + * It's used only in the dual shade configuration, where there are two separate shades: one for + * notifications (this overlay) and another for [QuickSettingsShade]. + * + * It's not used in the single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings) or in the "split" shade configuration (on + * large screens or unfolded foldables, where notifications and quick settings are shown + * side-by-side in their own columns). + */ + @JvmField val NotificationsShade = OverlayKey("notifications_shade") +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt index 368e4fa06a26..076613005959 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.asStateFlow * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled, * this base class takes care of it. */ +// TODO(b/363206563): Rename to UserActionsViewModel. abstract class SceneActionsViewModel : ExclusiveActivatable() { private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 7dfe8027d783..7bc2483961bf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -1,9 +1,9 @@ package com.android.systemui.scene -import com.android.compose.animation.scene.OverlayKey import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.shared.model.FakeScene +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.FakeOverlay @@ -26,8 +26,8 @@ val Kosmos.scenes by Fixture { fakeScenes } val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen } var Kosmos.overlayKeys by Fixture { - listOf<OverlayKey>( - // TODO(b/356596436): Add overlays here when we have them. + listOf( + Overlays.NotificationsShade, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayActionsViewModelKosmos.kt new file mode 100644 index 000000000000..24481bf1f67e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayActionsViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.notificationsShadeOverlayActionsViewModel: + NotificationsShadeOverlayActionsViewModel by Fixture { + NotificationsShadeOverlayActionsViewModel( + shadeInteractor = shadeInteractor, + ) +} |