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

Commit f7923ee9 authored by 0's avatar 0 Committed by Shawn Lee
Browse files

[flexiglass] Implement ShadeHeader status icon hover state

Bug: 298524053
Test: manually verified hover and click behavior with bluetooth mouse
Test: added unit tests
Flag: com.android.systemui.scene_container
Change-Id: I5502d9c2f456afcca171fcdc414b9920bbcd33be
parent 0c1aab59
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -59,6 +59,13 @@ val SceneContainerTransitions = transitions {
        goneToShadeTransition(durationScale = 0.9)
    }
    from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
    from(
        Scenes.Gone,
        to = Scenes.QuickSettings,
        key = SlightlyFasterShadeCollapse,
    ) {
        goneToQuickSettingsTransition(durationScale = 0.9)
    }
    from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
    from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
    from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
+46 −15
Original line number Diff line number Diff line
@@ -19,7 +19,10 @@ package com.android.systemui.shade.ui.composable

import android.view.ContextThemeWrapper
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -32,7 +35,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -40,6 +45,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
@@ -58,6 +64,7 @@ import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.TransitionState
import com.android.compose.animation.scene.ValueKey
import com.android.compose.animation.scene.animateElementFloatAsState
import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.settingslib.Utils
import com.android.systemui.battery.BatteryMeterView
@@ -69,6 +76,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag
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.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
@@ -79,7 +87,6 @@ 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
import kotlin.math.max

