Merge "[flexiglass] Implement ShadeHeader status icon hover state" into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 10c4030..68395b4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -59,6 +59,13 @@
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() }
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 ac3e015..b5a10ca 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
@@ -19,7 +19,10 @@
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.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.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.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.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.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 Colors {
val ColorScheme.shadeHeaderText: Color
get() = Color.White
+ val ColorScheme.onScrimDim: Color
+ get() = Color.DarkGray
}
object TestTags {
@@ -130,7 +139,7 @@
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 @@
}
}
+ 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 @@
Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
.padding(horizontal = horizontalPadding)
) {
+ if (isLargeScreenLayout) {
+ ShadeCarrierGroup(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
SystemIconContainer(
+ viewModel = viewModel,
+ isClickable = isLargeScreenLayout,
modifier = Modifier.align(Alignment.CenterVertically)
) {
- when (LocalWindowSizeClass.current.widthSizeClass) {
- WindowWidthSizeClass.Medium,
- WindowWidthSizeClass.Expanded ->
- ShadeCarrierGroup(
- viewModel = viewModel,
- 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 @@
BatteryIcon(
createBatteryMeterViewController =
createBatteryMeterViewController,
- useExpandedFormat = useExpandedFormat,
+ useExpandedFormat = useExpandedTextFormat,
modifier = Modifier.align(Alignment.CenterVertically),
)
}
@@ -322,7 +335,7 @@
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 @@
@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,
)
}
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 f89f18a..3ded8a3 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
@@ -6,15 +6,27 @@
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.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 @@
)
}
+ @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 @@
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> {
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 6c76061..b2e0cd0 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
@@ -30,6 +30,9 @@
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 @@
@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 @@
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(
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 8d653f7..0e21698 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
@@ -21,6 +21,7 @@
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 @@
applicationScope = applicationCoroutineScope,
context = applicationContext,
activityStarter = activityStarter,
+ sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,