diff options
22 files changed, 483 insertions, 134 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index b83c0ce702a7..ecfcc90982c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneContainerStartable import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos @@ -67,10 +68,13 @@ class BouncerViewModelTest : SysuiTestCase() { private val testScope = kosmos.testScope private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val sceneContainerStartable = kosmos.sceneContainerStartable + private lateinit var underTest: BouncerViewModel @Before fun setUp() { + sceneContainerStartable.start() underTest = kosmos.bouncerViewModel } 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 719828ce4a0a..d2a458c8a055 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 @@ -29,6 +29,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode 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.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository @@ -38,6 +39,8 @@ 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.domain.interactor.sceneBackInteractor +import com.android.systemui.scene.domain.interactor.sceneContainerStartable import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel @@ -58,6 +61,7 @@ import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class QuickSettingsSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -71,6 +75,8 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { private val footerActionsController = mock<FooterActionsController>() private val sceneInteractor = kosmos.sceneInteractor + private val sceneBackInteractor = kosmos.sceneBackInteractor + private val sceneContainerStartable = kosmos.sceneContainerStartable private lateinit var underTest: QuickSettingsSceneViewModel @@ -79,6 +85,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { fun setUp() { kosmos.fakeFeatureFlagsClassic.set(Flags.NEW_NETWORK_SLICE_UI, false) + sceneContainerStartable.start() underTest = QuickSettingsSceneViewModel( applicationScope = testScope.backgroundScope, @@ -89,7 +96,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { notifications = kosmos.notificationsPlaceholderViewModel, footerActionsViewModelFactory = footerActionsViewModelFactory, footerActionsController = footerActionsController, - sceneInteractor = sceneInteractor, + sceneBackInteractor = sceneBackInteractor, ) } @@ -127,11 +134,12 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { val destinations by collectLastValue(underTest.destinationScenes) val currentScene by collectLastValue(sceneInteractor.currentScene) - val previousScene by collectLastValue(sceneInteractor.previousScene()) + val backScene by collectLastValue(sceneBackInteractor.backScene) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") sceneInteractor.changeScene(Scenes.QuickSettings, "reason") assertThat(currentScene).isEqualTo(Scenes.QuickSettings) - assertThat(previousScene).isEqualTo(Scenes.Lockscreen) + assertThat(backScene).isEqualTo(Scenes.Lockscreen) + assertThat(destinations) .isEqualTo( mapOf( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 8e2eea178708..a45ac9f7d95d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -140,23 +140,4 @@ class SceneContainerRepositoryTest : SysuiTestCase() { ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } - - @Test - fun previousScene() = - testScope.runTest { - val underTest = kosmos.sceneContainerRepository - val currentScene by collectLastValue(underTest.currentScene) - val previousScene by collectLastValue(underTest.previousScene) - - assertThat(previousScene).isNull() - - val firstScene = currentScene - underTest.changeScene(Scenes.Shade) - assertThat(previousScene).isEqualTo(firstScene) - assertThat(currentScene).isEqualTo(Scenes.Shade) - - underTest.changeScene(Scenes.QuickSettings) - assertThat(previousScene).isEqualTo(Scenes.Shade) - assertThat(currentScene).isEqualTo(Scenes.QuickSettings) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt new file mode 100644 index 000000000000..c75e297f23c8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt @@ -0,0 +1,206 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.SceneKey +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneBackInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val sceneContainerStartable = kosmos.sceneContainerStartable + private val authenticationInteractor = kosmos.authenticationInteractor + + private val underTest = kosmos.sceneBackInteractor + + @Test + @EnableSceneContainer + fun navigateToQs_thenBouncer_thenBack_whileLocked() = + testScope.runTest { + sceneContainerStartable.start() + + assertRoute( + RouteNode(Scenes.Lockscreen, null), + RouteNode(Scenes.Shade, Scenes.Lockscreen), + RouteNode(Scenes.QuickSettings, Scenes.Shade), + RouteNode(Scenes.Bouncer, Scenes.QuickSettings), + RouteNode(Scenes.QuickSettings, Scenes.Shade), + RouteNode(Scenes.Shade, Scenes.Lockscreen), + RouteNode(Scenes.Lockscreen, null), + ) + } + + @Test + @EnableSceneContainer + fun navigateToQs_thenBouncer_thenUnlock() = + testScope.runTest { + sceneContainerStartable.start() + + assertRoute( + RouteNode(Scenes.Lockscreen, null), + RouteNode(Scenes.Shade, Scenes.Lockscreen), + RouteNode(Scenes.QuickSettings, Scenes.Shade), + RouteNode(Scenes.Bouncer, Scenes.QuickSettings, unlockDevice = true), + RouteNode(Scenes.Gone, null), + ) + } + + @Test + @EnableSceneContainer + fun navigateToQs_skippingShade_thenBouncer_thenBack_whileLocked() = + testScope.runTest { + sceneContainerStartable.start() + + assertRoute( + RouteNode(Scenes.Lockscreen, null), + RouteNode(Scenes.QuickSettings, Scenes.Lockscreen), + RouteNode(Scenes.Bouncer, Scenes.QuickSettings), + RouteNode(Scenes.QuickSettings, Scenes.Lockscreen), + RouteNode(Scenes.Lockscreen, null), + ) + } + + @Test + @EnableSceneContainer + fun navigateToBouncer_thenBack_whileLocked() = + testScope.runTest { + sceneContainerStartable.start() + + assertRoute( + RouteNode(Scenes.Lockscreen, null), + RouteNode(Scenes.Bouncer, Scenes.Lockscreen), + RouteNode(Scenes.Lockscreen, null), + ) + } + + @Test + @EnableSceneContainer + fun navigateToQs_skippingShade_thenBouncer_thenBack_thenShade_whileLocked() = + testScope.runTest { + sceneContainerStartable.start() + + assertRoute( + RouteNode(Scenes.Lockscreen, null), + RouteNode(Scenes.QuickSettings, Scenes.Lockscreen), + RouteNode(Scenes.Bouncer, Scenes.QuickSettings), + RouteNode(Scenes.QuickSettings, Scenes.Lockscreen), + RouteNode(Scenes.Lockscreen, null), + RouteNode(Scenes.Shade, Scenes.Lockscreen), + ) + } + + @Test + @EnableSceneContainer + fun navigateToQs_thenBack_whileUnlocked() = + testScope.runTest { + sceneContainerStartable.start() + unlockDevice() + + assertRoute( + RouteNode(Scenes.Gone, null), + RouteNode(Scenes.Shade, Scenes.Gone), + RouteNode(Scenes.QuickSettings, Scenes.Shade), + RouteNode(Scenes.Shade, Scenes.Gone), + RouteNode(Scenes.Gone, null), + ) + } + + @Test + @EnableSceneContainer + fun navigateToQs_skippingShade_thenBack_whileUnlocked() = + testScope.runTest { + sceneContainerStartable.start() + unlockDevice() + + assertRoute( + RouteNode(Scenes.Gone, null), + RouteNode(Scenes.QuickSettings, Scenes.Gone), + RouteNode(Scenes.Gone, null), + ) + } + + @Test + @EnableSceneContainer + fun navigateToQs_skippingShade_thenBack_thenShade_whileUnlocked() = + testScope.runTest { + sceneContainerStartable.start() + unlockDevice() + + assertRoute( + RouteNode(Scenes.Gone, null), + RouteNode(Scenes.QuickSettings, Scenes.Gone), + RouteNode(Scenes.Gone, null), + RouteNode(Scenes.Shade, Scenes.Gone), + ) + } + + private suspend fun TestScope.assertRoute(vararg route: RouteNode) { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val backScene by collectLastValue(underTest.backScene) + + route.forEachIndexed { index, node -> + sceneInteractor.changeScene(node.changeSceneTo, "") + assertWithMessage("node at index $index currentScene mismatch") + .that(currentScene) + .isEqualTo(node.changeSceneTo) + assertWithMessage("node at index $index backScene mismatch") + .that(backScene) + .isEqualTo(node.expectedBackScene) + if (node.unlockDevice) { + unlockDevice() + } + } + } + + private suspend fun TestScope.unlockDevice() { + val currentScene by collectLastValue(sceneInteractor.currentScene) + runCurrent() + assertThat(authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) + .isEqualTo(AuthenticationResult.SUCCEEDED) + assertThat(currentScene).isEqualTo(Scenes.Gone) + } + + private data class RouteNode( + val changeSceneTo: SceneKey, + val expectedBackScene: SceneKey? = null, + val unlockDevice: Boolean = false, + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 871ce6d56e97..2fb8212e2b7c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -291,34 +291,4 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(isVisible).isFalse() } - - @Test - fun previousScene() = - testScope.runTest { - val currentScene by collectLastValue(underTest.currentScene) - val previousScene by collectLastValue(underTest.previousScene()) - assertThat(previousScene).isNull() - - val firstScene = currentScene - underTest.changeScene(toScene = Scenes.Shade, "reason") - assertThat(previousScene).isEqualTo(firstScene) - - underTest.changeScene(toScene = Scenes.QuickSettings, "reason") - assertThat(previousScene).isEqualTo(Scenes.Shade) - } - - @Test - fun previousScene_withIgnoredScene() = - testScope.runTest { - val currentScene by collectLastValue(underTest.currentScene) - val previousScene by collectLastValue(underTest.previousScene(ignored = Scenes.Shade)) - assertThat(previousScene).isNull() - - val firstScene = currentScene - underTest.changeScene(toScene = Scenes.Shade, "reason") - assertThat(previousScene).isEqualTo(firstScene) - - underTest.changeScene(toScene = Scenes.QuickSettings, "reason") - assertThat(previousScene).isNull() - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 2586ad54156f..5779e37cb1ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -392,6 +392,7 @@ class SceneContainerStartableTest : SysuiTestCase() { Scenes.Gone, Scenes.Lockscreen, Scenes.Bouncer, + Scenes.Gone, Scenes.Shade, Scenes.QuickSettings, ) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index cb458efbdf69..45e39caf6cff 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -34,7 +34,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.log.SessionTracker import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.shared.model.Scenes import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -59,7 +59,7 @@ constructor( private val powerInteractor: PowerInteractor, private val uiEventLogger: UiEventLogger, private val sessionTracker: SessionTracker, - sceneInteractor: SceneInteractor, + sceneBackInteractor: SceneBackInteractor, ) { private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>() val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput @@ -95,7 +95,9 @@ constructor( /** The scene to show when bouncer is dismissed. */ val dismissDestination: Flow<SceneKey> = - sceneInteractor.previousScene(Scenes.Bouncer).map { it ?: Scenes.Lockscreen } + sceneBackInteractor.backScene + .filter { it != Scenes.Bouncer } + .map { it ?: Scenes.Lockscreen } /** Notifies that the user has places down a pointer, not necessarily dragging just yet. */ fun onDown() { diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 7fa091ac6455..2406cc6bcea2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -76,7 +76,12 @@ interface CommunalModule { val config = SceneContainerConfig( sceneKeys = listOf(CommunalScenes.Blank, CommunalScenes.Communal), - initialSceneKey = CommunalScenes.Blank + initialSceneKey = CommunalScenes.Blank, + navigationDistances = + mapOf( + CommunalScenes.Blank to 0, + CommunalScenes.Communal to 1, + ), ) return SceneDataSourceDelegator(applicationScope, config) } 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 c1986fa93dd9..22146ce3a18f 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.qs.ui.viewmodel import androidx.lifecycle.LifecycleOwner @@ -30,7 +32,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor 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.domain.interactor.SceneInteractor +import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel @@ -42,6 +44,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Models UI state and handles user input for the quick settings scene. */ @@ -57,21 +61,30 @@ constructor( val notifications: NotificationsPlaceholderViewModel, private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, - sceneInteractor: SceneInteractor, + sceneBackInteractor: SceneBackInteractor, ) { - @OptIn(ExperimentalCoroutinesApi::class) + private val backScene: StateFlow<SceneKey> = + sceneBackInteractor.backScene + .filter { it != Scenes.QuickSettings } + .map { it ?: Scenes.Shade } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = Scenes.Shade, + ) + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = combine( deviceEntryInteractor.isUnlocked, deviceEntryInteractor.canSwipeToEnter, qsSceneAdapter.isCustomizing, - sceneInteractor.previousScene(ignored = Scenes.QuickSettings), - ) { isUnlocked, canSwipeToDismiss, isCustomizing, previousScene -> + backScene, + ) { isUnlocked, canSwipeToDismiss, isCustomizing, backScene -> destinationScenes( isUnlocked, canSwipeToDismiss, isCustomizing, - previousScene, + backScene, ) } .stateIn( @@ -82,8 +95,7 @@ constructor( isUnlocked = deviceEntryInteractor.isUnlocked.value, canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value, isCustomizing = qsSceneAdapter.isCustomizing.value, - previousScene = sceneInteractor - .previousScene(ignored = Scenes.QuickSettings).value, + backScene = backScene.value, ), ) @@ -91,7 +103,7 @@ constructor( isUnlocked: Boolean, canSwipeToDismiss: Boolean?, isCustomizing: Boolean, - previousScene: SceneKey? + backScene: SceneKey?, ): Map<UserAction, UserActionResult> { val upBottomEdge = when { @@ -108,13 +120,15 @@ constructor( // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade // while customizing } else { - this[Back] = UserActionResult(previousScene ?: Scenes.Shade) - this[Swipe(SwipeDirection.Up)] = UserActionResult(previousScene ?: Scenes.Shade) - this[ + put(Back, UserActionResult(backScene ?: Scenes.Shade)) + put(Swipe(SwipeDirection.Up), UserActionResult(backScene ?: Scenes.Shade)) + put( Swipe( fromSource = Edge.Bottom, direction = SwipeDirection.Up, - )] = UserActionResult(upBottomEdge) + ), + UserActionResult(upBottomEdge), + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index 8277c73025d8..2a73b5394a79 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -47,6 +47,12 @@ object KeyguardlessSceneContainerFrameworkModule { Scenes.Shade, ), initialSceneKey = Scenes.Gone, + navigationDistances = + mapOf( + Scenes.Gone to 0, + Scenes.Shade to 1, + Scenes.QuickSettings to 2, + ), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 69f9443b334b..cd1b96508cce 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -73,6 +73,15 @@ interface SceneContainerFrameworkModule { Scenes.Shade, ), initialSceneKey = Scenes.Lockscreen, + navigationDistances = + mapOf( + Scenes.Gone to 0, + Scenes.Lockscreen to 0, + Scenes.Communal to 1, + Scenes.Shade to 2, + Scenes.QuickSettings to 3, + Scenes.Bouncer to 4, + ), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt index d202c24ae152..b918277bd3a4 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt @@ -47,6 +47,11 @@ object ShadelessSceneContainerFrameworkModule { Scenes.Bouncer, ), initialSceneKey = Scenes.Lockscreen, + mapOf( + Scenes.Gone to 0, + Scenes.Lockscreen to 0, + Scenes.Bouncer to 1, + ) ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index 3082eb904a51..994b01216c22 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -24,8 +24,6 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSource -import com.android.systemui.util.kotlin.WithPrev -import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -36,7 +34,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Source of truth for scene framework application state. */ @@ -47,32 +44,7 @@ constructor( private val config: SceneContainerConfig, private val dataSource: SceneDataSource, ) { - private val previousAndCurrentScene: StateFlow<WithPrev<SceneKey?, SceneKey>> = - dataSource.currentScene - .pairwise() - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = WithPrev(null, dataSource.currentScene.value), - ) - - val currentScene: StateFlow<SceneKey> = - previousAndCurrentScene - .map { it.newValue } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = previousAndCurrentScene.value.newValue, - ) - - val previousScene: StateFlow<SceneKey?> = - previousAndCurrentScene - .map { it.previousValue } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = previousAndCurrentScene.value.previousValue, - ) + val currentScene: StateFlow<SceneKey> = dataSource.currentScene private val _isVisible = MutableStateFlow(true) val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt new file mode 100644 index 000000000000..f66d08f781f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt @@ -0,0 +1,107 @@ +/* + * 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.domain.interactor + +import com.android.compose.animation.scene.SceneKey +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.logger.SceneLogger +import com.android.systemui.scene.shared.model.SceneContainerConfig +import java.util.Stack +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class SceneBackInteractor +@Inject +constructor( + private val logger: SceneLogger, + private val sceneContainerConfig: SceneContainerConfig, +) { + private val _backScene = MutableStateFlow<SceneKey?>(null) + /** + * The scene to navigate to when the user triggers back navigation. + * + * This is meant for scene implementations to consult with when they implement their destination + * scene flow. + * + * Note that this flow could emit any scene from the [SceneContainerConfig] and that it's an + * illegal state to have scene implementation map to itself in its destination scene flow. Thus, + * scene implementations might wish to filter their own scene key out before using this. + */ + val backScene: StateFlow<SceneKey?> = _backScene.asStateFlow() + + private val backStack = Stack<SceneKey>() + + fun onSceneChange(from: SceneKey, to: SceneKey) { + check(from != to) { "from == to, from=${from.debugName}, to=${to.debugName}" } + when (stackOperation(from, to)) { + Clear -> { + backStack.clear() + } + Push -> { + backStack.push(from) + } + Pop -> { + check(backStack.isNotEmpty()) { "Cannot pop ${from.debugName} when stack is empty" } + val popped = backStack.pop() + check(to == popped) { + "Expected to pop ${to.debugName} but instead popped ${popped.debugName}" + } + } + } + + logger.logSceneBackStack(backStack) + _backScene.value = peek() + } + + private fun stackOperation(from: SceneKey, to: SceneKey): StackOperation { + val fromDistance = + checkNotNull(sceneContainerConfig.navigationDistances[from]) { + "No distance mapping for scene \"${from.debugName}\"!" + } + val toDistance = + checkNotNull(sceneContainerConfig.navigationDistances[to]) { + "No distance mapping for scene \"${to.debugName}\"!" + } + + return when { + toDistance == 0 -> Clear + toDistance > fromDistance -> Push + toDistance < fromDistance -> Pop + else -> + error( + "No mapping when from=${from.debugName} (distance=$fromDistance)," + + " to=${to.debugName} (distance=$toDistance)!" + ) + } + } + + private fun peek(): SceneKey? { + return if (backStack.isNotEmpty()) { + backStack.peek() + } else { + null + } + } + + private sealed interface StackOperation + private data object Clear : StackOperation + private data object Push : StackOperation + private data object Pop : StackOperation +} 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 8ced22223527..2ccd3b9e9f8a 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 @@ -140,33 +140,6 @@ constructor( ) /** - * The previous scene (or `null` if the previous scene is the [ignored] scene). - * - * This is effectively the previous value of [currentScene] which means that all caveats, for - * example regarding when in a transition the current scene changes, apply. - * - * @param ignored If the previous scene is the same as [ignored], `null` is emitted. This is - * designed to reduce the chances of a scene using [previousScene] naively to then set up a - * user action that ends up leading to itself, which is an illegal operation that would cause - * a crash. - */ - fun previousScene( - ignored: SceneKey? = null, - ): StateFlow<SceneKey?> { - fun SceneKey?.nullifyIfIgnored(): SceneKey? { - return this?.takeIf { this != ignored } - } - - return repository.previousScene - .map { it.nullifyIfIgnored() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = repository.previousScene.value.nullifyIfIgnored(), - ) - } - - /** * Returns the keys of all scenes in the container. * * The scenes will be sorted in z-order such that the last one is the one that should be diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 4fc24b898605..39ec12f2701c 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -45,6 +45,7 @@ import com.android.systemui.model.updateFlags import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -56,6 +57,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNoti import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor import com.android.systemui.util.asIndenting +import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import com.android.systemui.util.printSection import com.android.systemui.util.println @@ -111,6 +113,7 @@ constructor( private val faceUnlockInteractor: DeviceEntryFaceAuthInteractor, private val shadeInteractor: ShadeInteractor, private val uiEventLogger: UiEventLogger, + private val sceneBackInteractor: SceneBackInteractor, ) : CoreStartable { override fun start() { @@ -124,6 +127,7 @@ constructor( hydrateInteractionState() handleBouncerOverscroll() hydrateWindowController() + hydrateBackStack() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -257,8 +261,7 @@ constructor( // Track the previous scene (sans Bouncer), so that we know where to go when the device // is unlocked whilst on the bouncer. val previousScene = - sceneInteractor - .previousScene() + sceneBackInteractor.backScene .filterNot { it == Scenes.Bouncer } .stateIn(this, SharingStarted.Eagerly, initialValue = null) deviceUnlockedInteractor.deviceUnlockStatus @@ -581,4 +584,12 @@ constructor( loggingReason = loggingReason, ) } + + private fun hydrateBackStack() { + applicationScope.launch { + sceneInteractor.currentScene.pairwise().collect { (from, to) -> + sceneBackInteractor.onSceneChange(from = from, to = to) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index f44779ade8db..5ebdd8698656 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -20,6 +20,7 @@ import com.android.compose.animation.scene.SceneKey import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.SceneFrameworkLog +import java.util.Stack import javax.inject.Inject class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: LogBuffer) { @@ -102,7 +103,7 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: tag = TAG, level = LogLevel.INFO, messageInitializer = { str1 = reason }, - messagePrinter = { "remote user interaction started, reason: $str3" }, + messagePrinter = { "remote user interaction started, reason: $str1" }, ) } @@ -115,6 +116,15 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } + fun logSceneBackStack(backStack: Stack<SceneKey>) { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = { str1 = backStack.joinToString(", ") { it.debugName } }, + messagePrinter = { "back stack: $str1" }, + ) + } + companion object { private const val TAG = "SceneFramework" } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt index 53cdaaab7478..0a30c31ca739 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt @@ -25,6 +25,9 @@ data class SceneContainerConfig( * The keys to all scenes in the container, sorted by z-order such that the last one renders on * top of all previous ones. Scene keys within the same container must not repeat but it's okay * to have the same scene keys in different containers. + * + * Note that this doesn't control how back navigation works; for that, we have + * [navigationDistances]. */ val sceneKeys: List<SceneKey>, @@ -33,6 +36,24 @@ data class SceneContainerConfig( * before taking any application state in to account. */ val initialSceneKey: SceneKey, + + /** + * Navigation distance of each scene. + * + * The navigation distance is a measure of how many non-back user action "steps" away from the + * starting scene, each scene is. + * + * The framework uses these to help scene implementations decide which scene to go back to when + * the user attempts to navigate back on them, if they need that. + * + * In general, the more non-back user actions are needed to get to a scene, the greater that + * scene's distance should be. Navigating "back" then goes from scenes with a higher distance to + * scenes with a lower distance. + * + * Note that this is not the z-order of rendering; that's determined by the order of declaration + * of scenes in the [sceneKeys] list. + */ + val navigationDistances: Map<SceneKey, Int> ) { init { check(sceneKeys.isNotEmpty()) { "A container must have at least one scene key." } @@ -40,5 +61,9 @@ data class SceneContainerConfig( check(sceneKeys.contains(initialSceneKey)) { "The initial key \"$initialSceneKey\" is not present in this container." } + + check(navigationDistances.keys == sceneKeys.toSet()) { + "Scene keys and distance map must match." + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt index 4a02f6ddbebd..43948472597d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt @@ -26,7 +26,7 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope import com.android.systemui.log.sessionTracker import com.android.systemui.power.domain.interactor.powerInteractor -import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.domain.interactor.sceneBackInteractor val Kosmos.bouncerInteractor by Fixture { BouncerInteractor( @@ -38,6 +38,6 @@ val Kosmos.bouncerInteractor by Fixture { powerInteractor = powerInteractor, uiEventLogger = uiEventLogger, sessionTracker = sessionTracker, - sceneInteractor = sceneInteractor, + sceneBackInteractor = sceneBackInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 7f6a7bd3f7d8..16d08dd91ca8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -32,4 +32,15 @@ val Kosmos.fakeScenes by Fixture { val Kosmos.scenes by Fixture { fakeScenes } val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen } -val Kosmos.sceneContainerConfig by Fixture { SceneContainerConfig(sceneKeys, initialSceneKey) } +val Kosmos.sceneContainerConfig by Fixture { + val navigationDistances = + mapOf( + Scenes.Gone to 0, + Scenes.Lockscreen to 0, + Scenes.Communal to 1, + Scenes.Shade to 2, + Scenes.QuickSettings to 3, + Scenes.Bouncer to 4, + ) + SceneContainerConfig(sceneKeys, initialSceneKey, navigationDistances) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.kt new file mode 100644 index 000000000000..e46ede65bfb6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorKosmos.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.scene.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.scene.sceneContainerConfig +import com.android.systemui.scene.shared.logger.sceneLogger + +val Kosmos.sceneBackInteractor by Fixture { + SceneBackInteractor( + logger = sceneLogger, + sceneContainerConfig = sceneContainerConfig, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt index c7cf9342f87e..c0f50393b1d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt @@ -39,7 +39,6 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads import com.android.systemui.statusbar.notificationShadeWindowController import com.android.systemui.statusbar.phone.centralSurfaces import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor -import dagger.Lazy val Kosmos.sceneContainerStartable by Fixture { SceneContainerStartable( @@ -55,8 +54,8 @@ val Kosmos.sceneContainerStartable by Fixture { falsingCollector = falsingCollector, falsingManager = falsingManager, powerInteractor = powerInteractor, - simBouncerInteractor = Lazy { simBouncerInteractor }, - authenticationInteractor = Lazy { authenticationInteractor }, + simBouncerInteractor = { simBouncerInteractor }, + authenticationInteractor = { authenticationInteractor }, windowController = notificationShadeWindowController, deviceProvisioningInteractor = deviceProvisioningInteractor, centralSurfaces = centralSurfaces, @@ -65,5 +64,6 @@ val Kosmos.sceneContainerStartable by Fixture { faceUnlockInteractor = deviceEntryFaceAuthInteractor, shadeInteractor = shadeInteractor, uiEventLogger = uiEventLogger, + sceneBackInteractor = sceneBackInteractor, ) } |