object ShadeHeader {
    object Elements {
@@ -103,6 +110,8 @@ object ShadeHeader {
    object Colors {
        val ColorScheme.shadeHeaderText: Color
            get() = Color.White
        val ColorScheme.onScrimDim: Color
            get() = Color.DarkGray
    }

    object TestTags {
@@ -130,7 +139,7 @@ fun SceneScope.CollapsedShadeHeader(
    val horizontalPadding =
        max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)

    val useExpandedFormat by
    val useExpandedTextFormat by
        remember(cutoutLocation) {
            derivedStateOf {
                cutoutLocation != CutoutLocation.CENTER ||
@@ -138,6 +147,10 @@ fun SceneScope.CollapsedShadeHeader(
            }
        }

    val isLargeScreenLayout =
            LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Medium ||
                    LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Expanded

    val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()

    // This layout assumes it is globally positioned at (0, 0) and is the
@@ -182,22 +195,22 @@ fun SceneScope.CollapsedShadeHeader(
                                Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
                                    .padding(horizontal = horizontalPadding)
                        ) {
                            SystemIconContainer(
                                modifier = Modifier.align(Alignment.CenterVertically)
                            ) {
                                when (LocalWindowSizeClass.current.widthSizeClass) {
                                    WindowWidthSizeClass.Medium,
                                    WindowWidthSizeClass.Expanded ->
                            if (isLargeScreenLayout) {
                                ShadeCarrierGroup(
                                        viewModel = viewModel,
                                        modifier = Modifier.align(Alignment.CenterVertically),
                                )
                            }
                            SystemIconContainer(
                                viewModel = viewModel,
                                isClickable = isLargeScreenLayout,
                                modifier = Modifier.align(Alignment.CenterVertically)
                            ) {
                                StatusIcons(
                                    viewModel = viewModel,
                                    createTintedIconManager = createTintedIconManager,
                                    statusBarIconController = statusBarIconController,
                                    useExpandedFormat = useExpandedFormat,
                                    useExpandedFormat = useExpandedTextFormat,
                                    modifier =
                                        Modifier.align(Alignment.CenterVertically)
                                            .padding(end = 6.dp)
@@ -206,7 +219,7 @@ fun SceneScope.CollapsedShadeHeader(
                                BatteryIcon(
                                    createBatteryMeterViewController =
                                        createBatteryMeterViewController,
                                    useExpandedFormat = useExpandedFormat,
                                    useExpandedFormat = useExpandedTextFormat,
                                    modifier = Modifier.align(Alignment.CenterVertically),
                                )
                            }
@@ -322,7 +335,7 @@ fun SceneScope.ExpandedShadeHeader(
                    modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
                )
                Spacer(modifier = Modifier.weight(1f))
                SystemIconContainer {
                SystemIconContainer(viewModel = viewModel, isClickable = false) {
                    StatusIcons(
                        viewModel = viewModel,
                        createTintedIconManager = createTintedIconManager,
@@ -531,12 +544,30 @@ private fun SceneScope.StatusIcons(

@Composable
private fun SystemIconContainer(
    viewModel: ShadeHeaderViewModel,
    isClickable: Boolean,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    // TODO(b/298524053): add hover state for this container
    val interactionSource = remember { MutableInteractionSource() }
    val isHovered by interactionSource.collectIsHoveredAsState()

    val hoverModifier = Modifier
            .clip(RoundedCornerShape(CollapsedHeight / 4))
            .background(MaterialTheme.colorScheme.onScrimDim)

    Row(
        modifier = modifier.height(CollapsedHeight),
        modifier = modifier
                .height(CollapsedHeight)
                .padding(vertical = CollapsedHeight / 4)
                .thenIf(isClickable) {
                    Modifier.clickable(
                            interactionSource = interactionSource,
                            indication = null,
                            onClick = { viewModel.onSystemIconContainerClicked() },
                    )
                }
                .thenIf(isHovered) { hoverModifier },
        content = content,
    )
}
+66 −0
Original line number Diff line number Diff line
@@ -6,15 +6,27 @@ import android.provider.Settings
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
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.testScope
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
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
import com.android.systemui.util.mockito.argThat
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -24,12 +36,16 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class ShadeHeaderViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
    private val sceneInteractor = kosmos.sceneInteractor
    private val deviceEntryInteractor = kosmos.deviceEntryInteractor

    private val underTest: ShadeHeaderViewModel = kosmos.shadeHeaderViewModel

@@ -77,6 +93,30 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
                )
        }

    @Test
    fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() =
        testScope.runTest {
            setDeviceEntered(false)
            setScene(Scenes.Shade)

            underTest.onSystemIconContainerClicked()
            runCurrent()

            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
        }

    @Test
    fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() =
            testScope.runTest {
                setDeviceEntered(true)
                setScene(Scenes.Shade)

                underTest.onSystemIconContainerClicked()
                runCurrent()

                assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
            }

    companion object {
        private val SUB_1 =
            SubscriptionModel(
@@ -93,6 +133,32 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
                profileClass = PROFILE_CLASS_UNSET,
            )
    }

    private fun setScene(key: SceneKey) {
        sceneInteractor.changeScene(key, "test")
        sceneInteractor.setTransitionState(
                MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
        )
        testScope.runCurrent()
    }

    private fun TestScope.setDeviceEntered(isEntered: Boolean) {
        if (isEntered) {
            // Unlock the device marking the device has entered.
            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                    SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()
        }
        setScene(
                if (isEntered) {
                    Scenes.Gone
                } else {
                    Scenes.Lockscreen
                }
        )
        assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
    }
}

private class IntentMatcherAction(private val action: String) : ArgumentMatcher<Intent> {
+13 −0
Original line number Diff line number Diff line
@@ -30,6 +30,9 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.TransitionKeys
import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -57,6 +60,7 @@ constructor(
    @Application private val applicationScope: CoroutineScope,
    context: Context,
    private val activityStarter: ActivityStarter,
    private val sceneInteractor: SceneInteractor,
    shadeInteractor: ShadeInteractor,
    mobileIconsInteractor: MobileIconsInteractor,
    val mobileIconsViewModel: MobileIconsViewModel,
@@ -139,6 +143,15 @@ constructor(
        clockInteractor.launchClockActivity()
    }

    /** Notifies that the system icons container was clicked. */
    fun onSystemIconContainerClicked() {
        sceneInteractor.changeScene(
            SceneFamilies.Home,
            "ShadeHeaderViewModel.onSystemIconContainerClicked",
            TransitionKeys.SlightlyFasterShadeCollapse,
        )
    }

    /** Notifies that the shadeCarrierGroup was clicked. */
    fun onShadeCarrierGroupClicked() {
        activityStarter.postStartActivityDismissingKeyguard(
+2 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.activityStarter
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
@@ -33,6 +34,7 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by
            applicationScope = applicationCoroutineScope,
            context = applicationContext,
            activityStarter = activityStarter,
            sceneInteractor = sceneInteractor,
            shadeInteractor = shadeInteractor,
            mobileIconsInteractor = mobileIconsInteractor,
            mobileIconsViewModel = mobileIconsViewModel,