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)