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

Commit 64d110db authored by burakov's avatar burakov Committed by Danny Burakov
Browse files

[flexiglass] Simplify shade header highlight chips rendering.

By moving the highlight chips decision directly into the corresponding
composables, we achieve simpler and more robust code. Having the chip
determined statically by the parent composable (notifications or quick
settings shade), instead of independently listening to the STL state
flow in the view model, achieves more stable rendering and removes
unwanted artifacts during transitions (see b/413766503 for an example of
such an issue).

Bonus: Eliminate the "None" chip highlight.

Bug: 413766503
Bug: 397223606
Test: Manually by expanding and collapsing the shades and observing the
 chip highlights behave as expected.
Test: ShadeHeaderViewModelTest
Flag: com.android.systemui.scene_container
Change-Id: I0ccf95aec5f42b669599544f5dbb2a2c89230a61
parent 9ae9bad4
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import com.android.systemui.scene.shared.model.Overlays
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.ShadeHeader
import com.android.systemui.shade.ui.composable.isFullWidthShade
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import dagger.Lazy
@@ -115,6 +116,8 @@ constructor(
                    }
                OverlayShadeHeader(
                    viewModel = headerViewModel,
                    notificationsHighlight = ShadeHeader.ChipHighlight.Strong,
                    quickSettingsHighlight = ShadeHeader.ChipHighlight.Weak,
                    showClock = !isFullWidth,
                    modifier = Modifier.element(NotificationsShade.Elements.StatusBar),
                )
