Loading packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +1 −0 Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ fun SceneContainer( val state: MutableSceneTransitionLayoutState = remember { MutableSceneTransitionLayoutState( initialScene = currentSceneKey.asComposeAware(), canChangeScene = { toScene -> viewModel.canChangeScene(toScene.asComposeUnaware()) }, transitions = SceneContainerTransitions, ) } Loading packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +101 −0 Original line number Diff line number Diff line Loading @@ -22,16 +22,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.sceneKeys import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.fakeSceneDataSource 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.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test Loading @@ -45,6 +49,8 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val testScope by lazy { kosmos.testScope } private val interactor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource = kosmos.fakeSceneDataSource private val sceneContainerConfig = kosmos.sceneContainerConfig private val falsingManager = kosmos.fakeFalsingManager private lateinit var underTest: SceneContainerViewModel Loading Loading @@ -86,4 +92,99 @@ class SceneContainerViewModelTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneKey.Shade) } @Test fun canChangeScene_whenAllowed_switchingFromGone_returnsTrue() = testScope.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Gone) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Gone) sceneContainerConfig.sceneKeys .filter { it != currentScene } .forEach { toScene -> assertWithMessage("Scene $toScene incorrectly protected when allowed") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } @Test fun canChangeScene_whenAllowed_switchingFromLockscreen_returnsTrue() = testScope.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) sceneContainerConfig.sceneKeys .filter { it != currentScene } .forEach { toScene -> assertWithMessage("Scene $toScene incorrectly protected when allowed") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingProtectedScenes_returnsFalse() = testScope.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) sceneContainerConfig.sceneKeys .filter { it != currentScene } .filter { // Moving to the Communal scene is not currently falsing protected. it != SceneKey.Communal } .forEach { toScene -> assertWithMessage("Protected scene $toScene not properly protected") .that(underTest.canChangeScene(toScene = toScene)) .isFalse() } } @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingUnprotectedScenes_returnsTrue() = testScope.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) sceneContainerConfig.sceneKeys .filter { // Moving to the Communal scene is not currently falsing protected. it == SceneKey.Communal } .forEach { toScene -> assertWithMessage("Unprotected scene $toScene is incorrectly protected") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } @Test fun canChangeScene_whenNotAllowed_fromGone_toAnyOtherScene_returnsTrue() = testScope.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Gone) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Gone) sceneContainerConfig.sceneKeys .filter { it != currentScene } .forEach { toScene -> assertWithMessage("Protected scene $toScene not properly protected") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } } packages/SystemUI/src/com/android/systemui/classifier/domain/interactor/FalsingInteractor.kt +12 −3 Original line number Diff line number Diff line Loading @@ -17,23 +17,27 @@ package com.android.systemui.classifier.domain.interactor import android.view.MotionEvent import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.FalsingClassifier import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.FalsingManager import javax.inject.Inject /** * Exposes the subset of the [FalsingCollector] API that's required by external callers. * Exposes the subset of the [FalsingCollector] and [FalsingManager] APIs that's required by * external callers. * * E.g. methods of [FalsingCollector] that are not exposed by this class don't need to be invoked by * external callers as they're already called by the scene framework. * E.g. methods of the above APIs that are not exposed by this class either don't need to be invoked * by external callers (as they're already called by the scene framework) or haven't been added yet. */ @SysUISingleton class FalsingInteractor @Inject constructor( @FalsingCollectorActual private val collector: FalsingCollector, private val manager: FalsingManager, ) { /** * Notifies of a [MotionEvent] that passed through the UI. Loading Loading @@ -62,4 +66,9 @@ constructor( fun updateFalseConfidence( result: FalsingClassifier.Result, ) = collector.updateFalseConfidence(result) /** Returns `true` if the gesture should be rejected. */ fun isFalseTouch( @Classifier.InteractionType interactionType: Int, ): Boolean = manager.isFalseTouch(interactionType) } packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +29 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.domain.interactor.SceneInteractor Loading Loading @@ -77,7 +78,33 @@ constructor( falsingInteractor.onMotionEventComplete() } companion object { private const val SCENE_TRANSITION_LOGGING_REASON = "user input" /** * Returns `true` if a change to [toScene] is currently allowed; `false` otherwise. * * This is invoked only for user-initiated transitions. The goal is to check with the falsing * system whether the change from the current scene to the given scene should be rejected due to * it being a false touch. */ fun canChangeScene(toScene: SceneKey): Boolean { val interactionTypeOrNull = when (toScene) { SceneKey.Bouncer -> Classifier.BOUNCER_UNLOCK SceneKey.Gone -> Classifier.UNLOCK SceneKey.Shade -> Classifier.NOTIFICATION_DRAG_DOWN SceneKey.QuickSettings -> 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 a new scene. val fromLockscreenScene = currentScene.value == SceneKey.Lockscreen !fromLockscreenScene || !isFalseTouch } ?: true } } packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt +2 −0 Original line number Diff line number Diff line Loading @@ -17,11 +17,13 @@ package com.android.systemui.classifier.domain.interactor import com.android.systemui.classifier.falsingCollector import com.android.systemui.classifier.falsingManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture val Kosmos.falsingInteractor by Fixture { FalsingInteractor( collector = falsingCollector, manager = falsingManager, ) } Loading
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +1 −0 Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ fun SceneContainer( val state: MutableSceneTransitionLayoutState = remember { MutableSceneTransitionLayoutState( initialScene = currentSceneKey.asComposeAware(), canChangeScene = { toScene -> viewModel.canChangeScene(toScene.asComposeUnaware()) }, transitions = SceneContainerTransitions, ) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +101 −0 Original line number Diff line number Diff line Loading @@ -22,16 +22,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.sceneKeys import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.fakeSceneDataSource 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.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test Loading @@ -45,6 +49,8 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val testScope by lazy { kosmos.testScope } private val interactor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource = kosmos.fakeSceneDataSource private val sceneContainerConfig = kosmos.sceneContainerConfig private val falsingManager = kosmos.fakeFalsingManager private lateinit var underTest: SceneContainerViewModel Loading Loading @@ -86,4 +92,99 @@ class SceneContainerViewModelTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneKey.Shade) } @Test fun canChangeScene_whenAllowed_switchingFromGone_returnsTrue() = testScope.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Gone) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Gone) sceneContainerConfig.sceneKeys .filter { it != currentScene } .forEach { toScene -> assertWithMessage("Scene $toScene incorrectly protected when allowed") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } @Test fun canChangeScene_whenAllowed_switchingFromLockscreen_returnsTrue() = testScope.runTest { val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) sceneContainerConfig.sceneKeys .filter { it != currentScene } .forEach { toScene -> assertWithMessage("Scene $toScene incorrectly protected when allowed") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingProtectedScenes_returnsFalse() = testScope.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) sceneContainerConfig.sceneKeys .filter { it != currentScene } .filter { // Moving to the Communal scene is not currently falsing protected. it != SceneKey.Communal } .forEach { toScene -> assertWithMessage("Protected scene $toScene not properly protected") .that(underTest.canChangeScene(toScene = toScene)) .isFalse() } } @Test fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingUnprotectedScenes_returnsTrue() = testScope.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Lockscreen) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) sceneContainerConfig.sceneKeys .filter { // Moving to the Communal scene is not currently falsing protected. it == SceneKey.Communal } .forEach { toScene -> assertWithMessage("Unprotected scene $toScene is incorrectly protected") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } @Test fun canChangeScene_whenNotAllowed_fromGone_toAnyOtherScene_returnsTrue() = testScope.runTest { falsingManager.setIsFalseTouch(true) val currentScene by collectLastValue(underTest.currentScene) fakeSceneDataSource.changeScene(toScene = SceneKey.Gone) runCurrent() assertThat(currentScene).isEqualTo(SceneKey.Gone) sceneContainerConfig.sceneKeys .filter { it != currentScene } .forEach { toScene -> assertWithMessage("Protected scene $toScene not properly protected") .that(underTest.canChangeScene(toScene = toScene)) .isTrue() } } }
packages/SystemUI/src/com/android/systemui/classifier/domain/interactor/FalsingInteractor.kt +12 −3 Original line number Diff line number Diff line Loading @@ -17,23 +17,27 @@ package com.android.systemui.classifier.domain.interactor import android.view.MotionEvent import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.FalsingClassifier import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.FalsingManager import javax.inject.Inject /** * Exposes the subset of the [FalsingCollector] API that's required by external callers. * Exposes the subset of the [FalsingCollector] and [FalsingManager] APIs that's required by * external callers. * * E.g. methods of [FalsingCollector] that are not exposed by this class don't need to be invoked by * external callers as they're already called by the scene framework. * E.g. methods of the above APIs that are not exposed by this class either don't need to be invoked * by external callers (as they're already called by the scene framework) or haven't been added yet. */ @SysUISingleton class FalsingInteractor @Inject constructor( @FalsingCollectorActual private val collector: FalsingCollector, private val manager: FalsingManager, ) { /** * Notifies of a [MotionEvent] that passed through the UI. Loading Loading @@ -62,4 +66,9 @@ constructor( fun updateFalseConfidence( result: FalsingClassifier.Result, ) = collector.updateFalseConfidence(result) /** Returns `true` if the gesture should be rejected. */ fun isFalseTouch( @Classifier.InteractionType interactionType: Int, ): Boolean = manager.isFalseTouch(interactionType) }
packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +29 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.domain.interactor.SceneInteractor Loading Loading @@ -77,7 +78,33 @@ constructor( falsingInteractor.onMotionEventComplete() } companion object { private const val SCENE_TRANSITION_LOGGING_REASON = "user input" /** * Returns `true` if a change to [toScene] is currently allowed; `false` otherwise. * * This is invoked only for user-initiated transitions. The goal is to check with the falsing * system whether the change from the current scene to the given scene should be rejected due to * it being a false touch. */ fun canChangeScene(toScene: SceneKey): Boolean { val interactionTypeOrNull = when (toScene) { SceneKey.Bouncer -> Classifier.BOUNCER_UNLOCK SceneKey.Gone -> Classifier.UNLOCK SceneKey.Shade -> Classifier.NOTIFICATION_DRAG_DOWN SceneKey.QuickSettings -> 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 a new scene. val fromLockscreenScene = currentScene.value == SceneKey.Lockscreen !fromLockscreenScene || !isFalseTouch } ?: true } }
packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt +2 −0 Original line number Diff line number Diff line Loading @@ -17,11 +17,13 @@ package com.android.systemui.classifier.domain.interactor import com.android.systemui.classifier.falsingCollector import com.android.systemui.classifier.falsingManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture val Kosmos.falsingInteractor by Fixture { FalsingInteractor( collector = falsingCollector, manager = falsingManager, ) }