Loading packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +7 −0 Original line number Diff line number Diff line Loading @@ -109,6 +109,13 @@ fun SceneContainer( rememberMutableSceneTransitionLayoutState( initialScene = initialSceneKey, canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, canShowOverlay = { overlay -> viewModel.canShowOrReplaceOverlay(overlay) }, canReplaceOverlay = { beingReplaced, newlyShown -> viewModel.canShowOrReplaceOverlay( newlyShown = newlyShown, beingReplaced = beingReplaced, ) }, transitions = sceneTransitions, onTransitionStart = { transition -> sceneJankMonitor.onTransitionStart( Loading packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +88 −32 Original line number Diff line number Diff line Loading @@ -25,8 +25,10 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.data.repository.fakePowerRepository Loading @@ -44,17 +46,15 @@ import com.android.systemui.shade.domain.interactor.shadeMode import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading @@ -64,10 +64,6 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope by lazy { kosmos.testScope } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } private val fakeRemoteInputRepository by lazy { kosmos.fakeRemoteInputRepository } private val falsingManager by lazy { kosmos.fakeFalsingManager } private val view = mock<View>() Loading @@ -91,14 +87,14 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun activate_setsMotionEventHandler() = testScope.runTest { kosmos.runTest { runCurrent() assertThat(motionEventHandler).isNotNull() } @Test fun deactivate_clearsMotionEventHandler() = testScope.runTest { kosmos.runTest { activationJob.cancel() runCurrent() Loading @@ -107,7 +103,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun isVisible() = testScope.runTest { kosmos.runTest { assertThat(underTest.isVisible).isTrue() sceneInteractor.setVisible(false, "reason") Loading @@ -121,7 +117,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun sceneTransition() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) Loading @@ -132,7 +128,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenAllowed_switchingFromGone_returnsTrue() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) runCurrent() Loading @@ -149,7 +145,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenAllowed_switchingFromLockscreen_returnsTrue() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) runCurrent() Loading @@ -166,7 +162,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingProtectedScenes_returnsFalse() = testScope.runTest { kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) Loading @@ -188,7 +184,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingUnprotectedScenes_returnsTrue() = testScope.runTest { kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) Loading @@ -209,7 +205,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenNotAllowed_fromGone_toAnyOtherScene_returnsTrue() = testScope.runTest { kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) Loading @@ -225,9 +221,71 @@ class SceneContainerViewModelTest : SysuiTestCase() { } } @Test fun canShowOrReplaceOverlay_whenAllowed_showingWhileOnGone_returnsTrue() = kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Overlay $overlay incorrectly protected when allowed") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isTrue() } } @Test fun canShowOrReplaceOverlay_whenAllowed_showingWhileOnLockscreen_returnsTrue() = kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Overlay $overlay incorrectly protected when allowed") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isTrue() } } @Test fun canShowOrReplaceOverlay_whenNotAllowed_whileOnLockscreen_returnsFalse() = kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Protected overlay $overlay not properly protected") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isFalse() } } @Test fun canShowOrReplaceOverlay_whenNotAllowed_whileOnGone_returnsTrue() = kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Protected overlay $overlay not properly protected") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isTrue() } } @Test fun userInput() = testScope.runTest { kosmos.runTest { assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() underTest.onMotionEvent(mock()) assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() Loading @@ -235,7 +293,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun userInputOnEmptySpace_insideEvent() = testScope.runTest { kosmos.runTest { assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() val insideMotionEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) underTest.onEmptySpaceMotionEvent(insideMotionEvent) Loading @@ -244,7 +302,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun userInputOnEmptySpace_outsideEvent_remoteInputActive() = testScope.runTest { kosmos.runTest { fakeRemoteInputRepository.isRemoteInputActive.value = true assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) Loading @@ -254,7 +312,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun userInputOnEmptySpace_outsideEvent_remoteInputInactive() = testScope.runTest { kosmos.runTest { fakeRemoteInputRepository.isRemoteInputActive.value = false assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) Loading @@ -264,7 +322,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun remoteUserInteraction_keepsContainerVisible() = testScope.runTest { kosmos.runTest { sceneInteractor.setVisible(false, "reason") runCurrent() assertThat(underTest.isVisible).isFalse() Loading @@ -272,9 +330,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { runCurrent() assertThat(underTest.isVisible).isTrue() underTest.onMotionEvent( mock { whenever(actionMasked).thenReturn(MotionEvent.ACTION_UP) } ) underTest.onMotionEvent(mock { on { actionMasked } doReturn MotionEvent.ACTION_UP }) runCurrent() assertThat(underTest.isVisible).isFalse() Loading @@ -282,7 +338,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun getActionableContentKey_noOverlays_returnsCurrentScene() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) Loading @@ -300,7 +356,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun getActionableContentKey_multipleOverlays_returnsTopOverlay() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) fakeSceneDataSource.showOverlay(Overlays.QuickSettingsShade) Loading @@ -321,7 +377,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_singleShade_usesDefaultEdgeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableSingleShade() Loading @@ -331,7 +387,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_splitShade_usesDefaultEdgeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableSplitShade() Loading @@ -341,7 +397,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = false) Loading @@ -352,7 +408,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = true) Loading packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +51 −25 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import com.android.systemui.lifecycle.Hydrator import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.domain.interactor.ShadeModeInteractor Loading Loading @@ -218,39 +219,36 @@ constructor( * it being a false touch. */ fun canChangeScene(toScene: SceneKey): Boolean { val interactionTypeOrNull = when (toScene) { Scenes.Bouncer -> Classifier.BOUNCER_UNLOCK Scenes.Gone -> Classifier.UNLOCK Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.QuickSettings -> Classifier.QUICK_SETTINGS else -> null } val fromScene = currentScene.value val isAllowed = interactionTypeOrNull?.let { interactionType -> // It's important that the falsing system is always queried, even if no enforcement // will occur. This helps build up the right signal in the system. val isFalseTouch = falsingInteractor.isFalseTouch(interactionType) // Only enforce falsing if moving from the lockscreen scene to a new scene. val fromLockscreenScene = fromScene == Scenes.Lockscreen !fromLockscreenScene || !isFalseTouch } ?: true if (isAllowed) { return isInteractionAllowedByFalsing(toScene).also { // A scene change is guaranteed; log it. logger.logSceneChanged( from = fromScene, from = currentScene.value, to = toScene, sceneState = null, reason = "user interaction", isInstant = false, ) } return isAllowed } /** * Returns `true` if showing the [newlyShown] overlay is currently allowed; `false` otherwise. * * This is invoked only for user-initiated transitions. The goal is to check with the falsing * system whether the overlay change should be rejected due to it being a false touch. */ fun canShowOrReplaceOverlay( newlyShown: OverlayKey, beingReplaced: OverlayKey? = null, ): Boolean { return isInteractionAllowedByFalsing(newlyShown).also { // An overlay change is guaranteed; log it. logger.logOverlayChangeRequested( from = beingReplaced, to = newlyShown, reason = "user interaction", ) } } /** Loading Loading @@ -313,6 +311,34 @@ constructor( return sceneInteractor.filteredUserActions(unfiltered) } /** * Returns `true` if transitioning to [content] is permissible by the falsing system; `false` * otherwise. */ private fun isInteractionAllowedByFalsing(content: ContentKey): Boolean { val interactionTypeOrNull = when (content) { Scenes.Bouncer -> Classifier.BOUNCER_UNLOCK Scenes.Gone -> Classifier.UNLOCK Scenes.Shade, Overlays.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.QuickSettings, Overlays.QuickSettingsShade -> Classifier.QUICK_SETTINGS else -> null } return interactionTypeOrNull?.let { interactionType -> // It's important that the falsing system is always queried, even if no enforcement // will occur. This helps build up the right signal in the system. val isFalseTouch = falsingInteractor.isFalseTouch(interactionType) // Only enforce falsing if moving from the lockscreen scene to new content. val fromLockscreenScene = currentScene.value == Scenes.Lockscreen !fromLockscreenScene || !isFalseTouch } ?: true } /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */ interface MotionEventHandler { /** Notifies that a [MotionEvent] has occurred. */ Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +7 −0 Original line number Diff line number Diff line Loading @@ -109,6 +109,13 @@ fun SceneContainer( rememberMutableSceneTransitionLayoutState( initialScene = initialSceneKey, canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, canShowOverlay = { overlay -> viewModel.canShowOrReplaceOverlay(overlay) }, canReplaceOverlay = { beingReplaced, newlyShown -> viewModel.canShowOrReplaceOverlay( newlyShown = newlyShown, beingReplaced = beingReplaced, ) }, transitions = sceneTransitions, onTransitionStart = { transition -> sceneJankMonitor.onTransitionStart( Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +88 −32 Original line number Diff line number Diff line Loading @@ -25,8 +25,10 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.data.repository.fakePowerRepository Loading @@ -44,17 +46,15 @@ import com.android.systemui.shade.domain.interactor.shadeMode import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock @OptIn(ExperimentalCoroutinesApi::class) @SmallTest Loading @@ -64,10 +64,6 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope by lazy { kosmos.testScope } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } private val fakeRemoteInputRepository by lazy { kosmos.fakeRemoteInputRepository } private val falsingManager by lazy { kosmos.fakeFalsingManager } private val view = mock<View>() Loading @@ -91,14 +87,14 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun activate_setsMotionEventHandler() = testScope.runTest { kosmos.runTest { runCurrent() assertThat(motionEventHandler).isNotNull() } @Test fun deactivate_clearsMotionEventHandler() = testScope.runTest { kosmos.runTest { activationJob.cancel() runCurrent() Loading @@ -107,7 +103,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun isVisible() = testScope.runTest { kosmos.runTest { assertThat(underTest.isVisible).isTrue() sceneInteractor.setVisible(false, "reason") Loading @@ -121,7 +117,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun sceneTransition() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) Loading @@ -132,7 +128,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenAllowed_switchingFromGone_returnsTrue() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) runCurrent() Loading @@ -149,7 +145,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenAllowed_switchingFromLockscreen_returnsTrue() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) runCurrent() Loading @@ -166,7 +162,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingProtectedScenes_returnsFalse() = testScope.runTest { kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) Loading @@ -188,7 +184,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingUnprotectedScenes_returnsTrue() = testScope.runTest { kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) Loading @@ -209,7 +205,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun canChangeScene_whenNotAllowed_fromGone_toAnyOtherScene_returnsTrue() = testScope.runTest { kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) Loading @@ -225,9 +221,71 @@ class SceneContainerViewModelTest : SysuiTestCase() { } } @Test fun canShowOrReplaceOverlay_whenAllowed_showingWhileOnGone_returnsTrue() = kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Overlay $overlay incorrectly protected when allowed") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isTrue() } } @Test fun canShowOrReplaceOverlay_whenAllowed_showingWhileOnLockscreen_returnsTrue() = kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Overlay $overlay incorrectly protected when allowed") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isTrue() } } @Test fun canShowOrReplaceOverlay_whenNotAllowed_whileOnLockscreen_returnsFalse() = kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Protected overlay $overlay not properly protected") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isFalse() } } @Test fun canShowOrReplaceOverlay_whenNotAllowed_whileOnGone_returnsTrue() = kosmos.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = Scenes.Gone) runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) sceneContainerConfig.overlayKeys.forEach { overlay -> assertWithMessage("Protected overlay $overlay not properly protected") .that(underTest.canShowOrReplaceOverlay(newlyShown = overlay)) .isTrue() } } @Test fun userInput() = testScope.runTest { kosmos.runTest { assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() underTest.onMotionEvent(mock()) assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() Loading @@ -235,7 +293,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun userInputOnEmptySpace_insideEvent() = testScope.runTest { kosmos.runTest { assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() val insideMotionEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) underTest.onEmptySpaceMotionEvent(insideMotionEvent) Loading @@ -244,7 +302,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun userInputOnEmptySpace_outsideEvent_remoteInputActive() = testScope.runTest { kosmos.runTest { fakeRemoteInputRepository.isRemoteInputActive.value = true assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) Loading @@ -254,7 +312,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun userInputOnEmptySpace_outsideEvent_remoteInputInactive() = testScope.runTest { kosmos.runTest { fakeRemoteInputRepository.isRemoteInputActive.value = false assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) Loading @@ -264,7 +322,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun remoteUserInteraction_keepsContainerVisible() = testScope.runTest { kosmos.runTest { sceneInteractor.setVisible(false, "reason") runCurrent() assertThat(underTest.isVisible).isFalse() Loading @@ -272,9 +330,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { runCurrent() assertThat(underTest.isVisible).isTrue() underTest.onMotionEvent( mock { whenever(actionMasked).thenReturn(MotionEvent.ACTION_UP) } ) underTest.onMotionEvent(mock { on { actionMasked } doReturn MotionEvent.ACTION_UP }) runCurrent() assertThat(underTest.isVisible).isFalse() Loading @@ -282,7 +338,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun getActionableContentKey_noOverlays_returnsCurrentScene() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) Loading @@ -300,7 +356,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun getActionableContentKey_multipleOverlays_returnsTopOverlay() = testScope.runTest { kosmos.runTest { val currentScene by collectLastValue(underTest.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) fakeSceneDataSource.showOverlay(Overlays.QuickSettingsShade) Loading @@ -321,7 +377,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_singleShade_usesDefaultEdgeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableSingleShade() Loading @@ -331,7 +387,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_splitShade_usesDefaultEdgeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableSplitShade() Loading @@ -341,7 +397,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = false) Loading @@ -352,7 +408,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() = testScope.runTest { kosmos.runTest { val shadeMode by collectLastValue(kosmos.shadeMode) kosmos.enableDualShade(wideLayout = true) Loading
packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +51 −25 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import com.android.systemui.lifecycle.Hydrator import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.domain.interactor.ShadeModeInteractor Loading Loading @@ -218,39 +219,36 @@ constructor( * it being a false touch. */ fun canChangeScene(toScene: SceneKey): Boolean { val interactionTypeOrNull = when (toScene) { Scenes.Bouncer -> Classifier.BOUNCER_UNLOCK Scenes.Gone -> Classifier.UNLOCK Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.QuickSettings -> Classifier.QUICK_SETTINGS else -> null } val fromScene = currentScene.value val isAllowed = interactionTypeOrNull?.let { interactionType -> // It's important that the falsing system is always queried, even if no enforcement // will occur. This helps build up the right signal in the system. val isFalseTouch = falsingInteractor.isFalseTouch(interactionType) // Only enforce falsing if moving from the lockscreen scene to a new scene. val fromLockscreenScene = fromScene == Scenes.Lockscreen !fromLockscreenScene || !isFalseTouch } ?: true if (isAllowed) { return isInteractionAllowedByFalsing(toScene).also { // A scene change is guaranteed; log it. logger.logSceneChanged( from = fromScene, from = currentScene.value, to = toScene, sceneState = null, reason = "user interaction", isInstant = false, ) } return isAllowed } /** * Returns `true` if showing the [newlyShown] overlay is currently allowed; `false` otherwise. * * This is invoked only for user-initiated transitions. The goal is to check with the falsing * system whether the overlay change should be rejected due to it being a false touch. */ fun canShowOrReplaceOverlay( newlyShown: OverlayKey, beingReplaced: OverlayKey? = null, ): Boolean { return isInteractionAllowedByFalsing(newlyShown).also { // An overlay change is guaranteed; log it. logger.logOverlayChangeRequested( from = beingReplaced, to = newlyShown, reason = "user interaction", ) } } /** Loading Loading @@ -313,6 +311,34 @@ constructor( return sceneInteractor.filteredUserActions(unfiltered) } /** * Returns `true` if transitioning to [content] is permissible by the falsing system; `false` * otherwise. */ private fun isInteractionAllowedByFalsing(content: ContentKey): Boolean { val interactionTypeOrNull = when (content) { Scenes.Bouncer -> Classifier.BOUNCER_UNLOCK Scenes.Gone -> Classifier.UNLOCK Scenes.Shade, Overlays.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.QuickSettings, Overlays.QuickSettingsShade -> Classifier.QUICK_SETTINGS else -> null } return interactionTypeOrNull?.let { interactionType -> // It's important that the falsing system is always queried, even if no enforcement // will occur. This helps build up the right signal in the system. val isFalseTouch = falsingInteractor.isFalseTouch(interactionType) // Only enforce falsing if moving from the lockscreen scene to new content. val fromLockscreenScene = currentScene.value == Scenes.Lockscreen !fromLockscreenScene || !isFalseTouch } ?: true } /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */ interface MotionEventHandler { /** Notifies that a [MotionEvent] has occurred. */ Loading