+3 −0
Original line number Diff line number Diff line
@@ -77,6 +77,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.QuickSettingsOverlayHeader
import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.shade.ui.composable.isFullWidthShade
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
@@ -152,6 +153,8 @@ constructor(
                header = {
                    OverlayShadeHeader(
                        viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel,
                        notificationsHighlight = ShadeHeader.ChipHighlight.Weak,
                        quickSettingsHighlight = ShadeHeader.ChipHighlight.Strong,
                        showClock = true,
                        modifier = Modifier.element(QuickSettingsShade.Elements.StatusBar),
                    )
+46 −21
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -99,7 +100,6 @@ import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddi
import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingVertical
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.core.NewStatusBarIcons
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -138,13 +138,37 @@ object ShadeHeader {
        val ChipPaddingVertical = 4.dp

        val StatusBarHeight: Dp
            @Composable
            get() = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
            @Composable get() = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
    }

    object TestTags {
        const val Root = "shade_header_root"
    }

    /** Represents the background highlighting of a header icons chip. */
    sealed interface ChipHighlight {
        val backgroundColor: Color
            @Composable @ReadOnlyComposable get

        val foregroundColor: Color
            @Composable @ReadOnlyComposable get

        data object Weak : ChipHighlight {
            override val backgroundColor: Color
                @Composable get() = MaterialTheme.colorScheme.surface.copy(alpha = 0.1f)

            override val foregroundColor: Color
                @Composable get() = MaterialTheme.colorScheme.onSurface
        }

        data object Strong : ChipHighlight {
            override val backgroundColor: Color
                @Composable get() = MaterialTheme.colorScheme.primaryContainer

            override val foregroundColor: Color
                @Composable get() = MaterialTheme.colorScheme.onPrimaryContainer
        }
    }
}

/** The status bar that appears above the Shade scene on small screens */
@@ -309,6 +333,8 @@ fun ContentScope.ExpandedShadeHeader(
@Composable
fun ContentScope.OverlayShadeHeader(
    viewModel: ShadeHeaderViewModel,
    notificationsHighlight: ShadeHeader.ChipHighlight,
    quickSettingsHighlight: ShadeHeader.ChipHighlight,
    showClock: Boolean,
    modifier: Modifier = Modifier,
) {
@@ -326,7 +352,6 @@ fun ContentScope.OverlayShadeHeader(
                horizontalArrangement = Arrangement.spacedBy(5.dp),
                modifier = Modifier.padding(horizontal = horizontalPadding),
            ) {
                val chipHighlight = viewModel.notificationsChipHighlight
                if (showClock) {
                    Clock(
                        onClick = viewModel::onClockClicked,
@@ -335,7 +360,7 @@ fun ContentScope.OverlayShadeHeader(
                }
                NotificationsChip(
                    onClick = viewModel::onNotificationIconChipClicked,
                    backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme),
                    backgroundColor = notificationsHighlight.backgroundColor,
                    modifier =
                        Modifier.bouncy(
                            isEnabled = viewModel.animateNotificationsChipBounce,
@@ -350,7 +375,7 @@ fun ContentScope.OverlayShadeHeader(
                    VariableDayDate(
                        longerDateText = viewModel.longerDateText,
                        shorterDateText = viewModel.shorterDateText,
                        textColor = chipHighlight.foregroundColor(MaterialTheme.colorScheme),
                        textColor = notificationsHighlight.foregroundColor,
                    )
                }
            }
@@ -361,9 +386,8 @@ fun ContentScope.OverlayShadeHeader(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.padding(horizontal = horizontalPadding),
            ) {
                val chipHighlight = viewModel.quickSettingsChipHighlight
                SystemIconChip(
                    backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme),
                    backgroundColor = quickSettingsHighlight.backgroundColor,
                    onClick = viewModel::onSystemIconChipClicked,
                    modifier =
                        Modifier.bouncy(
@@ -380,16 +404,18 @@ fun ContentScope.OverlayShadeHeader(
                        with(LocalDensity.current) {
                            (if (NewStatusBarIcons.isEnabled) 3.sp else 6.sp).toDp()
                        }
                    val isHighlighted = quickSettingsHighlight is ShadeHeader.ChipHighlight.Strong
                    StatusIcons(
                        viewModel = viewModel,
                        useExpandedFormat = false,
                        modifier = Modifier.padding(end = paddingEnd).weight(1f, fill = false),
                        isHighlighted = isHighlighted,
                    )
                    BatteryIcon(
                        createBatteryMeterViewController =
                            viewModel.createBatteryMeterViewController,
                        useExpandedFormat = false,
                        chipHighlight = chipHighlight,
                        isHighlighted = isHighlighted,
                    )
                }
                if (isPrivacyChipVisible) {
@@ -523,7 +549,7 @@ private fun BatteryIcon(
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
    useExpandedFormat: Boolean,
    modifier: Modifier = Modifier,
    chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None,
    isHighlighted: Boolean = false,
) {
    val localContext = LocalContext.current
    val themedContext =
@@ -557,11 +583,11 @@ private fun BatteryIcon(
                if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON
            )
            // TODO(b/397223606): Get the actual spec for this.
            if (chipHighlight is HeaderChipHighlight.Strong) {
                batteryIcon.updateColors(primaryColor, inverseColor, inverseColor)
            } else if (chipHighlight is HeaderChipHighlight.Weak) {
                batteryIcon.updateColors(primaryColor, inverseColor, primaryColor)
            }
            batteryIcon.updateColors(
                primaryColor,
                inverseColor,
                if (isHighlighted) inverseColor else primaryColor,
            )
        },
        modifier = modifier,
    )
@@ -642,6 +668,7 @@ private fun ContentScope.StatusIcons(
    viewModel: ShadeHeaderViewModel,
    useExpandedFormat: Boolean,
    modifier: Modifier = Modifier,
    isHighlighted: Boolean = false,
) {
    val localContext = LocalContext.current
    val themedContext =
@@ -672,11 +699,9 @@ private fun ContentScope.StatusIcons(
        viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS)
    }

    val chipHighlight = viewModel.quickSettingsChipHighlight

    // TODO(408001821): Use composable system status icons here instead.
    AndroidView(
        factory = { context ->
        factory = {
            iconManager.setTint(primaryColor, inverseColor)
            viewModel.statusBarIconController.addIconGroup(iconManager)

@@ -712,9 +737,9 @@ private fun ContentScope.StatusIcons(
            }

            // TODO(b/397223606): Get the actual spec for this.
            if (chipHighlight is HeaderChipHighlight.Strong) {
            if (isHighlighted) {
                iconManager.setTint(inverseColor, primaryColor)
            } else if (chipHighlight is HeaderChipHighlight.Weak) {
            } else {
                iconManager.setTint(primaryColor, inverseColor)
            }
        },
@@ -726,7 +751,7 @@ private fun ContentScope.StatusIcons(
private fun NotificationsChip(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    backgroundColor: Color = Color.Unspecified,
    backgroundColor: Color,
    content: @Composable BoxScope.() -> Unit,
) {
    val interactionSource = remember { MutableInteractionSource() }
+0 −88
Original line number Diff line number Diff line
@@ -15,7 +15,6 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
@@ -25,11 +24,8 @@ 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.domain.interactor.shadeMode
import com.android.systemui.shade.shared.model.ShadeMode
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
@@ -248,90 +244,6 @@ 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 {
            // Test the lockscreen scenario.
            setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
            assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
            assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)

            // Test the unlocked scenario.
            setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade)
            assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
            assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
        }

    @Test
    fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() =
        testScope.runTest {
            // Test the lockscreen scenario.
            setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
            assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
            assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)

            // Test the unlocked scenario.
            setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade)
            assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
            assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
        }

    @Test
    fun highlightChips_noOverlaysInDualShade_bothNone() =
        testScope.runTest {
            // Test the lockscreen scenario.
            setupDualShadeState(scene = Scenes.Lockscreen)
            assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
            assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)

            // Test the unlocked scenario.
            setupDualShadeState(scene = Scenes.Gone)
            assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
            assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
        }

    companion object {
        private val SUB_1 =
            SubscriptionModel(
+3 −62
Original line number Diff line number Diff line
@@ -24,9 +24,7 @@ import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.provider.Settings
import android.view.ViewGroup
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntRect
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.battery.BatteryMeterViewController
@@ -98,34 +96,6 @@ constructor(
        (ViewGroup, StatusBarLocation) -> BatteryMeterViewController =
        batteryMeterViewControllerFactory::create

    val notificationsChipHighlight: HeaderChipHighlight by
        hydrator.hydratedStateOf(
            traceName = "notificationsChipHighlight",
            initialValue = HeaderChipHighlight.None,
            source =
                sceneInteractor.currentOverlays.map { overlays ->
                    when {
                        Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Strong
                        Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Weak
                        else -> HeaderChipHighlight.None
                    }
                },
        )

    val quickSettingsChipHighlight: HeaderChipHighlight by
        hydrator.hydratedStateOf(
            traceName = "quickSettingsChipHighlight",
            initialValue = HeaderChipHighlight.None,
            source =
                sceneInteractor.currentOverlays.map { overlays ->
                    when {
                        Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Strong
                        Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Weak
                        else -> HeaderChipHighlight.None
                    }
                },
        )

    /** True if there is exactly one mobile connection. */
    val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier

@@ -264,39 +234,10 @@ constructor(
        dualShadeEducationInteractor.onDualShadeEducationElementBoundsChange(element, bounds)
    }

    /** Represents the background highlight of a header icons chip. */
    sealed interface HeaderChipHighlight {

        fun backgroundColor(colorScheme: ColorScheme): Color

        fun foregroundColor(colorScheme: ColorScheme): Color

        data object None : HeaderChipHighlight {
            override fun backgroundColor(colorScheme: ColorScheme): Color = Color.Unspecified

            override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary
        }

        data object Weak : HeaderChipHighlight {
            override fun backgroundColor(colorScheme: ColorScheme): Color =
                colorScheme.surface.copy(alpha = 0.1f)

            override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.onSurface
        }

        data object Strong : HeaderChipHighlight {
            override fun backgroundColor(colorScheme: ColorScheme): Color =
                colorScheme.primaryContainer

            override fun foregroundColor(colorScheme: ColorScheme): Color =
                colorScheme.onPrimaryContainer
        }
    }

    private fun getFormatFromPattern(pattern: String?): DateFormat {
        val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault())
        format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
        return format
        return DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()).apply {
            setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
        }
    }

    @AssistedFactory