diff options
19 files changed, 716 insertions, 127 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index a02781ba63f7..96520b21cc72 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** The lock screen scene shows when the device is locked. */ @@ -54,8 +53,17 @@ constructor( override val key = Scenes.Lockscreen override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = - combine(viewModel.upDestinationSceneKey, viewModel.leftDestinationSceneKey, ::Pair) - .map { (upKey, leftKey) -> destinationScenes(up = upKey, left = leftKey) } + combine( + viewModel.upDestinationSceneKey, + viewModel.leftDestinationSceneKey, + viewModel.downFromTopEdgeDestinationSceneKey, + ) { upKey, leftKey, downFromTopEdgeKey -> + destinationScenes( + up = upKey, + left = leftKey, + downFromTopEdge = downFromTopEdgeKey, + ) + } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -63,6 +71,7 @@ constructor( destinationScenes( up = viewModel.upDestinationSceneKey.value, left = viewModel.leftDestinationSceneKey.value, + downFromTopEdge = viewModel.downFromTopEdgeDestinationSceneKey.value, ) ) @@ -79,12 +88,15 @@ constructor( private fun destinationScenes( up: SceneKey?, left: SceneKey?, + downFromTopEdge: SceneKey?, ): Map<UserAction, UserActionResult> { return buildMap { up?.let { this[Swipe(SwipeDirection.Up)] = UserActionResult(up) } left?.let { this[Swipe(SwipeDirection.Left)] = UserActionResult(left) } - this[Swipe(fromSource = Edge.Top, direction = SwipeDirection.Down)] = - UserActionResult(Scenes.QuickSettings) + downFromTopEdge?.let { + this[Swipe(fromSource = Edge.Top, direction = SwipeDirection.Down)] = + UserActionResult(downFromTopEdge) + } this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index eb71490f049a..5d9b014c5c96 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.footer.ui.compose +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas import androidx.compose.foundation.LocalIndication @@ -76,9 +77,31 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.res.R import kotlinx.coroutines.launch +@Composable +fun FooterActionsWithAnimatedVisibility( + viewModel: FooterActionsViewModel, + isCustomizing: Boolean, + lifecycleOwner: LifecycleOwner, + footerActionsModifier: (Modifier) -> Modifier, + modifier: Modifier = Modifier, +) { + AnimatedVisibility(visible = !isCustomizing, modifier = modifier.fillMaxWidth()) { + QuickSettingsTheme { + // This view has its own horizontal padding + // TODO(b/321716470) This should use a lifecycle tied to the scene. + FooterActions( + viewModel = viewModel, + qsVisibilityLifecycleOwner = lifecycleOwner, + modifier = footerActionsModifier(Modifier), + ) + } + } +} + /** The Quick Settings footer actions row. */ @Composable fun FooterActions( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index 91b737d33418..bc48dd1d431f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -63,12 +63,14 @@ object QuickSettings { } private fun SceneScope.stateForQuickSettingsContent( + isSplitShade: Boolean, squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default ): QSSceneAdapter.State { return when (val transitionState = layoutState.transitionState) { is TransitionState.Idle -> { when (transitionState.currentScene) { - Scenes.Shade -> QSSceneAdapter.State.QQS + Scenes.Shade -> QSSceneAdapter.State.QQS.takeUnless { isSplitShade } + ?: QSSceneAdapter.State.QS Scenes.QuickSettings -> QSSceneAdapter.State.QS else -> QSSceneAdapter.State.CLOSED } @@ -76,6 +78,7 @@ private fun SceneScope.stateForQuickSettingsContent( is TransitionState.Transition -> with(transitionState) { when { + isSplitShade -> QSSceneAdapter.State.QS fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> Expanding(progress) fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> @@ -111,10 +114,11 @@ private fun SceneScope.stateForQuickSettingsContent( fun SceneScope.QuickSettings( qsSceneAdapter: QSSceneAdapter, heightProvider: () -> Int, + isSplitShade: Boolean, modifier: Modifier = Modifier, squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default, ) { - val contentState = stateForQuickSettingsContent(squishiness) + val contentState = stateForQuickSettingsContent(isSplitShade, squishiness) MovableElement( key = QuickSettings.Elements.Content, 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 3b8b863fdde2..6ae1410623a3 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 @@ -61,6 +61,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qs.footer.ui.compose.FooterActions +import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -238,24 +239,21 @@ private fun SceneScope.QuickSettingsScene( QuickSettings( viewModel.qsSceneAdapter, { viewModel.qsSceneAdapter.qsHeight }, + isSplitShade = false, modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"), ) } } - AnimatedVisibility( - visible = !isCustomizing, - modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth() - ) { - QuickSettingsTheme { - // This view has its own horizontal padding - // TODO(b/321716470) This should use a lifecycle tied to the scene. - FooterActions( - viewModel = footerActionsViewModel, - qsVisibilityLifecycleOwner = lifecycleOwner, - modifier = Modifier.element(QuickSettings.Elements.FooterActions) - ) - } - } + + FooterActionsWithAnimatedVisibility( + viewModel = footerActionsViewModel, + isCustomizing = isCustomizing, + lifecycleOwner = lifecycleOwner, + footerActionsModifier = { modifier -> + modifier.element(QuickSettings.Elements.FooterActions) + }, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 82f56abbded6..975829ab3760 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -20,21 +20,16 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import com.android.systemui.scene.ui.viewmodel.GoneSceneViewModel import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow /** * "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any @@ -44,22 +39,12 @@ import kotlinx.coroutines.flow.asStateFlow class GoneScene @Inject constructor( - private val notificationsViewModel: NotificationsPlaceholderViewModel, + private val viewModel: GoneSceneViewModel, ) : ComposableScene { override val key = Scenes.Gone override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = - MutableStateFlow<Map<UserAction, UserActionResult>>( - mapOf( - Swipe( - pointerCount = 2, - fromSource = Edge.Top, - direction = SwipeDirection.Down, - ) to UserActionResult(Scenes.QuickSettings), - Swipe(direction = SwipeDirection.Down) to UserActionResult(Scenes.Shade), - ) - ) - .asStateFlow() + viewModel.destinationScenes @Composable override fun SceneScope.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 3620cc570c66..0b9f503eec95 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 @@ -19,14 +19,26 @@ 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.clipScrollableContainer +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,22 +48,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexScenePicker -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.modifiers.thenIf import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager @@ -59,6 +68,7 @@ import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.notifications.ui.composable.NotificationScrollingStack +import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -71,11 +81,7 @@ import com.android.systemui.util.animation.MeasurementInput import javax.inject.Inject import javax.inject.Named import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn object Shade { object Elements { @@ -103,7 +109,6 @@ object Shade { class ShadeScene @Inject constructor( - @Application private val applicationScope: CoroutineScope, private val viewModel: ShadeSceneViewModel, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, @@ -114,13 +119,7 @@ constructor( override val key = Scenes.Shade override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = - viewModel.upDestinationSceneKey - .map { sceneKey -> destinationScenes(up = sceneKey) } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value), - ) + viewModel.destinationScenes @Composable override fun SceneScope.Content( @@ -141,19 +140,44 @@ constructor( mediaHost.showsOnlyActiveMedia = true mediaHost.init(MediaHierarchyManager.LOCATION_QQS) } +} - private fun destinationScenes( - up: SceneKey, - ): Map<UserAction, UserActionResult> { - return mapOf( - Swipe(SwipeDirection.Up) to UserActionResult(up), - Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings), +@Composable +private fun SceneScope.ShadeScene( + viewModel: ShadeSceneViewModel, + createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, + createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, + statusBarIconController: StatusBarIconController, + mediaCarouselController: MediaCarouselController, + mediaHost: MediaHost, + modifier: Modifier = Modifier, +) { + val isSplitShade by viewModel.isSplitShade.collectAsState() + if (isSplitShade) { + SplitShade( + viewModel = viewModel, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, + modifier = modifier, + ) + } else { + SingleShade( + viewModel = viewModel, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, + modifier = modifier, ) } } @Composable -private fun SceneScope.ShadeScene( +private fun SceneScope.SingleShade( viewModel: ShadeSceneViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -162,8 +186,6 @@ private fun SceneScope.ShadeScene( mediaHost: MediaHost, modifier: Modifier = Modifier, ) { - val density = LocalDensity.current - val layoutWidth = remember { mutableStateOf(0) } val maxNotifScrimTop = remember { mutableStateOf(0f) } val tileSquishiness by animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness) @@ -203,38 +225,15 @@ private fun SceneScope.ShadeScene( (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness) .roundToInt() }, + isSplitShade = false, squishiness = tileSquishiness, ) - if (viewModel.isMediaVisible()) { - val mediaHeight = - dimensionResource(R.dimen.qs_media_session_height_expanded) - MediaCarousel( - modifier = - Modifier.height(mediaHeight).fillMaxWidth().layout { - measurable, - constraints -> - val placeable = measurable.measure(constraints) - - // Notify controller to size the carousel for the - // current space - mediaHost.measurementInput = - MeasurementInput(placeable.width, placeable.height) - mediaCarouselController.setSceneContainerSize( - placeable.width, - placeable.height - ) - - layout(placeable.width, placeable.height) { - placeable.placeRelative(0, 0) - } - }, - mediaHost = mediaHost, - layoutWidth = layoutWidth.value, - layoutHeight = with(density) { mediaHeight.toPx() }.toInt(), - carouselController = mediaCarouselController, - ) - } + MediaIfVisible( + viewModel = viewModel, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, + ) Spacer(modifier = Modifier.height(16.dp)) } @@ -263,3 +262,133 @@ private fun SceneScope.ShadeScene( } } } + +@Composable +private fun SceneScope.SplitShade( + viewModel: ShadeSceneViewModel, + createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, + createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, + statusBarIconController: StatusBarIconController, + mediaCarouselController: MediaCarouselController, + mediaHost: MediaHost, + modifier: Modifier = Modifier, +) { + val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() + val lifecycleOwner = LocalLifecycleOwner.current + val footerActionsViewModel = + remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } + + val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + val density = LocalDensity.current + LaunchedEffect(navBarBottomHeight, density) { + with(density) { + viewModel.qsSceneAdapter.applyBottomNavBarPadding(navBarBottomHeight.roundToPx()) + } + } + + val quickSettingsScrollState = rememberScrollState() + LaunchedEffect(isCustomizing, quickSettingsScrollState) { + if (isCustomizing) { + quickSettingsScrollState.scrollTo(0) + } + } + + Box( + modifier = + modifier + .fillMaxSize() + .element(Shade.Elements.BackgroundScrim) + .background(colorResource(R.color.shade_scrim_background_dark)) + ) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + CollapsedShadeHeader( + viewModel = viewModel.shadeHeaderViewModel, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) + ) + + Row(modifier = Modifier.fillMaxWidth().weight(1f)) { + Column( + verticalArrangement = Arrangement.Top, + modifier = + Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) { + Modifier.verticalNestedScrollToScene() + .verticalScroll(quickSettingsScrollState) + .clipScrollableContainer(Orientation.Horizontal) + .padding(bottom = navBarBottomHeight) + } + ) { + QuickSettings( + qsSceneAdapter = viewModel.qsSceneAdapter, + heightProvider = { viewModel.qsSceneAdapter.qsHeight }, + isSplitShade = true, + modifier = Modifier.fillMaxWidth(), + ) + + MediaIfVisible( + viewModel = viewModel, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer( + modifier = Modifier.weight(1f), + ) + + FooterActionsWithAnimatedVisibility( + viewModel = footerActionsViewModel, + isCustomizing = isCustomizing, + lifecycleOwner = lifecycleOwner, + footerActionsModifier = { modifier -> + modifier.element(QuickSettings.Elements.FooterActions) + }, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + + NotificationScrollingStack( + viewModel = viewModel.notifications, + maxScrimTop = { 0f }, + modifier = Modifier.weight(1f).fillMaxHeight(), + ) + } + } + } +} + +@Composable +private fun SceneScope.MediaIfVisible( + viewModel: ShadeSceneViewModel, + mediaCarouselController: MediaCarouselController, + mediaHost: MediaHost, + modifier: Modifier = Modifier, +) { + if (viewModel.isMediaVisible()) { + val density = LocalDensity.current + val layoutWidth = remember { mutableStateOf(0) } + val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) + + MediaCarousel( + modifier = + modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + + // Notify controller to size the carousel for the + // current space + mediaHost.measurementInput = MeasurementInput(placeable.width, placeable.height) + mediaCarouselController.setSceneContainerSize(placeable.width, placeable.height) + + layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } + }, + mediaHost = mediaHost, + layoutWidth = layoutWidth.value, + layoutHeight = with(density) { mediaHeight.toPx() }.toInt(), + carouselController = mediaCarouselController, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 9ff76be30f79..19950a5fb89d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -31,8 +31,11 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.startable.shadeStartable import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock @@ -92,6 +95,24 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { assertThat(leftDestinationSceneKey).isEqualTo(Scenes.Communal) } + @Test + fun downFromTopEdgeDestinationSceneKey_whenNotSplitShade_quickSettings() = + testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, false) + kosmos.shadeStartable.start() + val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey) + assertThat(sceneKey).isEqualTo(Scenes.QuickSettings) + } + + @Test + fun downFromTopEdgeDestinationSceneKey_whenSplitShade_null() = + testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + kosmos.shadeStartable.start() + val sceneKey by collectLastValue(underTest.downFromTopEdgeDestinationSceneKey) + assertThat(sceneKey).isNull() + } + private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel { return LockscreenSceneViewModel( applicationScope = testScope.backgroundScope, @@ -102,6 +123,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { interactor = mock(), ), notifications = kosmos.notificationsPlaceholderViewModel, + shadeInteractor = kosmos.shadeInteractor, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index 63f00c1356e8..61089049bf89 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor @@ -108,14 +109,15 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, - footerActionsViewModelFactory, - footerActionsController, + footerActionsViewModelFactory = footerActionsViewModelFactory, + footerActionsController = footerActionsController, ) } @Test fun destinationsNotCustomizing() = testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, false) val destinations by collectLastValue(underTest.destinationScenes) qsFlexiglassAdapter.setCustomizing(false) @@ -131,6 +133,38 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { @Test fun destinationsCustomizing() = testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, false) + val destinations by collectLastValue(underTest.destinationScenes) + qsFlexiglassAdapter.setCustomizing(true) + + assertThat(destinations) + .isEqualTo( + mapOf( + Back to UserActionResult(Scenes.QuickSettings), + ) + ) + } + + @Test + fun destinations_whenNotCustomizing_inSplitShade() = + testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + val destinations by collectLastValue(underTest.destinationScenes) + qsFlexiglassAdapter.setCustomizing(false) + + assertThat(destinations) + .isEqualTo( + mapOf( + Back to UserActionResult(Scenes.Shade), + Swipe(SwipeDirection.Up) to UserActionResult(Scenes.Shade), + ) + ) + } + + @Test + fun destinations_whenCustomizing_inSplitShade() = + testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) val destinations by collectLastValue(underTest.destinationScenes) qsFlexiglassAdapter.setCustomizing(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index a2c4f4e63c19..42c33544416d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -20,10 +20,13 @@ package com.android.systemui.scene import android.telecom.TelecomManager import android.telephony.TelephonyManager +import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection import com.android.internal.R import com.android.internal.util.EmergencyAffordanceManager import com.android.internal.util.emergencyAffordanceManager @@ -59,6 +62,8 @@ import com.android.systemui.model.sceneContainerPlugin import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.qs.footerActionsController +import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.SceneContainerStartable @@ -69,6 +74,7 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -127,6 +133,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@RunWithLooper class SceneFrameworkIntegrationTest : SysuiTestCase() { private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } @@ -167,6 +174,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { interactor = mock(), ), notifications = kosmos.notificationsPlaceholderViewModel, + shadeInteractor = kosmos.shadeInteractor, ) } @@ -250,6 +258,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, + shadeInteractor = kosmos.shadeInteractor, + footerActionsController = kosmos.footerActionsController, + footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory, ) kosmos.fakeDeviceEntryRepository.setUnlocked(false) @@ -337,7 +348,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(shadeSceneViewModel.destinationScenes) setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) assertCurrentScene(Scenes.Lockscreen) @@ -345,6 +356,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { emulateUserDrivenTransition(to = Scenes.Shade) assertCurrentScene(Scenes.Shade) + val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -354,7 +366,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val upDestinationSceneKey by collectLastValue(shadeSceneViewModel.upDestinationSceneKey) + val destinationScenes by collectLastValue(shadeSceneViewModel.destinationScenes) setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) assertThat(deviceEntryInteractor.canSwipeToEnter.value).isTrue() assertCurrentScene(Scenes.Lockscreen) @@ -367,6 +379,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { emulateUserDrivenTransition(to = Scenes.Shade) assertCurrentScene(Scenes.Shade) + val upDestinationSceneKey = destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) emulateUserDrivenTransition( to = upDestinationSceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 853b00d345bc..2e68d12cebee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -16,8 +16,11 @@ package com.android.systemui.shade.ui.viewmodel +import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel @@ -28,11 +31,16 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.qs.footerActionsController +import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter +import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.startable.shadeStartable import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor @@ -57,6 +65,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper class ShadeSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -113,50 +122,56 @@ class ShadeSceneViewModelTest : SysuiTestCase() { qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, + shadeInteractor = kosmos.shadeInteractor, + footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory, + footerActionsController = kosmos.footerActionsController, ) } @Test fun upTransitionSceneKey_deviceLocked_lockScreen() = testScope.runTest { - val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + val destinationScenes by collectLastValue(underTest.destinationScenes) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) kosmos.fakeDeviceEntryRepository.setUnlocked(false) - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + .isEqualTo(Scenes.Lockscreen) } @Test fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { - val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + val destinationScenes by collectLastValue(underTest.destinationScenes) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) kosmos.fakeDeviceEntryRepository.setUnlocked(true) - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Gone) + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + .isEqualTo(Scenes.Gone) } @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + val destinationScenes by collectLastValue(underTest.destinationScenes) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + .isEqualTo(Scenes.Lockscreen) } @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + val destinationScenes by collectLastValue(underTest.destinationScenes) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeDeviceEntryRepository.setUnlocked(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -165,7 +180,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { runCurrent() sceneInteractor.changeScene(Scenes.Gone, "reason") - assertThat(upTransitionSceneKey).isEqualTo(Scenes.Gone) + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + .isEqualTo(Scenes.Gone) } @Test @@ -239,4 +255,23 @@ class ShadeSceneViewModelTest : SysuiTestCase() { assertThat(underTest.isMediaVisible()).isFalse() } + + @Test + fun downTransitionSceneKey_inSplitShade_null() = + testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + kosmos.shadeStartable.start() + val destinationScenes by collectLastValue(underTest.destinationScenes) + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene).isNull() + } + + @Test + fun downTransitionSceneKey_notSplitShade_quickSettings() = + testScope.runTest { + overrideResource(R.bool.config_use_split_notification_shade, false) + kosmos.shadeStartable.start() + val destinationScenes by collectLastValue(underTest.destinationScenes) + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene) + .isEqualTo(Scenes.QuickSettings) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index b60e99973348..d89523d2de62 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -38,6 +39,7 @@ constructor( @Application applicationScope: CoroutineScope, deviceEntryInteractor: DeviceEntryInteractor, communalInteractor: CommunalInteractor, + shadeInteractor: ShadeInteractor, val longPress: KeyguardLongPressViewModel, val notifications: NotificationsPlaceholderViewModel, ) { @@ -64,4 +66,14 @@ constructor( started = SharingStarted.WhileSubscribed(), initialValue = null, ) + + /** The key of the scene we should switch to when swiping down from the top edge. */ + val downFromTopEdgeDestinationSceneKey: StateFlow<SceneKey?> = + shadeInteractor.isSplitShade + .map { isSplitShade -> Scenes.QuickSettings.takeUnless { isSplitShade } } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt new file mode 100644 index 000000000000..a490fe2db5bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.scene.ui.viewmodel + +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class GoneSceneViewModel +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + shadeInteractor: ShadeInteractor, +) { + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + shadeInteractor.isSplitShade + .map { isSplitShade -> destinationScenes(isSplitShade = isSplitShade) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = destinationScenes(isSplitShade = shadeInteractor.isSplitShade.value) + ) + + private fun destinationScenes(isSplitShade: Boolean): Map<UserAction, UserActionResult> { + return buildMap { + if (!isSplitShade) { + this[ + Swipe( + pointerCount = 2, + fromSource = Edge.Top, + direction = SwipeDirection.Down, + )] = UserActionResult(Scenes.QuickSettings) + } + + this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade) + } + } +} 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 c9aa51c31060..8084a6f390a7 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 @@ -14,18 +14,31 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.shade.ui.viewmodel +import androidx.lifecycle.LifecycleOwner import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -43,28 +56,36 @@ constructor( val shadeHeaderViewModel: ShadeHeaderViewModel, val notifications: NotificationsPlaceholderViewModel, val mediaDataManager: MediaDataManager, + shadeInteractor: ShadeInteractor, + private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, + private val footerActionsController: FooterActionsController, ) { - /** The key of the scene we should switch to when swiping up. */ - val upDestinationSceneKey: StateFlow<SceneKey> = + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = combine( deviceEntryInteractor.isUnlocked, deviceEntryInteractor.canSwipeToEnter, - ) { isUnlocked, canSwipeToDismiss -> - upDestinationSceneKey( + shadeInteractor.isSplitShade, + ) { isUnlocked, canSwipeToDismiss, isSplitShade -> + destinationScenes( isUnlocked = isUnlocked, canSwipeToDismiss = canSwipeToDismiss, + isSplitShade = isSplitShade, ) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = - upDestinationSceneKey( + destinationScenes( isUnlocked = deviceEntryInteractor.isUnlocked.value, canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value, + isSplitShade = shadeInteractor.isSplitShade.value, ), ) + private val upDestinationSceneKey: Flow<SceneKey?> = + destinationScenes.map { it[Swipe(SwipeDirection.Up)]?.toScene } + /** Whether or not the shade container should be clickable. */ val isClickable: StateFlow<Boolean> = upDestinationSceneKey @@ -75,22 +96,43 @@ constructor( initialValue = false ) + /** Whether the current configuration requires the split shade to be shown. */ + val isSplitShade: StateFlow<Boolean> = shadeInteractor.isSplitShade + /** Notifies that some content in the shade was clicked. */ fun onContentClicked() = deviceEntryInteractor.attemptDeviceEntry() - private fun upDestinationSceneKey( - isUnlocked: Boolean, - canSwipeToDismiss: Boolean?, - ): SceneKey { - return when { - canSwipeToDismiss == true -> Scenes.Lockscreen - isUnlocked -> Scenes.Gone - else -> Scenes.Lockscreen - } - } - fun isMediaVisible(): Boolean { // TODO(b/296122467): handle updates to carousel visibility while scene is still visible return mediaDataManager.hasActiveMediaOrRecommendation() } + + private val footerActionsControllerInitialized = AtomicBoolean(false) + + fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { + if (footerActionsControllerInitialized.compareAndSet(false, true)) { + footerActionsController.init() + } + return footerActionsViewModelFactory.create(lifecycleOwner) + } + + private fun destinationScenes( + isUnlocked: Boolean, + canSwipeToDismiss: Boolean?, + isSplitShade: Boolean, + ): Map<UserAction, UserActionResult> { + val up = + when { + canSwipeToDismiss == true -> Scenes.Lockscreen + isUnlocked -> Scenes.Gone + else -> Scenes.Lockscreen + } + + val down = if (isSplitShade) null else Scenes.QuickSettings + + return buildMap { + this[Swipe(SwipeDirection.Up)] = UserActionResult(up) + down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) } + } + } } diff --git a/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt new file mode 100644 index 000000000000..a8ca9bfc7819 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 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 android.os + +import android.testing.TestableLooper +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testCase + +val Kosmos.looper by Fixture { + checkNotNull(TestableLooper.get(testCase).looper) { + "TestableLooper is returning null, make sure the test class is annotated with RunWithLooper" + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt new file mode 100644 index 000000000000..ed291d1b25a3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.animation + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.dialogTransitionAnimator by Fixture { fakeDialogTransitionAnimator() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt index 23d657d0abca..1ce26109ed04 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt @@ -16,16 +16,86 @@ package com.android.systemui.qs +import android.app.admin.devicePolicyManager +import android.content.applicationContext +import android.os.fakeExecutorHandler +import android.os.looper +import com.android.internal.logging.metricsLogger +import com.android.internal.logging.uiEventLogger import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.InstanceIdSequenceFake +import com.android.systemui.animation.dialogTransitionAnimator +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.classifier.falsingManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.plugins.activityStarter import com.android.systemui.plugins.qs.QSFactory +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl +import com.android.systemui.qs.footer.foregroundServicesRepository +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.tiles.di.NewQSTileFactory +import com.android.systemui.security.data.repository.securityRepository +import com.android.systemui.settings.userTracker +import com.android.systemui.statusbar.policy.deviceProvisionedController +import com.android.systemui.statusbar.policy.securityController +import com.android.systemui.user.data.repository.userSwitcherRepository +import com.android.systemui.user.domain.interactor.userSwitcherInteractor +import com.android.systemui.util.mockito.mock -val Kosmos.instanceIdSequenceFake: InstanceIdSequenceFake by - Kosmos.Fixture { InstanceIdSequenceFake(0) } -val Kosmos.qsEventLogger: QsEventLoggerFake by - Kosmos.Fixture { QsEventLoggerFake(uiEventLoggerFake, instanceIdSequenceFake) } +val Kosmos.instanceIdSequenceFake: InstanceIdSequenceFake by Fixture { InstanceIdSequenceFake(0) } +val Kosmos.qsEventLogger: QsEventLoggerFake by Fixture { + QsEventLoggerFake(uiEventLoggerFake, instanceIdSequenceFake) +} -var Kosmos.newQSTileFactory by Kosmos.Fixture<NewQSTileFactory>() -var Kosmos.qsTileFactory by Kosmos.Fixture<QSFactory>() +var Kosmos.newQSTileFactory by Fixture<NewQSTileFactory>() +var Kosmos.qsTileFactory by Fixture<QSFactory>() + +val Kosmos.fgsManagerController by Fixture { FakeFgsManagerController() } + +val Kosmos.footerActionsController by Fixture { + FooterActionsController( + fgsManagerController = fgsManagerController, + ) +} + +val Kosmos.qsSecurityFooterUtils by Fixture { + QSSecurityFooterUtils( + applicationContext, + devicePolicyManager, + userTracker, + fakeExecutorHandler, + activityStarter, + securityController, + looper, + dialogTransitionAnimator, + ) +} + +val Kosmos.footerActionsInteractor by Fixture { + FooterActionsInteractorImpl( + activityStarter = activityStarter, + metricsLogger = metricsLogger, + uiEventLogger = uiEventLogger, + deviceProvisionedController = deviceProvisionedController, + qsSecurityFooterUtils = qsSecurityFooterUtils, + fgsManagerController = fgsManagerController, + userSwitcherInteractor = userSwitcherInteractor, + securityRepository = securityRepository, + foregroundServicesRepository = foregroundServicesRepository, + userSwitcherRepository = userSwitcherRepository, + broadcastDispatcher = broadcastDispatcher, + bgDispatcher = testDispatcher, + ) +} + +val Kosmos.footerActionsViewModelFactory by Fixture { + FooterActionsViewModel.Factory( + context = applicationContext, + falsingManager = falsingManager, + footerActionsInteractor = footerActionsInteractor, + globalActionsDialogLiteProvider = { mock() }, + showPowerButton = true, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/ForegroundServicesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/ForegroundServicesRepositoryKosmos.kt new file mode 100644 index 000000000000..8f81b5efb01c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/ForegroundServicesRepositoryKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 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.qs.footer + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.qs.fgsManagerController +import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl + +val Kosmos.foregroundServicesRepository by Fixture { + ForegroundServicesRepositoryImpl( + fgsManagerController = fgsManagerController, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/security/data/repository/SecurityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/security/data/repository/SecurityRepositoryKosmos.kt new file mode 100644 index 000000000000..6ac5bcbd3af6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/security/data/repository/SecurityRepositoryKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.security.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.policy.securityController + +val Kosmos.securityRepository by Fixture { + SecurityRepositoryImpl( + securityController = securityController, + bgDispatcher = testDispatcher, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/SecurityControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/SecurityControllerKosmos.kt new file mode 100644 index 000000000000..67a5cc98db98 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/SecurityControllerKosmos.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 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.statusbar.policy + +import android.content.applicationContext +import android.os.fakeExecutorHandler +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.settings.userTracker + +val Kosmos.securityController by Fixture { + SecurityControllerImpl( + applicationContext, + userTracker, + fakeExecutorHandler, + broadcastDispatcher, + fakeExecutor, + fakeExecutor, + dumpManager, + ) +} |