diff options
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) |