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