Loading packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +2 −1 Original line number Diff line number Diff line Loading @@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene( modifier = Modifier.padding(horizontal = 16.dp), ) } else -> CollapsedShadeHeader(viewModel = headerViewModel) else -> CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false) } Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding Loading packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +16 −18 Original line number Diff line number Diff line Loading @@ -127,6 +127,7 @@ object ShadeHeader { @Composable fun ContentScope.CollapsedShadeHeader( viewModel: ShadeHeaderViewModel, isSplitShade: Boolean, modifier: Modifier = Modifier, ) { val cutoutLocation = LocalDisplayCutout.current.location Loading @@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader( } } 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. Loading @@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { Clock(scale = 1f, onClick = viewModel::onClockClicked) Clock(onClick = viewModel::onClockClicked) VariableDayDate( longerDateText = viewModel.longerDateText, shorterDateText = viewModel.shorterDateText, Loading Loading @@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader( Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) .padding(horizontal = horizontalPadding), ) { if (isShadeLayoutWide) { if (isSplitShade) { ShadeCarrierGroup(viewModel = viewModel) } SystemIconChip( onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide } onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade } ) { StatusIcons( viewModel = viewModel, Loading Loading @@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader( .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight), ) { Box(modifier = Modifier.fillMaxWidth()) { Box { Clock( scale = 2.57f, onClick = viewModel::onClockClicked, modifier = Modifier.align(Alignment.CenterStart), scale = 2.57f, ) } Box( modifier = Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth() Loading Loading @@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader( val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) 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. Loading @@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader( startContent = { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { val chipHighlight = viewModel.notificationsChipHighlight if (isShadeLayoutWide) { if (viewModel.showClock) { Clock( scale = 1f, onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), ) Spacer(modifier = Modifier.width(5.dp)) } NotificationsChip( onClick = viewModel::onNotificationIconChipClicked, Loading Loading @@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader( } @Composable private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) { private fun ContentScope.Clock( onClick: () -> Unit, modifier: Modifier = Modifier, scale: Float = 1f, ) { val layoutDirection = LocalLayoutDirection.current ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) { Loading packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +5 −10 Original line number Diff line number Diff line Loading @@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey Loading @@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateContentDpAsState import com.android.compose.animation.scene.animateContentFloatAsState import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.padding Loading Loading @@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene( viewModel = viewModel, headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, mediaCarouselController = mediaCarouselController, mediaHost = qqsMediaHost, modifier = modifier, Loading Loading @@ -253,9 +251,6 @@ private fun ContentScope.SingleShade( viewModel: ShadeSceneContentViewModel, headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, Loading Loading @@ -340,6 +335,7 @@ private fun ContentScope.SingleShade( content = { CollapsedShadeHeader( viewModel = headerViewModel, isSplitShade = false, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) Loading Loading @@ -434,15 +430,13 @@ private fun ContentScope.SplitShade( val footerActionsViewModel = remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } val tileSquishiness by animateSceneFloatAsState( animateContentFloatAsState( value = 1f, key = QuickSettings.SharedValues.TilesSquishiness, canOverflow = false, ) val unfoldTranslationXForStartSide by viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f) val unfoldTranslationXForEndSide by viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f) val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings) val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() Loading Loading @@ -512,6 +506,7 @@ private fun ContentScope.SplitShade( Column(modifier = Modifier.fillMaxSize()) { CollapsedShadeHeader( viewModel = headerViewModel, isSplitShade = true, modifier = Modifier.then(brightnessMirrorShowingModifier) .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }), Loading packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +30 −0 Original line number Diff line number Diff line Loading @@ -93,6 +93,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { ) } @Test fun showClock_wideLayout_returnsTrue() = testScope.runTest { kosmos.enableDualShade(wideLayout = true) setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) assertThat(underTest.showClock).isTrue() setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) assertThat(underTest.showClock).isTrue() } @Test fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() = testScope.runTest { kosmos.enableDualShade(wideLayout = false) setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) assertThat(underTest.showClock).isFalse() } @Test fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() = testScope.runTest { kosmos.enableDualShade(wideLayout = false) setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) assertThat(underTest.showClock).isTrue() } @Test fun onShadeCarrierGroupClicked_launchesNetworkSettings() = testScope.runTest { Loading packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +22 −7 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.OverlayKey import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator Loading Loading @@ -86,6 +87,22 @@ constructor( (ViewGroup, StatusBarLocation) -> BatteryMeterViewController = batteryMeterViewControllerFactory::create val showClock: Boolean by hydrator.hydratedStateOf( traceName = "showClock", initialValue = shouldShowClock( isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, overlays = sceneInteractor.currentOverlays.value, ), source = combine( shadeInteractor.isShadeLayoutWide, sceneInteractor.currentOverlays, ::shouldShowClock, ), ) val notificationsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( traceName = "notificationsChipHighlight", Loading Loading @@ -114,13 +131,6 @@ constructor( }, ) val isShadeLayoutWide: Boolean by hydrator.hydratedStateOf( traceName = "isShadeLayoutWide", initialValue = shadeInteractor.isShadeLayoutWide.value, source = shadeInteractor.isShadeLayoutWide, ) /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier Loading Loading @@ -271,6 +281,11 @@ constructor( } } private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean { // Notifications shade on narrow layout renders its own clock. Hide the header clock. return isShadeLayoutWide || Overlays.NotificationsShade !in overlays } private fun getFormatFromPattern(pattern: String?): DateFormat { val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()) format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE) Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +2 −1 Original line number Diff line number Diff line Loading @@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene( modifier = Modifier.padding(horizontal = 16.dp), ) } else -> CollapsedShadeHeader(viewModel = headerViewModel) else -> CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false) } Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding Loading
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +16 −18 Original line number Diff line number Diff line Loading @@ -127,6 +127,7 @@ object ShadeHeader { @Composable fun ContentScope.CollapsedShadeHeader( viewModel: ShadeHeaderViewModel, isSplitShade: Boolean, modifier: Modifier = Modifier, ) { val cutoutLocation = LocalDisplayCutout.current.location Loading @@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader( } } 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. Loading @@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { Clock(scale = 1f, onClick = viewModel::onClockClicked) Clock(onClick = viewModel::onClockClicked) VariableDayDate( longerDateText = viewModel.longerDateText, shorterDateText = viewModel.shorterDateText, Loading Loading @@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader( Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) .padding(horizontal = horizontalPadding), ) { if (isShadeLayoutWide) { if (isSplitShade) { ShadeCarrierGroup(viewModel = viewModel) } SystemIconChip( onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide } onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade } ) { StatusIcons( viewModel = viewModel, Loading Loading @@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader( .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight), ) { Box(modifier = Modifier.fillMaxWidth()) { Box { Clock( scale = 2.57f, onClick = viewModel::onClockClicked, modifier = Modifier.align(Alignment.CenterStart), scale = 2.57f, ) } Box( modifier = Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth() Loading Loading @@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader( val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) 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. Loading @@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader( startContent = { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { val chipHighlight = viewModel.notificationsChipHighlight if (isShadeLayoutWide) { if (viewModel.showClock) { Clock( scale = 1f, onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), ) Spacer(modifier = Modifier.width(5.dp)) } NotificationsChip( onClick = viewModel::onNotificationIconChipClicked, Loading Loading @@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader( } @Composable private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) { private fun ContentScope.Clock( onClick: () -> Unit, modifier: Modifier = Modifier, scale: Float = 1f, ) { val layoutDirection = LocalLayoutDirection.current ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) { Loading
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +5 −10 Original line number Diff line number Diff line Loading @@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey Loading @@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateContentDpAsState import com.android.compose.animation.scene.animateContentFloatAsState import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.padding Loading Loading @@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene( viewModel = viewModel, headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, mediaCarouselController = mediaCarouselController, mediaHost = qqsMediaHost, modifier = modifier, Loading Loading @@ -253,9 +251,6 @@ private fun ContentScope.SingleShade( viewModel: ShadeSceneContentViewModel, headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, Loading Loading @@ -340,6 +335,7 @@ private fun ContentScope.SingleShade( content = { CollapsedShadeHeader( viewModel = headerViewModel, isSplitShade = false, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) Loading Loading @@ -434,15 +430,13 @@ private fun ContentScope.SplitShade( val footerActionsViewModel = remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } val tileSquishiness by animateSceneFloatAsState( animateContentFloatAsState( value = 1f, key = QuickSettings.SharedValues.TilesSquishiness, canOverflow = false, ) val unfoldTranslationXForStartSide by viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f) val unfoldTranslationXForEndSide by viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f) val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings) val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() Loading Loading @@ -512,6 +506,7 @@ private fun ContentScope.SplitShade( Column(modifier = Modifier.fillMaxSize()) { CollapsedShadeHeader( viewModel = headerViewModel, isSplitShade = true, modifier = Modifier.then(brightnessMirrorShowingModifier) .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }), Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +30 −0 Original line number Diff line number Diff line Loading @@ -93,6 +93,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { ) } @Test fun showClock_wideLayout_returnsTrue() = testScope.runTest { kosmos.enableDualShade(wideLayout = true) setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) assertThat(underTest.showClock).isTrue() setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) assertThat(underTest.showClock).isTrue() } @Test fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() = testScope.runTest { kosmos.enableDualShade(wideLayout = false) setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) assertThat(underTest.showClock).isFalse() } @Test fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() = testScope.runTest { kosmos.enableDualShade(wideLayout = false) setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) assertThat(underTest.showClock).isTrue() } @Test fun onShadeCarrierGroupClicked_launchesNetworkSettings() = testScope.runTest { Loading
packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +22 −7 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.OverlayKey import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator Loading Loading @@ -86,6 +87,22 @@ constructor( (ViewGroup, StatusBarLocation) -> BatteryMeterViewController = batteryMeterViewControllerFactory::create val showClock: Boolean by hydrator.hydratedStateOf( traceName = "showClock", initialValue = shouldShowClock( isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, overlays = sceneInteractor.currentOverlays.value, ), source = combine( shadeInteractor.isShadeLayoutWide, sceneInteractor.currentOverlays, ::shouldShowClock, ), ) val notificationsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( traceName = "notificationsChipHighlight", Loading Loading @@ -114,13 +131,6 @@ constructor( }, ) val isShadeLayoutWide: Boolean by hydrator.hydratedStateOf( traceName = "isShadeLayoutWide", initialValue = shadeInteractor.isShadeLayoutWide.value, source = shadeInteractor.isShadeLayoutWide, ) /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier Loading Loading @@ -271,6 +281,11 @@ constructor( } } private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean { // Notifications shade on narrow layout renders its own clock. Hide the header clock. return isShadeLayoutWide || Overlays.NotificationsShade !in overlays } private fun getFormatFromPattern(pattern: String?): DateFormat { val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()) format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE) Loading