diff options
21 files changed, 484 insertions, 366 deletions
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 index 931134795a53..26e7524f4fa8 100644 --- 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 @@ -29,7 +29,6 @@ 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 @@ -44,10 +43,7 @@ import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder 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 import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -58,11 +54,6 @@ class NotificationsShadeOverlay constructor( private val actionsViewModelFactory: NotificationsShadeOverlayActionsViewModel.Factory, private val contentViewModelFactory: NotificationsShadeOverlayContentViewModel.Factory, - private val tintedIconManagerFactory: TintedIconManager.Factory, - private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, - private val statusBarIconController: StatusBarIconController, - private val notificationIconContainerStatusBarViewBinder: - NotificationIconContainerStatusBarViewBinder, private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, @@ -94,18 +85,16 @@ constructor( } OverlayShade( - isShadeLayoutWide = viewModel.isShadeLayoutWide, panelAlignment = Alignment.TopStart, modifier = modifier, onScrimClicked = viewModel::onScrimClicked, header = { + val headerViewModel = + rememberViewModel("NotificationsShadeOverlayHeader") { + viewModel.shadeHeaderViewModelFactory.create() + } OverlayShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, - notificationIconContainerStatusBarViewBinder = - notificationIconContainerStatusBarViewBinder, + viewModel = headerViewModel, modifier = Modifier.element(NotificationsShade.Elements.StatusBar) .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), @@ -114,7 +103,7 @@ constructor( ) { Box { Column { - if (viewModel.showHeader) { + if (viewModel.showClock) { val burnIn = rememberBurnIn(clockInteractor) with(clockSection) { @@ -140,8 +129,7 @@ constructor( modifier = Modifier.fillMaxWidth(), ) } - // Communicates the bottom position of the drawable area within the shade to - // NSSL. + // Communicates the bottom position of the drawable area within the shade to NSSL. NotificationStackCutoffGuideline( stackScrollView = stackScrollView.get(), viewModel = placeholderViewModel, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 4bfbb3a908fa..62a8cc5a7fe3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.ui.composable -import android.view.ViewGroup import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState @@ -76,7 +75,6 @@ import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf import com.android.compose.windowsizeclass.LocalWindowSizeClass -import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.compose.modifiers.sysuiResTag @@ -100,15 +98,14 @@ import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader +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.StatusBarLocation -import com.android.systemui.statusbar.phone.ui.StatusBarIconController -import com.android.systemui.statusbar.phone.ui.TintedIconManager import dagger.Lazy import javax.inject.Inject import javax.inject.Named @@ -125,9 +122,6 @@ constructor( private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val actionsViewModelFactory: QuickSettingsUserActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory, - private val tintedIconManagerFactory: TintedIconManager.Factory, - private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, - private val statusBarIconController: StatusBarIconController, private val mediaCarouselController: MediaCarouselController, @Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost, ) : ExclusiveActivatable(), Scene { @@ -145,16 +139,26 @@ constructor( @Composable override fun ContentScope.Content(modifier: Modifier) { + val viewModel = + rememberViewModel("QuickSettingsScene-viewModel") { contentViewModelFactory.create() } + val headerViewModel = + rememberViewModel("QuickSettingsScene-headerViewModel") { + viewModel.shadeHeaderViewModelFactory.create() + } + val brightnessMirrorViewModel = + rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") { + viewModel.brightnessMirrorViewModelFactory.create() + } + val notificationsPlaceholderViewModel = + rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") { + notificationsPlaceholderViewModelFactory.create() + } QuickSettingsScene( notificationStackScrollView = notificationStackScrollView.get(), - viewModelFactory = contentViewModelFactory, - notificationsPlaceholderViewModel = - rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") { - notificationsPlaceholderViewModelFactory.create() - }, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, + viewModel = viewModel, + headerViewModel = headerViewModel, + brightnessMirrorViewModel = brightnessMirrorViewModel, + notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, mediaCarouselController = mediaCarouselController, mediaHost = mediaHost, modifier = modifier, @@ -166,23 +170,16 @@ constructor( @Composable private fun ContentScope.QuickSettingsScene( notificationStackScrollView: NotificationScrollView, - viewModelFactory: QuickSettingsSceneContentViewModel.Factory, + viewModel: QuickSettingsSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, + brightnessMirrorViewModel: BrightnessMirrorViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, shadeSession: SaveableSession, ) { val cutoutLocation = LocalDisplayCutout.current.location - - val viewModel = rememberViewModel("QuickSettingsScene-viewModel") { viewModelFactory.create() } - val brightnessMirrorViewModel = - rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") { - viewModel.brightnessMirrorViewModelFactory.create() - } val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( @@ -222,8 +219,7 @@ private fun ContentScope.QuickSettingsScene( .graphicsLayer { alpha = contentAlpha } .thenIf(shouldPunchHoleBehindScrim) { // Render the scene to an offscreen buffer so that BlendMode.DstOut only clears - // this - // scene (and not the one under it) during a scene transition. + // this scene (and not the one under it) during a scene transition. Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) } .thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() } @@ -348,21 +344,11 @@ private fun ContentScope.QuickSettingsScene( fadeOut(tween(customizingAnimationDuration)), ) { ExpandedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = - createBatteryMeterViewController, - statusBarIconController = statusBarIconController, + viewModel = headerViewModel, modifier = Modifier.padding(horizontal = 16.dp), ) } - else -> - CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, - ) + else -> CollapsedShadeHeader(viewModel = headerViewModel) } Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 3ec14a23421c..2fa370458ab0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -45,7 +45,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton @@ -67,13 +66,10 @@ import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader import com.android.systemui.shade.ui.composable.QuickSettingsOverlayHeader import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape 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 javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -84,11 +80,7 @@ class QuickSettingsShadeOverlay constructor( private val actionsViewModelFactory: QuickSettingsShadeOverlayActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsShadeOverlayContentViewModel.Factory, - private val tintedIconManagerFactory: TintedIconManager.Factory, - private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, - private val statusBarIconController: StatusBarIconController, - private val notificationIconContainerStatusBarViewBinder: - NotificationIconContainerStatusBarViewBinder, + private val quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory, private val notificationStackScrollView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, ) : Overlay { @@ -107,35 +99,35 @@ constructor( @Composable override fun ContentScope.Content(modifier: Modifier) { - val viewModel = - rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() } + val contentViewModel = + rememberViewModel("QuickSettingsShadeOverlayContent") { + contentViewModelFactory.create() + } + val quickSettingsContainerViewModel = + rememberViewModel("QuickSettingsShadeOverlayContainer") { + // TODO(b/393054014): Add support for brightness mirroring. + quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = false) + } val panelCornerRadius = with(LocalDensity.current) { OverlayShade.Dimensions.PanelCornerRadius.toPx().toInt() } - // set the bounds to null when the QuickSettings overlay disappears - DisposableEffect(Unit) { onDispose { viewModel.onPanelShapeChanged(null) } } + // Set the bounds to null when the QuickSettings overlay disappears. + DisposableEffect(Unit) { onDispose { contentViewModel.onPanelShapeChanged(null) } } Box(modifier = modifier) { SnoozeableHeadsUpNotificationSpace( stackScrollView = notificationStackScrollView.get(), viewModel = - rememberViewModel("QuickSettingsShadeOverlay") { + rememberViewModel("QuickSettingsShadeOverlayPlaceholder") { notificationsPlaceholderViewModelFactory.create() }, ) OverlayShade( - isShadeLayoutWide = viewModel.isShadeLayoutWide, panelAlignment = Alignment.TopEnd, - onScrimClicked = viewModel::onScrimClicked, + onScrimClicked = contentViewModel::onScrimClicked, header = { OverlayShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = - batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, - notificationIconContainerStatusBarViewBinder = - notificationIconContainerStatusBarViewBinder, + viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel, modifier = Modifier.element(NotificationsShade.Elements.StatusBar) .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), @@ -143,7 +135,7 @@ constructor( }, ) { ShadeBody( - viewModel = viewModel.quickSettingsContainerViewModel, + viewModel = quickSettingsContainerViewModel, modifier = Modifier.onPlaced { coordinates -> val boundsInWindow = coordinates.boundsInWindow() @@ -160,14 +152,12 @@ constructor( topRadius = 0, bottomRadius = panelCornerRadius, ) - viewModel.onPanelShapeChanged(shape) + contentViewModel.onPanelShapeChanged(shape) }, header = { - if (viewModel.isShadeLayoutWide) { + if (quickSettingsContainerViewModel.showHeader) { QuickSettingsOverlayHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createBatteryMeterViewController = - batteryMeterViewControllerFactory::create, + viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel, modifier = Modifier.padding(top = QuickSettingsShade.Dimensions.Padding), ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index fc59d40ec443..3d2d7c37ce48 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -58,29 +58,31 @@ import com.android.systemui.res.R /** Renders a lightweight shade UI container, as an overlay. */ @Composable fun ContentScope.OverlayShade( - isShadeLayoutWide: Boolean, panelAlignment: Alignment, onScrimClicked: () -> Unit, modifier: Modifier = Modifier, header: @Composable () -> Unit, content: @Composable () -> Unit, ) { + val isFullWidth = isFullWidthShade() Box(modifier) { Scrim(onClicked = onScrimClicked) - Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = panelAlignment) { + Box( + modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth), + contentAlignment = panelAlignment, + ) { Panel( - isShadeLayoutWide = isShadeLayoutWide, modifier = Modifier.overscroll(verticalOverscrollEffect) .element(OverlayShade.Elements.Panel) - .panelSize(), - header = header, + .panelWidth(isFullWidth), + header = header.takeIf { isFullWidth }, content = content, ) } - if (isShadeLayoutWide) { + if (!isFullWidth) { header() } } @@ -100,9 +102,8 @@ private fun ContentScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modif @Composable private fun ContentScope.Panel( - isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, - header: @Composable () -> Unit, + header: (@Composable () -> Unit)?, content: @Composable () -> Unit, ) { Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) { @@ -117,9 +118,7 @@ private fun ContentScope.Panel( ) Column { - if (!isShadeLayoutWide) { - header() - } + header?.invoke() // This content is intentionally rendered as a separate element from the background in // order to allow for more flexibility when defining transitions. @@ -129,14 +128,12 @@ private fun ContentScope.Panel( } @Composable -private fun Modifier.panelSize(): Modifier { - return this.then( - if (isFullWidthShade()) { - Modifier.fillMaxWidth() - } else { - Modifier.width(dimensionResource(id = R.dimen.shade_panel_width)) - } - ) +private fun Modifier.panelWidth(isFullWidthPanel: Boolean): Modifier { + return if (isFullWidthPanel) { + fillMaxWidth() + } else { + width(dimensionResource(id = R.dimen.shade_panel_width)) + } } @Composable @@ -146,27 +143,23 @@ internal fun isFullWidthShade(): Boolean { } @Composable -private fun Modifier.panelPadding(): Modifier { - val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass +private fun Modifier.panelContainerPadding(isFullWidthPanel: Boolean): Modifier { + if (isFullWidthPanel) { + return this + } val systemBars = WindowInsets.systemBarsIgnoringVisibility val displayCutout = WindowInsets.displayCutout val waterfall = WindowInsets.waterfall val horizontalPadding = PaddingValues(horizontal = dimensionResource(id = R.dimen.shade_panel_margin_horizontal)) - - val combinedPadding = + return padding( combinePaddings( systemBars.asPaddingValues(), displayCutout.asPaddingValues(), waterfall.asPaddingValues(), horizontalPadding, ) - - return if (widthSizeClass == WindowWidthSizeClass.Compact) { - padding(bottom = combinedPadding.calculateBottomPadding()) - } else { - padding(combinedPadding) - } + ) } /** Creates a union of [paddingValues] by using the max padding of each edge. */ diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index c5d28adce601..02de78bc84ce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -74,23 +74,20 @@ import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag -import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipBackground import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipHighlighted import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer -import com.android.systemui.statusbar.phone.ui.StatusBarIconController -import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel import com.android.systemui.statusbar.policy.Clock @@ -137,14 +134,9 @@ object ShadeHeader { /** The status bar that appears above the Shade scene on small screens */ @Composable fun ContentScope.CollapsedShadeHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, + viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel("CollapsedShadeHeader") { viewModelFactory.create() } - val cutoutLocation = LocalDisplayCutout.current.location val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) @@ -157,12 +149,14 @@ fun ContentScope.CollapsedShadeHeader( } } + val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() + val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() + val isShadeLayoutWide = viewModel.isShadeLayoutWide val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() - // This layout assumes it is globally positioned at (0, 0) and is the - // same size as the screen. + // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root), startContent = { @@ -171,9 +165,11 @@ fun ContentScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { - Clock(scale = 1f, viewModel = viewModel) + Clock(scale = 1f, onClick = viewModel::onClockClicked) VariableDayDate( - viewModel = viewModel, + longerDateText = longerDateText, + shorterDateText = shorterDateText, + chipHighlight = viewModel.notificationsChipHighlight, modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart), ) } @@ -202,17 +198,17 @@ fun ContentScope.CollapsedShadeHeader( if (isShadeLayoutWide) { ShadeCarrierGroup(viewModel = viewModel) } - SystemIconChip(viewModel = viewModel, isClickable = isShadeLayoutWide) { + SystemIconChip( + onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide } + ) { StatusIcons( viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, useExpandedFormat = useExpandedTextFormat, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), ) BatteryIcon( - viewModel = viewModel, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = + viewModel.createBatteryMeterViewController, useExpandedFormat = useExpandedTextFormat, modifier = Modifier.padding(vertical = 8.dp), ) @@ -226,18 +222,15 @@ fun ContentScope.CollapsedShadeHeader( /** The status bar that appears above the Quick Settings scene on small screens */ @Composable fun ContentScope.ExpandedShadeHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, + viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel("ExpandedShadeHeader") { viewModelFactory.create() } - val useExpandedFormat by remember { derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } } + val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() + val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { @@ -256,7 +249,7 @@ fun ContentScope.ExpandedShadeHeader( Box { Clock( scale = 2.57f, - viewModel = viewModel, + onClick = viewModel::onClockClicked, modifier = Modifier.align(Alignment.CenterStart), ) } @@ -275,20 +268,23 @@ fun ContentScope.ExpandedShadeHeader( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent), ) { - VariableDayDate(viewModel = viewModel, modifier = Modifier.widthIn(max = 90.dp)) + VariableDayDate( + longerDateText = longerDateText, + shorterDateText = shorterDateText, + chipHighlight = viewModel.notificationsChipHighlight, + modifier = Modifier.widthIn(max = 90.dp), + ) Spacer(modifier = Modifier.weight(1f)) - SystemIconChip(viewModel = viewModel) { + SystemIconChip { StatusIcons( viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, useExpandedFormat = useExpandedFormat, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), ) BatteryIcon( - viewModel = viewModel, useExpandedFormat = useExpandedFormat, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = + viewModel.createBatteryMeterViewController, ) } } @@ -302,15 +298,9 @@ fun ContentScope.ExpandedShadeHeader( */ @Composable fun ContentScope.OverlayShadeHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, - notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, + viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel("OverlayShadeHeader") { viewModelFactory.create() } - val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) @@ -318,8 +308,7 @@ fun ContentScope.OverlayShadeHeader( val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() - // This layout assumes it is globally positioned at (0, 0) and is the - // same size as the screen. + // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root), startContent = { @@ -330,21 +319,32 @@ fun ContentScope.OverlayShadeHeader( if (isShadeLayoutWide) { Clock( scale = 1f, - viewModel = viewModel, + onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), ) Spacer(modifier = Modifier.width(5.dp)) } - NotificationIconChip(viewModel = viewModel) { + val chipHighlight = viewModel.notificationsChipHighlight + NotificationIconChip( + chipHighlight = chipHighlight, + onClick = viewModel::onNotificationIconChipClicked, + ) { if (isShadeLayoutWide) { NotificationIcons( - viewModel = viewModel, + chipHighlight = chipHighlight, notificationIconContainerStatusBarViewBinder = - notificationIconContainerStatusBarViewBinder, + viewModel.notificationIconContainerStatusBarViewBinder, modifier = Modifier.width(IntrinsicSize.Min).height(20.dp), ) } else { - VariableDayDate(viewModel = viewModel) + val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() + val shorterDateText by + viewModel.shorterDateText.collectAsStateWithLifecycle() + VariableDayDate( + longerDateText = longerDateText, + shorterDateText = shorterDateText, + chipHighlight = viewModel.notificationsChipHighlight, + ) } } } @@ -355,20 +355,22 @@ fun ContentScope.OverlayShadeHeader( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = horizontalPadding), ) { - SystemIconChip(viewModel = viewModel, isClickable = true, showBackground = true) { + val chipHighlight = viewModel.quickSettingsChipHighlight + SystemIconChip( + chipHighlight = chipHighlight, + onClick = viewModel::onSystemIconChipClicked, + ) { StatusIcons( viewModel = viewModel, - createTintedIconManager = createTintedIconManager, - statusBarIconController = statusBarIconController, useExpandedFormat = false, - highlightable = true, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), + chipHighlight = chipHighlight, ) BatteryIcon( - viewModel = viewModel, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = + viewModel.createBatteryMeterViewController, useExpandedFormat = false, - highlightable = true, + chipHighlight = chipHighlight, ) } if (isPrivacyChipVisible) { @@ -391,13 +393,7 @@ fun ContentScope.OverlayShadeHeader( /** The header that appears at the top of the Quick Settings shade overlay. */ @Composable -fun ContentScope.QuickSettingsOverlayHeader( - viewModelFactory: ShadeHeaderViewModel.Factory, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - modifier: Modifier = Modifier, -) { - val viewModel = rememberViewModel("QuickSettingsOverlayHeader") { viewModelFactory.create() } - +fun QuickSettingsOverlayHeader(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -405,8 +401,7 @@ fun ContentScope.QuickSettingsOverlayHeader( ) { ShadeCarrierGroup(viewModel = viewModel) BatteryIcon( - viewModel = viewModel, - createBatteryMeterViewController = createBatteryMeterViewController, + createBatteryMeterViewController = viewModel.createBatteryMeterViewController, useExpandedFormat = true, ) } @@ -468,11 +463,7 @@ private fun CutoutAwareShadeHeader( } @Composable -private fun ContentScope.Clock( - scale: Float, - viewModel: ShadeHeaderViewModel, - modifier: Modifier = Modifier, -) { +private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) { val layoutDirection = LocalLayoutDirection.current Element(key = ShadeHeader.Elements.Clock, modifier = modifier) { @@ -500,18 +491,17 @@ private fun ContentScope.Clock( 0.5f, ) } - .clickable { viewModel.onClockClicked() }, + .clickable { onClick() }, ) } } @Composable private fun BatteryIcon( - viewModel: ShadeHeaderViewModel, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, useExpandedFormat: Boolean, - highlightable: Boolean = false, modifier: Modifier = Modifier, + chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, ) { val localContext = LocalContext.current val themedContext = @@ -521,8 +511,6 @@ private fun BatteryIcon( val inverseColor = Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse) - val isHighlighted = viewModel.highlightQuickSettingsIcons - AndroidView( factory = { context -> val batteryIcon = BatteryMeterView(context, null) @@ -544,18 +532,12 @@ private fun BatteryIcon( // TODO(b/298525212): use MODE_ESTIMATE in collapsed view when the screen // has no center cutout. See [QsBatteryModeController.getBatteryMode] batteryIcon.setPercentShowMode( - if (useExpandedFormat) { - BatteryMeterView.MODE_ESTIMATE - } else { - BatteryMeterView.MODE_ON - } + if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON ) - if (highlightable) { - if (isHighlighted) { - batteryIcon.updateColors(primaryColor, inverseColor, inverseColor) - } else { - batteryIcon.updateColors(primaryColor, inverseColor, primaryColor) - } + if (chipHighlight is HeaderChipHighlight.Strong) { + batteryIcon.updateColors(primaryColor, inverseColor, inverseColor) + } else if (chipHighlight is HeaderChipHighlight.Weak) { + batteryIcon.updateColors(primaryColor, inverseColor, primaryColor) } }, modifier = modifier, @@ -590,14 +572,12 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie @Composable private fun NotificationIcons( - viewModel: ShadeHeaderViewModel, + chipHighlight: HeaderChipHighlight, notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val isHighlighted = viewModel.highlightNotificationIcons - AndroidView( factory = { context -> NotificationIconContainer(context, null).also { view -> @@ -610,7 +590,7 @@ private fun NotificationIcons( } } }, - update = { it.setUseInverseOverrideIconColor(isHighlighted) }, + update = { it.setUseInverseOverrideIconColor(chipHighlight is HeaderChipHighlight.Strong) }, modifier = modifier, ) } @@ -618,11 +598,9 @@ private fun NotificationIcons( @Composable private fun ContentScope.StatusIcons( viewModel: ShadeHeaderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - statusBarIconController: StatusBarIconController, useExpandedFormat: Boolean, - highlightable: Boolean = false, modifier: Modifier = Modifier, + chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, ) { val localContext = LocalContext.current val themedContext = @@ -632,8 +610,6 @@ private fun ContentScope.StatusIcons( val inverseColor = Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse) - val isHighlighted = viewModel.highlightQuickSettingsIcons - val carrierIconSlots = listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile)) val cameraSlot = stringResource(id = com.android.internal.R.string.status_bar_camera) @@ -648,12 +624,14 @@ private fun ContentScope.StatusIcons( viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle() val iconContainer = remember { StatusIconContainer(themedContext, null) } - val iconManager = remember { createTintedIconManager(iconContainer, StatusBarLocation.QS) } + val iconManager = remember { + viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS) + } AndroidView( factory = { context -> iconManager.setTint(primaryColor, inverseColor) - statusBarIconController.addIconGroup(iconManager) + viewModel.statusBarIconController.addIconGroup(iconManager) iconContainer }, @@ -686,12 +664,10 @@ private fun ContentScope.StatusIcons( iconContainer.removeIgnoredSlot(locationSlot) } - if (highlightable) { - if (isHighlighted) { - iconManager.setTint(inverseColor, primaryColor) - } else { - iconManager.setTint(primaryColor, inverseColor) - } + if (chipHighlight is HeaderChipHighlight.Strong) { + iconManager.setTint(inverseColor, primaryColor) + } else if (chipHighlight is HeaderChipHighlight.Weak) { + iconManager.setTint(primaryColor, inverseColor) } }, modifier = modifier, @@ -700,15 +676,12 @@ private fun ContentScope.StatusIcons( @Composable private fun NotificationIconChip( - viewModel: ShadeHeaderViewModel, + chipHighlight: HeaderChipHighlight, + onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } - val backgroundColor = - if (viewModel.highlightNotificationIcons) MaterialTheme.colorScheme.chipHighlighted - else MaterialTheme.colorScheme.chipBackground - Box(modifier = modifier) { Row( modifier = @@ -716,16 +689,15 @@ private fun NotificationIconChip( .clickable( interactionSource = interactionSource, indication = null, - onClick = { viewModel.onNotificationIconChipClicked() }, + onClick = { onClick() }, ) - .thenIf(DualShade.isEnabled) { - Modifier.graphicsLayer { - shape = RoundedCornerShape(25.dp) - clip = true - } - .background(backgroundColor) - .padding(horizontal = 8.dp, vertical = 4.dp) - } + .clip(RoundedCornerShape(25.dp)) + .background( + if (chipHighlight is HeaderChipHighlight.Strong) + MaterialTheme.colorScheme.chipHighlighted + else MaterialTheme.colorScheme.chipBackground + ) + .padding(horizontal = 8.dp, vertical = 4.dp) ) { content() } @@ -734,10 +706,9 @@ private fun NotificationIconChip( @Composable private fun SystemIconChip( - viewModel: ShadeHeaderViewModel, - isClickable: Boolean = false, - showBackground: Boolean = false, modifier: Modifier = Modifier, + chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, + onClick: (() -> Unit)? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } @@ -746,14 +717,14 @@ private fun SystemIconChip( Modifier.clip(RoundedCornerShape(CollapsedHeight / 4)) .background(MaterialTheme.colorScheme.onScrimDim) val backgroundColor = - if (viewModel.highlightQuickSettingsIcons) MaterialTheme.colorScheme.chipHighlighted + if (chipHighlight is HeaderChipHighlight.Strong) MaterialTheme.colorScheme.chipHighlighted else MaterialTheme.colorScheme.chipBackground Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .thenIf(showBackground) { + .thenIf(chipHighlight !is HeaderChipHighlight.None) { Modifier.graphicsLayer { shape = RoundedCornerShape(25.dp) clip = true @@ -761,11 +732,11 @@ private fun SystemIconChip( .background(backgroundColor) .padding(horizontal = 8.dp, vertical = 4.dp) } - .thenIf(isClickable) { + .thenIf(onClick != null) { Modifier.clickable( interactionSource = interactionSource, indication = null, - onClick = { viewModel.onSystemIconChipClicked() }, + onClick = { onClick?.invoke() }, ) } .thenIf(isHovered) { hoverModifier }, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index f829a0d6facf..5040490da8f6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -101,6 +101,7 @@ import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel import com.android.systemui.shade.ui.viewmodel.ShadeUserActionsViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -124,8 +125,6 @@ object Shade { object Dimensions { val HorizontalPadding = 16.dp - val ScrimOverscrollLimit = 32.dp - const val ScrimVisibilityThreshold = 5f } } @@ -160,15 +159,22 @@ constructor( override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions @Composable - override fun ContentScope.Content(modifier: Modifier) = + override fun ContentScope.Content(modifier: Modifier) { + val viewModel = + rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() } + val headerViewModel = + rememberViewModel("ShadeScene-headerViewModel") { + viewModel.shadeHeaderViewModelFactory.create() + } + val notificationsPlaceholderViewModel = + rememberViewModel("ShadeScene-notifPlaceholderViewModel") { + notificationsPlaceholderViewModelFactory.create() + } ShadeScene( notificationStackScrollView.get(), - viewModel = - rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() }, - notificationsPlaceholderViewModel = - rememberViewModel("ShadeScene-notifPlaceholderViewModel") { - notificationsPlaceholderViewModelFactory.create() - }, + viewModel = viewModel, + headerViewModel = headerViewModel, + notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, @@ -180,6 +186,7 @@ constructor( usingCollapsedLandscapeMedia = Utils.useCollapsedMediaInLandscape(LocalContext.current.resources), ) + } init { qqsMediaHost.expansion = EXPANDED @@ -196,6 +203,7 @@ constructor( private fun ContentScope.ShadeScene( notificationStackScrollView: NotificationScrollView, viewModel: ShadeSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -207,13 +215,13 @@ private fun ContentScope.ShadeScene( shadeSession: SaveableSession, usingCollapsedLandscapeMedia: Boolean, ) { - val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle() when (shadeMode) { is ShadeMode.Single -> SingleShade( notificationStackScrollView = notificationStackScrollView, viewModel = viewModel, + headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, @@ -228,10 +236,8 @@ private fun ContentScope.ShadeScene( SplitShade( notificationStackScrollView = notificationStackScrollView, viewModel = viewModel, + headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, mediaCarouselController = mediaCarouselController, mediaHost = qsMediaHost, modifier = modifier, @@ -245,6 +251,7 @@ private fun ContentScope.ShadeScene( private fun ContentScope.SingleShade( notificationStackScrollView: NotificationScrollView, viewModel: ShadeSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -332,10 +339,7 @@ private fun ContentScope.SingleShade( }, content = { CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, + viewModel = headerViewModel, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) @@ -413,10 +417,8 @@ private fun ContentScope.SingleShade( private fun ContentScope.SplitShade( notificationStackScrollView: NotificationScrollView, viewModel: ShadeSceneContentViewModel, + headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, @@ -509,10 +511,7 @@ private fun ContentScope.SplitShade( Column(modifier = Modifier.fillMaxSize()) { CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, + viewModel = headerViewModel, modifier = Modifier.then(brightnessMirrorShowingModifier) .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt index 93eca86e15cf..64aada52626b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt @@ -5,17 +5,19 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.colorAttr -import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight @Composable -fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { - val longerText = viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterText = viewModel.shorterDateText.collectAsStateWithLifecycle() - +fun VariableDayDate( + longerDateText: String, + shorterDateText: String, + chipHighlight: HeaderChipHighlight, + modifier: Modifier = Modifier, +) { val textColor = - if (viewModel.highlightNotificationIcons) colorAttr(android.R.attr.textColorPrimaryInverse) + if (chipHighlight is HeaderChipHighlight.Strong) + colorAttr(android.R.attr.textColorPrimaryInverse) else colorAttr(android.R.attr.textColorPrimary) Layout( @@ -23,7 +25,7 @@ fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifi listOf( { Text( - text = longerText.value, + text = longerDateText, style = MaterialTheme.typography.bodyMedium, color = textColor, maxLines = 1, @@ -31,7 +33,7 @@ fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifi }, { Text( - text = shorterText.value, + text = shorterDateText, style = MaterialTheme.typography.bodyMedium, color = textColor, maxLines = 1, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index 675960832edc..43db50ad675f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -124,35 +124,35 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showHeader_showsOnNarrowScreen() = + fun showClock_showsOnNarrowScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(false) // Shown when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() - assertThat(underTest.showHeader).isTrue() + assertThat(underTest.showClock).isTrue() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() - assertThat(underTest.showHeader).isFalse() + assertThat(underTest.showClock).isFalse() } @Test - fun showHeader_hidesOnWideScreen() = + fun showClock_hidesOnWideScreen() = testScope.runTest { kosmos.shadeRepository.setShadeLayoutWide(true) // Hidden when notifications are present. kosmos.activeNotificationListRepository.setActiveNotifs(1) runCurrent() - assertThat(underTest.showHeader).isFalse() + assertThat(underTest.showClock).isFalse() // Hidden when notifications are not present. kosmos.activeNotificationListRepository.setActiveNotifs(0) runCurrent() - assertThat(underTest.showHeader).isFalse() + assertThat(underTest.showClock).isFalse() } private fun TestScope.lockDevice() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt new file mode 100644 index 000000000000..6e26fa119888 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 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.qs.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +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.qs.composefragment.dagger.usingMediaInComposeFragment +import com.android.systemui.scene.domain.startable.sceneContainerStartable +import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.shadeModeInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class QuickSettingsContainerViewModelTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { + usingMediaInComposeFragment = false // This is not for the compose fragment + } + private val testScope = kosmos.testScope + + private val shadeModeInteractor = kosmos.shadeModeInteractor + + private val underTest by lazy { + kosmos.quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = false) + } + + @Before + fun setUp() { + kosmos.sceneContainerStartable.start() + kosmos.enableDualShade() + underTest.activateIn(testScope) + } + + @Test + fun showHeader_showsOnNarrowScreen() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = false) + val isShadeLayoutWide by collectLastValue(shadeModeInteractor.isShadeLayoutWide) + assertThat(isShadeLayoutWide).isFalse() + + assertThat(underTest.showHeader).isTrue() + } + + @Test + fun showHeader_hidesOnWideScreen() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = true) + val isShadeLayoutWide by collectLastValue(shadeModeInteractor.isShadeLayoutWide) + assertThat(isShadeLayoutWide).isTrue() + + assertThat(underTest.showHeader).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt index 01714d7a4b87..b532554f5dfd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt @@ -127,24 +127,6 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showHeader_showsOnNarrowScreen() = - testScope.runTest { - kosmos.enableDualShade(wideLayout = false) - runCurrent() - - assertThat(underTest.showHeader).isTrue() - } - - @Test - fun showHeader_hidesOnWideScreen() = - testScope.runTest { - kosmos.enableDualShade(wideLayout = true) - runCurrent() - - assertThat(underTest.showHeader).isFalse() - } - - @Test fun onPanelShapeChanged() = testScope.runTest { var actual: ShadeScrimShape? = null diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 8ce20d2a05e9..d08c8a7c5974 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -23,6 +23,9 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.enableSingleShade +import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor import com.android.systemui.testKosmos @@ -273,6 +276,116 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) } + @Test + fun highlightChips_notifsOpenInSingleShade_bothNone() = + testScope.runTest { + kosmos.enableSingleShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setScene(Scenes.Shade) + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + + @Test + fun highlightChips_notifsOpenInSplitShade_bothNone() = + testScope.runTest { + kosmos.enableSplitShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setScene(Scenes.Shade) + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + + @Test + fun highlightChips_quickSettingsOpenInSingleShade_bothNone() = + testScope.runTest { + kosmos.enableSingleShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setScene(Scenes.QuickSettings) + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + assertThat(currentOverlays).isEmpty() + + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + + @Test + fun highlightChips_notifsOpenInDualShade_notifsStrongQuickSettingsWeak() = + testScope.runTest { + kosmos.enableDualShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + + // Test the lockscreen scenario. + setScene(Scenes.Lockscreen) + setOverlay(Overlays.NotificationsShade) + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + + // Test the unlocked scenario. + setDeviceEntered(true) + setScene(Scenes.Gone) + setOverlay(Overlays.NotificationsShade) + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isNotEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + } + + @Test + fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() = + testScope.runTest { + kosmos.enableDualShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + + // Test the lockscreen scenario. + setScene(Scenes.Lockscreen) + setOverlay(Overlays.QuickSettingsShade) + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + + // Test the unlocked scenario. + setDeviceEntered(true) + setScene(Scenes.Gone) + setOverlay(Overlays.QuickSettingsShade) + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isNotEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) + } + + @Test + fun highlightChips_noOverlaysInDualShade_bothNone() = + testScope.runTest { + kosmos.enableDualShade() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + + // Test the lockscreen scenario. + setScene(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + + // Test the unlocked scenario. + setDeviceEntered(true) + setScene(Scenes.Gone) + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isEmpty() + assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) + assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) + } + companion object { private val SUB_1 = SubscriptionModel( diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 8aad61a8c7cb..c7b165415aea 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -52,18 +52,11 @@ constructor( private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") - val isShadeLayoutWide: Boolean by + val showClock: Boolean by hydrator.hydratedStateOf( - traceName = "isShadeLayoutWide", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - - val showHeader: Boolean by - hydrator.hydratedStateOf( - traceName = "showHeader", + traceName = "showClock", initialValue = - shouldShowHeader( + shouldShowClock( isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, areAnyNotificationsPresent = activeNotificationsInteractor.areAnyNotificationsPresentValue, @@ -72,7 +65,7 @@ constructor( combine( shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, - this::shouldShowHeader, + this::shouldShowClock, ), ) @@ -110,7 +103,7 @@ constructor( shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } - private fun shouldShowHeader( + private fun shouldShowClock( isShadeLayoutWide: Boolean, areAnyNotificationsPresent: Boolean, ): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 85b677b65aeb..cf3b4969b07d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -573,8 +573,7 @@ constructor( onDispose { qqsVisible.value = false } } val squishiness by - viewModel.containerViewModel.quickQuickSettingsViewModel.squishinessViewModel - .squishiness + viewModel.quickQuickSettingsViewModel.squishinessViewModel.squishiness .collectAsStateWithLifecycle() Column(modifier = modifier.sysuiResTag(ResIdTags.quickQsPanel)) { @@ -607,9 +606,7 @@ constructor( ) { val Tiles = @Composable { - QuickQuickSettings( - viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel - ) + QuickQuickSettings(viewModel = viewModel.quickQuickSettingsViewModel) } val Media = @Composable { diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 219fc2fdc5ec..0dade7438720 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -59,6 +59,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel import com.android.systemui.qs.panels.ui.viewmodel.MediaInRowInLandscapeViewModel +import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -94,6 +95,7 @@ class QSFragmentComposeViewModel @AssistedInject constructor( containerViewModelFactory: QuickSettingsContainerViewModel.Factory, + quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory, @Main private val resources: Resources, footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, @@ -102,7 +104,7 @@ constructor( DisableFlagsInteractor: DisableFlagsInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator, - private val shadeInteractor: ShadeInteractor, + shadeInteractor: ShadeInteractor, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, private val squishinessInteractor: TileSquishinessInteractor, @@ -118,6 +120,8 @@ constructor( ) : Dumpable, ExclusiveActivatable() { val containerViewModel = containerViewModelFactory.create(true) + val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create() + private val qqsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QQS) private val qsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QS) @@ -475,6 +479,7 @@ constructor( } launch { hydrator.activate() } launch { containerViewModel.activate() } + launch { quickQuickSettingsViewModel.activate() } launch { qqsMediaInRowViewModel.activate() } launch { qsMediaInRowViewModel.activate() } awaitCancellation() diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt index c7db04a6b7b2..aa8e4242f64e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt @@ -19,43 +19,52 @@ package com.android.systemui.qs.ui.viewmodel import androidx.compose.runtime.getValue import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class QuickSettingsContainerViewModel @AssistedInject constructor( brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory, - quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory, shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, @Assisted supportsBrightnessMirroring: Boolean, val tileGridViewModel: TileGridViewModel, val editModeViewModel: EditModeViewModel, val detailsViewModel: DetailsViewModel, val toolbarViewModelFactory: ToolbarViewModel.Factory, + shadeModeInteractor: ShadeModeInteractor, ) : ExclusiveActivatable() { + private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator") + val brightnessSliderViewModel = brightnessSliderViewModelFactory.create(supportsBrightnessMirroring) - val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create() - val shadeHeaderViewModel = shadeHeaderViewModelFactory.create() + val showHeader: Boolean by + hydrator.hydratedStateOf( + traceName = "showHeader", + initialValue = !shadeModeInteractor.isShadeLayoutWide.value, + source = shadeModeInteractor.isShadeLayoutWide.map { !it }, + ) + override suspend fun onActivated(): Nothing { coroutineScope { + launch { hydrator.activate() } launch { brightnessSliderViewModel.activate() } - launch { quickQuickSettingsViewModel.activate() } launch { shadeHeaderViewModel.activate() } awaitCancellation() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt index d9df1ef36847..0add3f515ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt @@ -16,13 +16,10 @@ package com.android.systemui.qs.ui.viewmodel -import androidx.compose.runtime.getValue 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.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import dagger.assisted.AssistedFactory @@ -31,7 +28,6 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** @@ -46,39 +42,10 @@ constructor( val shadeInteractor: ShadeInteractor, val sceneInteractor: SceneInteractor, val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor, - val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, - quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory, ) : ExclusiveActivatable() { - private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator") - - val isShadeLayoutWide: Boolean by - hydrator.hydratedStateOf( - traceName = "isShadeLayoutWide", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - - val showHeader: Boolean by - hydrator.hydratedStateOf( - traceName = "showHeader", - initialValue = !shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide.map { !it }, - ) - - val quickSettingsContainerViewModel = quickSettingsContainerViewModelFactory.create(false) - - val showQuickSettingsOverlayHeader: Boolean by - hydrator.hydratedStateOf( - traceName = "showQuickSettingsOverlayHeader", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - override suspend fun onActivated(): Nothing { coroutineScope { - launch { hydrator.activate() } - launch { sceneInteractor.currentScene.collect { currentScene -> when (currentScene) { @@ -101,8 +68,6 @@ constructor( ) } } - - launch { quickSettingsContainerViewModel.activate() } } awaitCancellation() diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 96128df1b723..51fcf7da3c13 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -23,8 +23,10 @@ import android.icu.text.DateFormat import android.icu.text.DisplayContext import android.os.UserHandle import android.provider.Settings +import android.view.ViewGroup import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -40,6 +42,10 @@ import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import dagger.assisted.AssistedFactory @@ -67,27 +73,46 @@ constructor( val mobileIconsViewModel: MobileIconsViewModel, private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, + private val tintedIconManagerFactory: TintedIconManager.Factory, + private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, + val statusBarIconController: StatusBarIconController, + val notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, private val broadcastDispatcher: BroadcastDispatcher, ) : ExclusiveActivatable() { private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator") - val highlightNotificationIcons: Boolean by + val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager = + tintedIconManagerFactory::create + + val createBatteryMeterViewController: + (ViewGroup, StatusBarLocation) -> BatteryMeterViewController = + batteryMeterViewControllerFactory::create + + val notificationsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( - traceName = "highlightNotificationIcons", - initialValue = false, + traceName = "notificationsChipHighlight", + initialValue = HeaderChipHighlight.None, source = sceneInteractor.currentOverlays.map { overlays -> - Overlays.NotificationsShade in overlays + when { + Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Strong + Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Weak + else -> HeaderChipHighlight.None + } }, ) - val highlightQuickSettingsIcons: Boolean by + val quickSettingsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( - traceName = "highlightQuickSettingsIcons", - initialValue = false, + traceName = "quickSettingsChipHighlight", + initialValue = HeaderChipHighlight.None, source = sceneInteractor.currentOverlays.map { overlays -> - Overlays.QuickSettingsShade in overlays + when { + Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Strong + Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Weak + else -> HeaderChipHighlight.None + } }, ) @@ -225,6 +250,15 @@ constructor( ) } + /** Represents the background highlight of a header icons chip. */ + sealed interface HeaderChipHighlight { + data object None : HeaderChipHighlight + + data object Weak : HeaderChipHighlight + + data object Strong : HeaderChipHighlight + } + private fun updateDateTexts(invalidateFormats: Boolean) { if (invalidateFormats) { longerDateFormat.value = getFormatFromPattern(longerPattern) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt index 65e580cafcb5..583a9def8094 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt @@ -33,6 +33,7 @@ import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor import com.android.systemui.qs.panels.ui.viewmodel.inFirstPageViewModel import com.android.systemui.qs.panels.ui.viewmodel.mediaInRowInLandscapeViewModelFactory +import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.largeScreenHeaderHelper @@ -48,6 +49,7 @@ val Kosmos.qsFragmentComposeViewModelFactory by ): QSFragmentComposeViewModel { return QSFragmentComposeViewModel( quickSettingsContainerViewModelFactory, + quickQuickSettingsViewModelFactory, mainResources, footerActionsViewModelFactory, footerActionsController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt index f8fa5db4ddf7..3fc73cbc5552 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt @@ -20,9 +20,9 @@ import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFac import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.panels.ui.viewmodel.detailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.toolbarViewModelFactory +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory val Kosmos.quickSettingsContainerViewModelFactory by @@ -33,13 +33,13 @@ val Kosmos.quickSettingsContainerViewModelFactory by ): QuickSettingsContainerViewModel { return QuickSettingsContainerViewModel( brightnessSliderViewModelFactory, - quickQuickSettingsViewModelFactory, shadeHeaderViewModelFactory, supportsBrightnessMirroring, tileGridViewModel, editModeViewModel, detailsViewModel, toolbarViewModelFactory, + shadeModeInteractor, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt index 4f3b8f3541e1..108a20ab73d6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by @@ -28,7 +27,5 @@ val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayC shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, notificationStackAppearanceInteractor = notificationStackAppearanceInteractor, - shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, - quickSettingsContainerViewModelFactory = quickSettingsContainerViewModelFactory, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 7eb9f3472482..2be8acb845b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade.ui.viewmodel import android.content.applicationContext +import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter @@ -24,8 +25,12 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsViewModel +import org.mockito.kotlin.mock val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by Kosmos.Fixture { @@ -38,6 +43,11 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by mobileIconsViewModel = mobileIconsViewModel, privacyChipInteractor = privacyChipInteractor, clockInteractor = shadeHeaderClockInteractor, + tintedIconManagerFactory = mock<TintedIconManager.Factory>(), + batteryMeterViewControllerFactory = mock<BatteryMeterViewController.Factory>(), + statusBarIconController = mock<StatusBarIconController>(), + notificationIconContainerStatusBarViewBinder = + mock<NotificationIconContainerStatusBarViewBinder>(), broadcastDispatcher = broadcastDispatcher, ) } |