summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Shawn Lee <syeonlee@google.com> 2023-08-30 18:56:29 -0700
committer Shawn Lee <syeonlee@google.com> 2023-09-06 16:50:22 +0000
commit7f4a2552ae1033c0e997dc5f033f95955eed773d (patch)
treeaf9805a6d712e78fe68204d0b7b69cdb0bae139e
parent9dbc7bee8382e44926dc77b5201b427baba4e393 (diff)
[flexiglass] Rewrite Shade Header in Compose and migrate it to flexiglass
Reimplemented the shade header UI in Compose and inserted it into the Shade and QS scenes. The day/date view and ShadeCarrierGroup were rewritten; all other children were dropped in as AndroidViews for now. The only visual difference between the new implementation and the old is the lack of the Clock animation during the Shade -> QS transition, which is blocked by the dynamics framework not yet supporting shared text animations. Additionally, camera cutout handling for the collapsed state has not yet been implemented. Bug: 296251116 Test: new viewmodel tests Test: see video attached to bug Change-Id: I7bbc20ecc90c9c4c1b3b82d9e054b1707890cd35
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt43
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt8
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt314
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt33
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java47
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt134
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt47
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt92
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt155
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt58
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt1
17 files changed, 1046 insertions, 38 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index e5cd4397166e..7ac39011d4da 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -16,15 +16,19 @@
package com.android.systemui.qs.ui.composable
+import android.view.ViewGroup
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
@@ -33,6 +37,10 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
+import com.android.systemui.statusbar.phone.StatusBarLocation
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +52,9 @@ class QuickSettingsScene
@Inject
constructor(
private val viewModel: QuickSettingsSceneViewModel,
+ private val tintedIconManagerFactory: TintedIconManager.Factory,
+ private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
+ private val statusBarIconController: StatusBarIconController,
) : ComposableScene {
override val key = SceneKey.QuickSettings
@@ -61,6 +72,9 @@ constructor(
) {
QuickSettingsScene(
viewModel = viewModel,
+ createTintedIconManager = tintedIconManagerFactory::create,
+ createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
+ statusBarIconController = statusBarIconController,
modifier = modifier,
)
}
@@ -69,16 +83,27 @@ constructor(
@Composable
private fun SceneScope.QuickSettingsScene(
viewModel: QuickSettingsSceneViewModel,
+ createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
+ createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
+ statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
// TODO(b/280887232): implement the real UI.
-
- Box(
- modifier
- .fillMaxSize()
- .clickable(onClick = { viewModel.onContentClicked() })
- .padding(horizontal = 16.dp, vertical = 48.dp)
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ modifier
+ .fillMaxSize()
+ .clickable(onClick = { viewModel.onContentClicked() })
+ .padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
) {
- QuickSettings(modifier = Modifier.fillMaxHeight())
+ ExpandedShadeHeader(
+ viewModel = viewModel.shadeHeaderViewModel,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ QuickSettings()
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
index 21a10b1bc936..be85beea6ee0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
@@ -5,10 +5,18 @@ import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.QuickSettings
+import com.android.systemui.shade.ui.composable.ShadeHeader
fun TransitionBuilder.shadeToQuickSettingsTransition() {
spec = tween(durationMillis = 500)
translate(Notifications.Elements.Notifications, Edge.Bottom)
timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }
+
+ translate(ShadeHeader.Elements.CollapsedContent, y = ShadeHeader.Dimensions.CollapsedHeight)
+ translate(ShadeHeader.Elements.ExpandedContent, y = (-ShadeHeader.Dimensions.ExpandedHeight))
+
+ fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContent) }
+
+ fractionRange(start = .58f) { fade(ShadeHeader.Elements.ExpandedContent) }
}
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
new file mode 100644
index 000000000000..272e507d5d4a
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.shade.ui.composable
+
+import android.view.ContextThemeWrapper
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+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.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.ValueKey
+import com.android.compose.animation.scene.animateSharedFloatAsState
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.phone.StatusIconContainer
+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
+
+object ShadeHeader {
+ object Elements {
+ val FormatPlaceholder = ElementKey("ShadeHeaderFormatPlaceholder")
+ val ExpandedContent = ElementKey("ShadeHeaderExpandedContent")
+ val CollapsedContent = ElementKey("ShadeHeaderCollapsedContent")
+ }
+
+ object Keys {
+ val transitionProgress = ValueKey("ShadeHeaderTransitionProgress")
+ }
+
+ object Dimensions {
+ val CollapsedHeight = 48.dp
+ val ExpandedHeight = 120.dp
+ }
+}
+
+@Composable
+fun SceneScope.CollapsedShadeHeader(
+ viewModel: ShadeHeaderViewModel,
+ createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
+ createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
+ statusBarIconController: StatusBarIconController,
+ modifier: Modifier = Modifier,
+) {
+ // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null.
+ Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder))
+ val formatProgress =
+ animateSharedFloatAsState(
+ 0.0f,
+ ShadeHeader.Keys.transitionProgress,
+ ShadeHeader.Elements.FormatPlaceholder
+ )
+ val useExpandedFormat by
+ remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } }
+
+ Row(
+ modifier =
+ modifier
+ .element(ShadeHeader.Elements.CollapsedContent)
+ .fillMaxWidth()
+ .defaultMinSize(minHeight = ShadeHeader.Dimensions.CollapsedHeight),
+ ) {
+ AndroidView(
+ factory = { context ->
+ Clock(ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), null)
+ },
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ Spacer(modifier = Modifier.width(5.dp))
+ VariableDayDate(
+ viewModel = viewModel,
+ modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ SystemIconContainer {
+ StatusIcons(
+ viewModel = viewModel,
+ createTintedIconManager = createTintedIconManager,
+ statusBarIconController = statusBarIconController,
+ useExpandedFormat = useExpandedFormat,
+ modifier = Modifier.align(Alignment.CenterVertically).padding(end = 6.dp),
+ )
+ BatteryIcon(
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ useExpandedFormat = useExpandedFormat,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
+}
+
+@Composable
+fun SceneScope.ExpandedShadeHeader(
+ viewModel: ShadeHeaderViewModel,
+ createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
+ createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
+ statusBarIconController: StatusBarIconController,
+ modifier: Modifier = Modifier,
+) {
+ // TODO(b/298153892): Remove this once animateSharedFloatAsState.element can be null.
+ Spacer(Modifier.element(ShadeHeader.Elements.FormatPlaceholder))
+ val formatProgress =
+ animateSharedFloatAsState(
+ 1.0f,
+ ShadeHeader.Keys.transitionProgress,
+ ShadeHeader.Elements.FormatPlaceholder
+ )
+ val useExpandedFormat by
+ remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } }
+
+ Column(
+ verticalArrangement = Arrangement.Bottom,
+ modifier =
+ modifier
+ .element(ShadeHeader.Elements.ExpandedContent)
+ .fillMaxWidth()
+ .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight)
+ ) {
+ Row {
+ AndroidView(
+ factory = { context ->
+ Clock(ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), null)
+ },
+ modifier =
+ Modifier.align(Alignment.CenterVertically)
+ // use graphicsLayer instead of Modifier.scale to anchor transform to
+ // top left corner
+ .graphicsLayer(
+ scaleX = 2.57f,
+ scaleY = 2.57f,
+ transformOrigin = TransformOrigin(0f, 0.5f)
+ ),
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ ShadeCarrierGroup(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ Spacer(modifier = Modifier.width(5.dp))
+ Row {
+ VariableDayDate(
+ viewModel = viewModel,
+ modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ SystemIconContainer {
+ StatusIcons(
+ viewModel = viewModel,
+ createTintedIconManager = createTintedIconManager,
+ statusBarIconController = statusBarIconController,
+ useExpandedFormat = useExpandedFormat,
+ modifier = Modifier.align(Alignment.CenterVertically).padding(end = 6.dp),
+ )
+ BatteryIcon(
+ useExpandedFormat = useExpandedFormat,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun BatteryIcon(
+ createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
+ useExpandedFormat: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ AndroidView(
+ factory = { context ->
+ val batteryIcon = BatteryMeterView(context, null)
+ batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ON)
+
+ val batteryMaterViewController =
+ createBatteryMeterViewController(batteryIcon, StatusBarLocation.QS)
+ batteryMaterViewController.init()
+ batteryMaterViewController.ignoreTunerUpdates()
+
+ batteryIcon
+ },
+ update = { batteryIcon ->
+ // TODO(b/298525212): use MODE_ESTIMATE in collapsed view when the screen
+ // has no center cutout. See [QsBatteryModeController.getBatteryMode]
+ batteryIcon.setPercentShowMode(
+ if (useExpandedFormat) {
+ BatteryMeterView.MODE_ESTIMATE
+ } else {
+ BatteryMeterView.MODE_ON
+ }
+ )
+ },
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun ShadeCarrierGroup(
+ viewModel: ShadeHeaderViewModel,
+ modifier: Modifier = Modifier,
+) {
+ Row(modifier = modifier) {
+ val subIds by viewModel.mobileSubIds.collectAsState()
+
+ for (subId in subIds) {
+ Spacer(modifier = Modifier.width(5.dp))
+ AndroidView(
+ factory = { context ->
+ ModernShadeCarrierGroupMobileView.constructAndBind(
+ context = context,
+ logger = viewModel.mobileIconsViewModel.logger,
+ slot = "mobile_carrier_shade_group",
+ viewModel =
+ (viewModel.mobileIconsViewModel.viewModelForSub(
+ subId,
+ StatusBarLocation.SHADE_CARRIER_GROUP
+ ) as ShadeCarrierGroupMobileIconViewModel),
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun StatusIcons(
+ viewModel: ShadeHeaderViewModel,
+ createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
+ statusBarIconController: StatusBarIconController,
+ useExpandedFormat: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ val carrierIconSlots =
+ listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
+ val isSingleCarrier by viewModel.isSingleCarrier.collectAsState()
+ val isTransitioning by viewModel.isTransitioning.collectAsState()
+
+ AndroidView(
+ factory = { context ->
+ val iconContainer = StatusIconContainer(context, null)
+ val iconManager = createTintedIconManager(iconContainer, StatusBarLocation.QS)
+ iconManager.setTint(
+ Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
+ )
+ statusBarIconController.addIconGroup(iconManager)
+
+ iconContainer
+ },
+ update = { iconContainer ->
+ iconContainer.setQsExpansionTransitioning(isTransitioning)
+ if (isSingleCarrier || !useExpandedFormat) {
+ iconContainer.removeIgnoredSlots(carrierIconSlots)
+ } else {
+ iconContainer.addIgnoredSlots(carrierIconSlots)
+ }
+ },
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun SystemIconContainer(
+ modifier: Modifier = Modifier,
+ content: @Composable RowScope.() -> Unit
+) {
+ // TODO(b/298524053): add hover state for this container
+ Row(
+ modifier = modifier.height(ShadeHeader.Dimensions.CollapsedHeight),
+ content = content,
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index f985aa2a2aa0..b1056376220f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -16,9 +16,9 @@
package com.android.systemui.shade.ui.composable
+import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.notifications.ui.composable.Notifications
@@ -43,6 +44,9 @@ import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
+import com.android.systemui.statusbar.phone.StatusBarLocation
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -77,6 +81,9 @@ class ShadeScene
constructor(
@Application private val applicationScope: CoroutineScope,
private val viewModel: ShadeSceneViewModel,
+ private val tintedIconManagerFactory: TintedIconManager.Factory,
+ private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
+ private val statusBarIconController: StatusBarIconController,
) : ComposableScene {
override val key = SceneKey.Shade
@@ -92,7 +99,14 @@ constructor(
@Composable
override fun SceneScope.Content(
modifier: Modifier,
- ) = ShadeScene(viewModel, modifier)
+ ) =
+ ShadeScene(
+ viewModel = viewModel,
+ createTintedIconManager = tintedIconManagerFactory::create,
+ createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
+ statusBarIconController = statusBarIconController,
+ modifier = modifier,
+ )
private fun destinationScenes(
up: SceneKey,
@@ -107,6 +121,9 @@ constructor(
@Composable
private fun SceneScope.ShadeScene(
viewModel: ShadeSceneViewModel,
+ createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
+ createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
+ statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
Box(modifier.element(Shade.Elements.Scrim)) {
@@ -116,16 +133,22 @@ private fun SceneScope.ShadeScene(
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
)
-
Column(
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(16.dp),
modifier =
Modifier.fillMaxSize()
.clickable(onClick = { viewModel.onContentClicked() })
- .padding(horizontal = 16.dp, vertical = 48.dp)
+ .padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
) {
+ CollapsedShadeHeader(
+ viewModel = viewModel.shadeHeaderViewModel,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
QuickSettings(modifier = Modifier.height(160.dp))
+ Spacer(modifier = Modifier.height(16.dp))
Notifications(modifier = Modifier.weight(1f))
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt
new file mode 100644
index 000000000000..799dbd66d8a4
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt
@@ -0,0 +1,64 @@
+package com.android.systemui.shade.ui.composable
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+
+@Composable
+fun VariableDayDate(
+ viewModel: ShadeHeaderViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val longerText = viewModel.longerDateText.collectAsState()
+ val shorterText = viewModel.shorterDateText.collectAsState()
+
+ Layout(
+ contents =
+ listOf(
+ {
+ Text(
+ text = longerText.value,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onBackground,
+ maxLines = 1,
+ )
+ },
+ {
+ Text(
+ text = shorterText.value,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onBackground,
+ maxLines = 1,
+ )
+ },
+ ),
+ modifier = modifier,
+ ) { measureables, constraints ->
+ check(measureables.size == 2)
+ check(measureables[0].size == 1)
+ check(measureables[1].size == 1)
+
+ val longerMeasurable = measureables[0][0]
+ val shorterMeasurable = measureables[1][0]
+
+ val longerPlaceable = longerMeasurable.measure(constraints)
+ val shorterPlaceable = shorterMeasurable.measure(constraints)
+
+ // If width < maxWidth (and not <=), we can assume that the text fits.
+ val placeable =
+ when {
+ longerPlaceable.width < constraints.maxWidth &&
+ longerPlaceable.height <= constraints.maxHeight -> longerPlaceable
+ shorterPlaceable.width < constraints.maxWidth &&
+ shorterPlaceable.height <= constraints.maxHeight -> shorterPlaceable
+ else -> null
+ }
+
+ layout(placeable?.width ?: 0, placeable?.height ?: 0) { placeable?.placeRelative(0, 0) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index 0ca38834960c..b6f47e9c907e 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -31,6 +31,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
@@ -240,4 +241,50 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView>
}
}
}
+
+ /** */
+ @SysUISingleton
+ public static class Factory {
+ private final UserTracker mUserTracker;
+ private final ConfigurationController mConfigurationController;
+ private final TunerService mTunerService;
+ private final @Main Handler mMainHandler;
+ private final ContentResolver mContentResolver;
+ private final FeatureFlags mFeatureFlags;
+ private final BatteryController mBatteryController;
+
+ @Inject
+ public Factory(
+ UserTracker userTracker,
+ ConfigurationController configurationController,
+ TunerService tunerService,
+ @Main Handler mainHandler,
+ ContentResolver contentResolver,
+ FeatureFlags featureFlags,
+ BatteryController batteryController
+ ) {
+ mUserTracker = userTracker;
+ mConfigurationController = configurationController;
+ mTunerService = tunerService;
+ mMainHandler = mainHandler;
+ mContentResolver = contentResolver;
+ mFeatureFlags = featureFlags;
+ mBatteryController = batteryController;
+ }
+
+ /** */
+ public BatteryMeterViewController create(View view, StatusBarLocation location) {
+ return new BatteryMeterViewController(
+ (BatteryMeterView) view,
+ location,
+ mUserTracker,
+ mConfigurationController,
+ mTunerService,
+ mMainHandler,
+ mContentResolver,
+ mFeatureFlags,
+ mBatteryController
+ );
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 4c6281e1cdb0..9edd2c6cf927 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -18,13 +18,17 @@ package com.android.systemui.qs.ui.viewmodel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import javax.inject.Inject
/** Models UI state and handles user input for the quick settings scene. */
@SysUISingleton
class QuickSettingsSceneViewModel
@Inject
-constructor(private val bouncerInteractor: BouncerInteractor) {
+constructor(
+ private val bouncerInteractor: BouncerInteractor,
+ val shadeHeaderViewModel: ShadeHeaderViewModel,
+) {
/** Notifies that some content in quick settings was clicked. */
fun onContentClicked() {
bouncerInteractor.showOrUnlockDevice()
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 45ee7be35ec3..7353379b2e79 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.stateIn
class SceneInteractor
@Inject
constructor(
- @Application applicationScope: CoroutineScope,
+ @Application private val applicationScope: CoroutineScope,
private val repository: SceneContainerRepository,
private val powerRepository: PowerRepository,
private val logger: SceneLogger,
@@ -146,6 +146,28 @@ constructor(
return repository.setVisible(isVisible)
}
+ /** True if there is a transition happening from and to the specified scenes. */
+ fun transitioning(from: SceneKey, to: SceneKey): StateFlow<Boolean> {
+ fun transitioning(
+ state: ObservableTransitionState,
+ from: SceneKey,
+ to: SceneKey,
+ ): Boolean {
+ return (state as? ObservableTransitionState.Transition)?.let {
+ it.fromScene == from && it.toScene == to
+ }
+ ?: false
+ }
+
+ return transitionState
+ .map { state -> transitioning(state, from, to) }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = transitioning(transitionState.value, from, to),
+ )
+ }
+
/**
* Binds the given flow so the system remembers it.
*
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
new file mode 100644
index 000000000000..c6c664dbdcf0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.icu.text.DateFormat
+import android.icu.text.DisplayContext
+import android.os.UserHandle
+import com.android.systemui.R
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Models UI state for the shade header. */
+@SysUISingleton
+class ShadeHeaderViewModel
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ context: Context,
+ sceneInteractor: SceneInteractor,
+ mobileIconsInteractor: MobileIconsInteractor,
+ val mobileIconsViewModel: MobileIconsViewModel,
+ broadcastDispatcher: BroadcastDispatcher,
+) {
+ /** True if we are transitioning between Shade and QuickSettings scenes, in either direction. */
+ val isTransitioning =
+ combine(
+ sceneInteractor.transitioning(from = SceneKey.Shade, to = SceneKey.QuickSettings),
+ sceneInteractor.transitioning(from = SceneKey.QuickSettings, to = SceneKey.Shade)
+ ) { shadeToQuickSettings, quickSettingsToShade ->
+ shadeToQuickSettings || quickSettingsToShade
+ }
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+ /** True if there is exactly one mobile connection. */
+ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
+
+ /** The list of subscription Ids for current mobile connections. */
+ val mobileSubIds =
+ mobileIconsInteractor.filteredSubscriptions
+ .map { list -> list.map { it.subscriptionId } }
+ .stateIn(applicationScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm)
+ private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year)
+ private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern))
+ private val shorterDateFormat = MutableStateFlow(getFormatFromPattern(shorterPattern))
+
+ private val _shorterDateText: MutableStateFlow<String> = MutableStateFlow("")
+ val shorterDateText: StateFlow<String> = _shorterDateText.asStateFlow()
+
+ private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("")
+ val longerDateText: StateFlow<String> = _longerDateText.asStateFlow()
+
+ init {
+ broadcastDispatcher
+ .broadcastFlow(
+ filter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_TIME_TICK)
+ addAction(Intent.ACTION_TIME_CHANGED)
+ addAction(Intent.ACTION_TIMEZONE_CHANGED)
+ addAction(Intent.ACTION_LOCALE_CHANGED)
+ },
+ user = UserHandle.SYSTEM,
+ map = { intent, _ ->
+ intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
+ intent.action == Intent.ACTION_LOCALE_CHANGED
+ }
+ )
+ .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
+ .launchIn(applicationScope)
+
+ applicationScope.launch { updateDateTexts(false) }
+ }
+
+ private fun updateDateTexts(invalidateFormats: Boolean) {
+ if (invalidateFormats) {
+ longerDateFormat.value = getFormatFromPattern(longerPattern)
+ shorterDateFormat.value = getFormatFromPattern(shorterPattern)
+ }
+
+ val currentTime = Date()
+
+ _longerDateText.value = longerDateFormat.value.format(currentTime)
+ _shorterDateText.value = shorterDateFormat.value.format(currentTime)
+ }
+
+ private fun getFormatFromPattern(pattern: String?): DateFormat {
+ val l = Locale.getDefault()
+ val format = DateFormat.getInstanceForSkeleton(pattern, l)
+ // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of
+ // CAPITALIZATION_FOR_STANDALONE is to address
+ // https://unicode-org.atlassian.net/browse/ICU-21631
+ // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE
+ format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE)
+ return format
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 8edc26d01d71..068d5a59ca2e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -36,6 +36,7 @@ constructor(
@Application private val applicationScope: CoroutineScope,
authenticationInteractor: AuthenticationInteractor,
private val bouncerInteractor: BouncerInteractor,
+ val shadeHeaderViewModel: ShadeHeaderViewModel,
) {
/** The key of the scene we should switch to when swiping up. */
val upDestinationSceneKey: StateFlow<SceneKey> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index a4ec3a36694d..0f55910d8779 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -20,7 +20,6 @@ import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
@@ -56,7 +55,6 @@ constructor(
private val airplaneModeInteractor: AirplaneModeInteractor,
private val constants: ConnectivityConstants,
@Application private val scope: CoroutineScope,
- private val statusBarPipelineFlags: StatusBarPipelineFlags,
) {
@VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>()
@VisibleForTesting
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 2cb02058ab03..8ae89304974d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -23,10 +23,19 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -44,15 +53,49 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
repository = utils.authenticationRepository(),
)
- private val underTest =
- QuickSettingsSceneViewModel(
- bouncerInteractor =
- utils.bouncerInteractor(
- authenticationInteractor = authenticationInteractor,
- sceneInteractor = sceneInteractor,
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+ private var mobileIconsViewModel: MobileIconsViewModel =
+ MobileIconsViewModel(
+ logger = mock(),
+ verboseLogger = mock(),
+ interactor = mobileIconsInteractor,
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
),
+ constants = mock(),
+ scope = testScope.backgroundScope,
)
+ private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
+
+ private lateinit var underTest: QuickSettingsSceneViewModel
+
+ @Before
+ fun setUp() {
+ shadeHeaderViewModel =
+ ShadeHeaderViewModel(
+ applicationScope = testScope.backgroundScope,
+ context = context,
+ sceneInteractor = sceneInteractor,
+ mobileIconsInteractor = mobileIconsInteractor,
+ mobileIconsViewModel = mobileIconsViewModel,
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ )
+
+ underTest =
+ QuickSettingsSceneViewModel(
+ bouncerInteractor =
+ utils.bouncerInteractor(
+ authenticationInteractor = authenticationInteractor,
+ sceneInteractor = sceneInteractor,
+ ),
+ shadeHeaderViewModel = shadeHeaderViewModel,
+ )
+ }
+
@Test
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 2f26a53afe7c..141fcbb15c0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -36,7 +36,14 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -123,13 +130,25 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
),
)
- private val shadeSceneViewModel =
- ShadeSceneViewModel(
- applicationScope = testScope.backgroundScope,
- authenticationInteractor = authenticationInteractor,
- bouncerInteractor = bouncerInteractor,
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+ private var mobileIconsViewModel: MobileIconsViewModel =
+ MobileIconsViewModel(
+ logger = mock(),
+ verboseLogger = mock(),
+ interactor = mobileIconsInteractor,
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
+ ),
+ constants = mock(),
+ scope = testScope.backgroundScope,
)
+ private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
+ private lateinit var shadeSceneViewModel: ShadeSceneViewModel
+
private val keyguardRepository = utils.keyguardRepository
private val keyguardInteractor =
utils.keyguardInteractor(
@@ -138,6 +157,24 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
@Before
fun setUp() {
+ shadeHeaderViewModel =
+ ShadeHeaderViewModel(
+ applicationScope = testScope.backgroundScope,
+ context = context,
+ sceneInteractor = sceneInteractor,
+ mobileIconsInteractor = mobileIconsInteractor,
+ mobileIconsViewModel = mobileIconsViewModel,
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ )
+
+ shadeSceneViewModel =
+ ShadeSceneViewModel(
+ applicationScope = testScope.backgroundScope,
+ authenticationInteractor = authenticationInteractor,
+ bouncerInteractor = bouncerInteractor,
+ shadeHeaderViewModel = shadeHeaderViewModel,
+ )
+
authenticationRepository.setUnlocked(false)
val displayTracker = FakeDisplayTracker(context)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 8620f6184107..ed716a97410f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -28,6 +28,7 @@ import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -136,6 +137,97 @@ class SceneInteractorTest : SysuiTestCase() {
}
@Test
+ fun transitioning_idle_false() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(SceneKey.Shade)
+ )
+ val transitioning by
+ collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
+ underTest.setTransitionState(transitionState)
+
+ assertThat(transitioning).isFalse()
+ }
+
+ @Test
+ fun transitioning_wrongFromScene_false() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Gone,
+ toScene = SceneKey.Lockscreen,
+ progress = flowOf(0.5f)
+ )
+ )
+ val transitioning by
+ collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
+ underTest.setTransitionState(transitionState)
+
+ assertThat(transitioning).isFalse()
+ }
+
+ @Test
+ fun transitioning_wrongToScene_false() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Shade,
+ toScene = SceneKey.QuickSettings,
+ progress = flowOf(0.5f)
+ )
+ )
+ underTest.setTransitionState(transitionState)
+
+ assertThat(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen).value).isFalse()
+ }
+
+ @Test
+ fun transitioning_correctFromAndToScenes_true() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Shade,
+ toScene = SceneKey.Lockscreen,
+ progress = flowOf(0.5f)
+ )
+ )
+ val transitioning by
+ collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
+ underTest.setTransitionState(transitionState)
+
+ assertThat(transitioning).isTrue()
+ }
+
+ @Test
+ fun transitioning_updates() =
+ testScope.runTest {
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(SceneKey.Shade)
+ )
+ val transitioning by
+ collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
+ underTest.setTransitionState(transitionState)
+
+ assertThat(transitioning).isFalse()
+
+ transitionState.value =
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Shade,
+ toScene = SceneKey.Lockscreen,
+ progress = flowOf(0.5f)
+ )
+ assertThat(transitioning).isTrue()
+
+ transitionState.value = ObservableTransitionState.Idle(SceneKey.Lockscreen)
+ assertThat(transitioning).isFalse()
+ }
+
+ @Test
fun isVisible() =
testScope.runTest {
val isVisible by collectLastValue(underTest.isVisible)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
new file mode 100644
index 000000000000..a09e844c739f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -0,0 +1,155 @@
+package com.android.systemui.shade.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class ShadeHeaderViewModelTest : SysuiTestCase() {
+ private val utils = SceneTestUtils(this)
+ private val testScope = utils.testScope
+ private val sceneInteractor = utils.sceneInteractor()
+
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+ private var mobileIconsViewModel: MobileIconsViewModel =
+ MobileIconsViewModel(
+ logger = mock(),
+ verboseLogger = mock(),
+ interactor = mobileIconsInteractor,
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
+ ),
+ constants = mock(),
+ scope = testScope.backgroundScope,
+ )
+
+ private lateinit var underTest: ShadeHeaderViewModel
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ ShadeHeaderViewModel(
+ applicationScope = testScope.backgroundScope,
+ context = context,
+ sceneInteractor = sceneInteractor,
+ mobileIconsInteractor = mobileIconsInteractor,
+ mobileIconsViewModel = mobileIconsViewModel,
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ )
+ }
+
+ @Test
+ fun isTransitioning_idle_false() =
+ testScope.runTest {
+ val isTransitioning by collectLastValue(underTest.isTransitioning)
+ sceneInteractor.setTransitionState(
+ MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Shade))
+ )
+
+ assertThat(isTransitioning).isFalse()
+ }
+
+ @Test
+ fun isTransitioning_shadeToQs_true() =
+ testScope.runTest {
+ val isTransitioning by collectLastValue(underTest.isTransitioning)
+ sceneInteractor.setTransitionState(
+ MutableStateFlow(
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Shade,
+ toScene = SceneKey.QuickSettings,
+ progress = MutableStateFlow(0.5f)
+ )
+ )
+ )
+
+ assertThat(isTransitioning).isTrue()
+ }
+
+ @Test
+ fun isTransitioning_qsToShade_true() =
+ testScope.runTest {
+ val isTransitioning by collectLastValue(underTest.isTransitioning)
+ sceneInteractor.setTransitionState(
+ MutableStateFlow(
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.QuickSettings,
+ toScene = SceneKey.Shade,
+ progress = MutableStateFlow(0.5f)
+ )
+ )
+ )
+
+ assertThat(isTransitioning).isTrue()
+ }
+
+ @Test
+ fun isTransitioning_otherTransition_false() =
+ testScope.runTest {
+ val isTransitioning by collectLastValue(underTest.isTransitioning)
+ sceneInteractor.setTransitionState(
+ MutableStateFlow(
+ ObservableTransitionState.Transition(
+ fromScene = SceneKey.Gone,
+ toScene = SceneKey.Shade,
+ progress = MutableStateFlow(0.5f)
+ )
+ )
+ )
+
+ assertThat(isTransitioning).isFalse()
+ }
+
+ @Test
+ fun mobileSubIds_update() =
+ testScope.runTest {
+ val mobileSubIds by collectLastValue(underTest.mobileSubIds)
+ mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
+
+ assertThat(mobileSubIds).isEqualTo(listOf(1))
+
+ mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+
+ assertThat(mobileSubIds).isEqualTo(listOf(1, 2))
+ }
+
+ companion object {
+ private val SUB_1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ )
+ private val SUB_2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 69b952542c29..5c75d9cff10a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -23,10 +23,18 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -45,17 +53,51 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
sceneInteractor = sceneInteractor,
)
- private val underTest =
- ShadeSceneViewModel(
- applicationScope = testScope.backgroundScope,
- authenticationInteractor = authenticationInteractor,
- bouncerInteractor =
- utils.bouncerInteractor(
- authenticationInteractor = authenticationInteractor,
- sceneInteractor = sceneInteractor,
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+ private var mobileIconsViewModel: MobileIconsViewModel =
+ MobileIconsViewModel(
+ logger = mock(),
+ verboseLogger = mock(),
+ interactor = mobileIconsInteractor,
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
),
+ constants = mock(),
+ scope = testScope.backgroundScope,
)
+ private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
+
+ private lateinit var underTest: ShadeSceneViewModel
+
+ @Before
+ fun setUp() {
+ shadeHeaderViewModel =
+ ShadeHeaderViewModel(
+ applicationScope = testScope.backgroundScope,
+ context = context,
+ sceneInteractor = sceneInteractor,
+ mobileIconsInteractor = mobileIconsInteractor,
+ mobileIconsViewModel = mobileIconsViewModel,
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ )
+
+ underTest =
+ ShadeSceneViewModel(
+ applicationScope = testScope.backgroundScope,
+ authenticationInteractor = authenticationInteractor,
+ bouncerInteractor =
+ utils.bouncerInteractor(
+ authenticationInteractor = authenticationInteractor,
+ sceneInteractor = sceneInteractor,
+ ),
+ shadeHeaderViewModel = shadeHeaderViewModel,
+ )
+ }
+
@Test
fun upTransitionSceneKey_deviceLocked_lockScreen() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
index e42515e5871d..eb6f2f81fde4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -78,7 +78,6 @@ class MobileIconsViewModelTest : SysuiTestCase() {
airplaneModeInteractor,
constants,
testScope.backgroundScope,
- statusBarPipelineFlags,
)
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)