Merge "[flexiglass] Rewrite Shade Header in Compose and migrate it to flexiglass" into main
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 e5cd439..7ac3901 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.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 @@
@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 @@
) {
QuickSettingsScene(
viewModel = viewModel,
+ createTintedIconManager = tintedIconManagerFactory::create,
+ createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
+ statusBarIconController = statusBarIconController,
modifier = modifier,
)
}
@@ -69,16 +83,27 @@
@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 21a10b1..be85bee 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.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 0000000..272e507
--- /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 f985aa2..b105637 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.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.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 @@
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 @@
@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 @@
@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 @@
.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 0000000..799dbd6
--- /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 0ca3883..b6f47e9 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 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 @@
}
}
}
+
+ /** */
+ @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 4c6281e..9edd2c6 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 @@
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 45ee7be..7353379 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 @@
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 @@
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 0000000..c6c664d
--- /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 8edc26d..068d5a5 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 @@
@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 a4ec3a3..0f55910 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 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 @@
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 2cb0205..8ae8930 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.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 @@
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 2f26a53..141fcbb 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.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 @@
),
)
- 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 @@
@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 8620f61..ed716a9 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.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 @@
}
@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 0000000..a09e844
--- /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 69b9525..5c75d9c 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.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 @@
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 e42515e..eb6f2f8 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 @@
airplaneModeInteractor,
constants,
testScope.backgroundScope,
- statusBarPipelineFlags,
)
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)