Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2a84385f authored by burakov's avatar burakov
Browse files

[Dual Shade] Refactor + show the status bar clock on QS shade.

BONUS: Remove `isShadeLayout` from `ShadeHeaderViewModel`.

Fix: 397197000
Test: Added unit tests.
Test: Existing unit tests still pass.
Test: Manually by opening the quick settings shade on both narrow and
 wide screens and verifying the status bar clock is shown in both cases.
Flag: com.android.systemui.scene_container
Change-Id: I57b812607508121d6fe59b42e2b3bc490bd27b0d
parent 732243fc
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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
+16 −18
Original line number Diff line number Diff line
@@ -127,6 +127,7 @@ object ShadeHeader {
@Composable
fun ContentScope.CollapsedShadeHeader(
    viewModel: ShadeHeaderViewModel,
    isSplitShade: Boolean,
    modifier: Modifier = Modifier,
) {
    val cutoutLocation = LocalDisplayCutout.current.location
@@ -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.
@@ -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,
@@ -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,
@@ -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()
@@ -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.
@@ -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,
@@ -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) {
+5 −10
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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,
@@ -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,
@@ -340,6 +335,7 @@ private fun ContentScope.SingleShade(
            content = {
                CollapsedShadeHeader(
                    viewModel = headerViewModel,
                    isSplitShade = false,
                    modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
                )

@@ -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()
@@ -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() }),
+30 −0
Original line number Diff line number Diff line
@@ -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 {
+22 −7
Original line number Diff line number Diff line
@@ -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
@@ -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",
@@ -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

@@ -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)