Loading packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +37 −10 Original line number Diff line number Diff line Loading @@ -18,23 +18,27 @@ 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 androidx.compose.ui.layout.layoutId import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel 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.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager Loading @@ -53,6 +57,8 @@ constructor( private val statusBarIconController: StatusBarIconController, private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, private val clockInteractor: KeyguardClockInteractor, ) : Overlay { override val key = Overlays.NotificationsShade Loading Loading @@ -80,14 +86,29 @@ constructor( OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { Column { ExpandedShadeHeader( if (viewModel.showHeader) { val burnIn = rememberBurnIn(clockInteractor) CollapsedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.element(NotificationsShade.Elements.StatusBar) .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) with(clockSection) { SmallClock( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, modifier = Modifier.fillMaxWidth(), ) } } NotificationScrollingStack( shadeSession = shadeSession, stackScrollView = stackScrollView.get(), Loading @@ -110,3 +131,9 @@ constructor( } } } object NotificationsShade { object Elements { val StatusBar = ElementKey("NotificationsShadeStatusBar") } } packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +5 −1 Original line number Diff line number Diff line Loading @@ -105,13 +105,17 @@ val SceneContainerTransitions = transitions { // Overlay transitions // TODO(b/376659778): Remove this transition once nested STLs are supported. from(Scenes.Gone, to = Overlays.NotificationsShade) { toNotificationsShadeTransition(translateClock = true) } to(Overlays.NotificationsShade) { toNotificationsShadeTransition() } to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() } from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) { notificationsShadeToQuickSettingsShadeTransition() } from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { toNotificationsShadeTransition(durationScale = 0.9) toNotificationsShadeTransition(translateClock = true, durationScale = 0.9) } from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { toQuickSettingsShadeTransition(durationScale = 0.9) Loading packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +21 −9 Original line number Diff line number Diff line Loading @@ -21,30 +21,42 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.notifications.ui.composable.NotificationsShade import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { fun TransitionBuilder.toNotificationsShadeTransition( translateClock: Boolean = false, durationScale: Double = 1.0, ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) // Ensure the clock isn't clipped by the shade outline during the transition from lockscreen. sharedElement( ClockElementKeys.smallClockElementKey, elevateInContent = Overlays.NotificationsShade, ) scaleSize(OverlayShade.Elements.Panel, height = 0f) // TODO(b/376659778): This is a temporary hack to have a shared element transition with the // lockscreen clock. Remove once nested STLs are supported. if (!translateClock) { translate(ClockElementKeys.smallClockElementKey) } // Avoid translating the status bar with the shade panel. translate(NotificationsShade.Elements.StatusBar) // Slide in the shade panel from the top edge. translate(OverlayShade.Elements.Panel, Edge.Top) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } fractionRange(start = .5f) { fade(ShadeHeader.Elements.Clock) fade(ShadeHeader.Elements.ExpandedContent) fade(ShadeHeader.Elements.PrivacyChip) fade(Notifications.Elements.NotificationScrim) } fractionRange(start = .5f) { fade(Notifications.Elements.NotificationScrim) } } private val DefaultDuration = 300.milliseconds packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +35 −0 Original line number Diff line number Diff line Loading @@ -35,9 +35,12 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi Loading Loading @@ -121,6 +124,38 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) } @Test fun showHeader_showsOnNarrowScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(false) // Shown when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() assertThat(underTest.showHeader).isTrue() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() assertThat(underTest.showHeader).isFalse() } @Test fun showHeader_hidesOnWideScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(true) // Hidden when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() assertThat(underTest.showHeader).isFalse() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() assertThat(underTest.showHeader).isFalse() } private fun TestScope.lockDevice() { val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.powerInteractor.setAsleepForTest() Loading packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +34 −1 Original line number Diff line number Diff line Loading @@ -16,19 +16,23 @@ package com.android.systemui.notifications.ui.viewmodel import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import com.android.app.tracing.coroutines.launchTraced as launch /** * Models UI state used to render the content of the notifications shade overlay. Loading @@ -43,10 +47,32 @@ constructor( val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") val showHeader: Boolean by hydrator.hydratedStateOf( traceName = "showHeader", initialValue = shouldShowHeader( isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, areAnyNotificationsPresent = activeNotificationsInteractor.areAnyNotificationsPresentValue, ), source = combine( shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, this::shouldShowHeader, ), ) override suspend fun onActivated(): Nothing { coroutineScope { launch { hydrator.activate() } launch { sceneInteractor.currentScene.collect { currentScene -> when (currentScene) { Loading Loading @@ -77,6 +103,13 @@ constructor( shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } private fun shouldShowHeader( isShadeLayoutWide: Boolean, areAnyNotificationsPresent: Boolean, ): Boolean { return !isShadeLayoutWide && areAnyNotificationsPresent } @AssistedFactory interface Factory { fun create(): NotificationsShadeOverlayContentViewModel Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +37 −10 Original line number Diff line number Diff line Loading @@ -18,23 +18,27 @@ 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 androidx.compose.ui.layout.layoutId import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel 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.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager Loading @@ -53,6 +57,8 @@ constructor( private val statusBarIconController: StatusBarIconController, private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, private val clockInteractor: KeyguardClockInteractor, ) : Overlay { override val key = Overlays.NotificationsShade Loading Loading @@ -80,14 +86,29 @@ constructor( OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { Column { ExpandedShadeHeader( if (viewModel.showHeader) { val burnIn = rememberBurnIn(clockInteractor) CollapsedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.element(NotificationsShade.Elements.StatusBar) .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) with(clockSection) { SmallClock( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, modifier = Modifier.fillMaxWidth(), ) } } NotificationScrollingStack( shadeSession = shadeSession, stackScrollView = stackScrollView.get(), Loading @@ -110,3 +131,9 @@ constructor( } } } object NotificationsShade { object Elements { val StatusBar = ElementKey("NotificationsShadeStatusBar") } }
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +5 −1 Original line number Diff line number Diff line Loading @@ -105,13 +105,17 @@ val SceneContainerTransitions = transitions { // Overlay transitions // TODO(b/376659778): Remove this transition once nested STLs are supported. from(Scenes.Gone, to = Overlays.NotificationsShade) { toNotificationsShadeTransition(translateClock = true) } to(Overlays.NotificationsShade) { toNotificationsShadeTransition() } to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() } from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) { notificationsShadeToQuickSettingsShadeTransition() } from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { toNotificationsShadeTransition(durationScale = 0.9) toNotificationsShadeTransition(translateClock = true, durationScale = 0.9) } from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { toQuickSettingsShadeTransition(durationScale = 0.9) Loading
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +21 −9 Original line number Diff line number Diff line Loading @@ -21,30 +21,42 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.notifications.ui.composable.NotificationsShade import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { fun TransitionBuilder.toNotificationsShadeTransition( translateClock: Boolean = false, durationScale: Double = 1.0, ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) // Ensure the clock isn't clipped by the shade outline during the transition from lockscreen. sharedElement( ClockElementKeys.smallClockElementKey, elevateInContent = Overlays.NotificationsShade, ) scaleSize(OverlayShade.Elements.Panel, height = 0f) // TODO(b/376659778): This is a temporary hack to have a shared element transition with the // lockscreen clock. Remove once nested STLs are supported. if (!translateClock) { translate(ClockElementKeys.smallClockElementKey) } // Avoid translating the status bar with the shade panel. translate(NotificationsShade.Elements.StatusBar) // Slide in the shade panel from the top edge. translate(OverlayShade.Elements.Panel, Edge.Top) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } fractionRange(start = .5f) { fade(ShadeHeader.Elements.Clock) fade(ShadeHeader.Elements.ExpandedContent) fade(ShadeHeader.Elements.PrivacyChip) fade(Notifications.Elements.NotificationScrim) } fractionRange(start = .5f) { fade(Notifications.Elements.NotificationScrim) } } private val DefaultDuration = 300.milliseconds
packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +35 −0 Original line number Diff line number Diff line Loading @@ -35,9 +35,12 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi Loading Loading @@ -121,6 +124,38 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) } @Test fun showHeader_showsOnNarrowScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(false) // Shown when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() assertThat(underTest.showHeader).isTrue() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() assertThat(underTest.showHeader).isFalse() } @Test fun showHeader_hidesOnWideScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(true) // Hidden when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() assertThat(underTest.showHeader).isFalse() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() assertThat(underTest.showHeader).isFalse() } private fun TestScope.lockDevice() { val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.powerInteractor.setAsleepForTest() Loading
packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +34 −1 Original line number Diff line number Diff line Loading @@ -16,19 +16,23 @@ package com.android.systemui.notifications.ui.viewmodel import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import com.android.app.tracing.coroutines.launchTraced as launch /** * Models UI state used to render the content of the notifications shade overlay. Loading @@ -43,10 +47,32 @@ constructor( val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") val showHeader: Boolean by hydrator.hydratedStateOf( traceName = "showHeader", initialValue = shouldShowHeader( isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, areAnyNotificationsPresent = activeNotificationsInteractor.areAnyNotificationsPresentValue, ), source = combine( shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, this::shouldShowHeader, ), ) override suspend fun onActivated(): Nothing { coroutineScope { launch { hydrator.activate() } launch { sceneInteractor.currentScene.collect { currentScene -> when (currentScene) { Loading Loading @@ -77,6 +103,13 @@ constructor( shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } private fun shouldShowHeader( isShadeLayoutWide: Boolean, areAnyNotificationsPresent: Boolean, ): Boolean { return !isShadeLayoutWide && areAnyNotificationsPresent } @AssistedFactory interface Factory { fun create(): NotificationsShadeOverlayContentViewModel Loading