Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26ceeeb8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] Adds falsing protection for overlays." into main

parents be24db38 1f48041f
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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(
+88 −32
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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>()

@@ -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()

@@ -107,7 +103,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {

    @Test
    fun isVisible() =
        testScope.runTest {
        kosmos.runTest {
            assertThat(underTest.isVisible).isTrue()

            sceneInteractor.setVisible(false, "reason")
@@ -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)

@@ -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()
@@ -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()
@@ -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)
@@ -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)
@@ -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)
@@ -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()
@@ -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)
@@ -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)
@@ -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)
@@ -264,7 +322,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {

    @Test
    fun remoteUserInteraction_keepsContainerVisible() =
        testScope.runTest {
        kosmos.runTest {
            sceneInteractor.setVisible(false, "reason")
            runCurrent()
            assertThat(underTest.isVisible).isFalse()
@@ -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()
@@ -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)
@@ -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)
@@ -321,7 +377,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {

    @Test
    fun edgeDetector_singleShade_usesDefaultEdgeDetector() =
        testScope.runTest {
        kosmos.runTest {
            val shadeMode by collectLastValue(kosmos.shadeMode)
            kosmos.enableSingleShade()

@@ -331,7 +387,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {

    @Test
    fun edgeDetector_splitShade_usesDefaultEdgeDetector() =
        testScope.runTest {
        kosmos.runTest {
            val shadeMode by collectLastValue(kosmos.shadeMode)
            kosmos.enableSplitShade()

@@ -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)

@@ -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)

+51 −25
Original line number Diff line number Diff line
@@ -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
@@ -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",
            )
        }
    }

    /**
@@ -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. */