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

Commit 0d8256dd authored by William Xiao's avatar William Xiao Committed by Android (Google) Code Review
Browse files

Merge "Fix hub touch handling interfering with bouncer swipe on dream" into main

parents 7a695707 96e8b051
Loading
Loading
Loading
Loading
+65 −24
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.communal.dagger.Communal
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.ui.compose.CommunalContainer
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.res.R
@@ -52,6 +53,7 @@ constructor(
    private val communalViewModel: CommunalViewModel,
    private val dialogFactory: SystemUIDialogFactory,
    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
    private val keyguardInteractor: KeyguardInteractor,
    private val shadeInteractor: ShadeInteractor,
    private val powerManager: PowerManager,
    @Communal private val dataSourceDelegator: SceneDataSourceDelegator,
@@ -89,6 +91,9 @@ constructor(
    /** True if we are currently tracking a touch on the hub while it's open. */
    private var isTrackingHubTouch = false

    /** True if we are tracking a top or bottom swipe gesture while the hub is open. */
    private var isTrackingHubGesture = false

    /**
     * True if the hub UI is fully open, meaning it should receive touch input.
     *
@@ -111,6 +116,16 @@ constructor(
     */
    private var shadeShowing = false

    /**
     * True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes
     * and just let the dream overlay's touch handling deal with them.
     *
     * Tracks [KeyguardInteractor.isDreaming].
     *
     * TODO(b/328838259): figure out a proper solution for touch handling above the lock screen too
     */
    private var isDreaming = false

    /** Returns a flow that tracks whether communal hub is available. */
    fun communalAvailable(): Flow<Boolean> = communalInteractor.isCommunalAvailable

@@ -166,6 +181,7 @@ constructor(
        )
        collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it })
        collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it })
        collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })

        communalContainerView = containerView

@@ -194,61 +210,86 @@ constructor(
    }

    private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
        // If the hub is fully visible, send all touch events to it, other than top and bottom edge
        // swipes.
        return if (hubShowing) {
            handleHubOpenTouch(view, ev)
        } else {
            handleHubClosedTouch(view, ev)
        }
    }

    private fun handleHubOpenTouch(view: View, ev: MotionEvent): Boolean {
        val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
        val isUp = ev.actionMasked == MotionEvent.ACTION_UP
        val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL

        // TODO(b/315207481): also account for opening animations of shade/bouncer and not just
        //  fully showing state
        val hubOccluded = anyBouncerShowing || shadeShowing

        // If the hub is fully visible, send all touch events to it, other than top and bottom edge
        // swipes.
        if (hubShowing && isDown) {
        if (isDown && !hubOccluded) {
            // Only intercept down events if the hub isn't occluded by the bouncer or
            // notification shade.
            val y = ev.rawY
            val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth
            val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth

            if (topSwipe || bottomSwipe) {
                // Don't intercept touches at the top/bottom edge so that swipes can open the
                // notification shade and bouncer.
                return false
            }

            if (!hubOccluded) {
                isTrackingHubGesture = true
            } else {
                isTrackingHubTouch = true
                dispatchTouchEvent(view, ev)
                // Return true regardless of dispatch result as some touches at the start of a
                // gesture may return false from dispatchTouchEvent.
                return true
            }
        } else if (isTrackingHubTouch) {
        }

        if (isTrackingHubTouch) {
            // Tracking a touch on the hub UI itself.
            if (isUp || isCancel) {
                isTrackingHubTouch = false
            }
            dispatchTouchEvent(view, ev)
            // Return true regardless of dispatch result as some touches at the start of a gesture
            // Return true regardless of dispatch result as some touches at the start of a
            // gesture
            // may return false from dispatchTouchEvent.
            return true
        } else if (isTrackingHubGesture) {
            // Tracking a top or bottom swipe on the hub UI.
            if (isUp || isCancel) {
                isTrackingHubGesture = false
            }

            // If we're dreaming, intercept touches so the hub UI doesn't receive them, but
            // don't do anything so that the dream's touch handling takes care of opening
            // the bouncer or shade.
            //
            // If we're not dreaming, we don't intercept touches at the top/bottom edge so that
            // swipes can open the notification shade and bouncer.
            return isDreaming
        }

        return false
    }

    private fun handleHubClosedTouch(view: View, ev: MotionEvent): Boolean {
        val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
        val isUp = ev.actionMasked == MotionEvent.ACTION_UP
        val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL

        val hubOccluded = anyBouncerShowing || shadeShowing

        if (rightEdgeSwipeRegionWidth == 0) {
            // If the edge region width has not been read yet for whatever reason, don't bother
            // intercepting touches to open the hub.
            return false
        }

        if (!isTrackingOpenGesture && isDown) {
        if (isDown && !hubOccluded) {
            val x = ev.rawX
            val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
            if (inOpeningSwipeRegion && !hubOccluded) {
            if (inOpeningSwipeRegion) {
                isTrackingOpenGesture = true
                dispatchTouchEvent(view, ev)
                // Return true regardless of dispatch result as some touches at the start of a
                // gesture may return false from dispatchTouchEvent.
                return true
            }
        } else if (isTrackingOpenGesture) {
        }

        if (isTrackingOpenGesture) {
            if (isUp || isCancel) {
                isTrackingOpenGesture = false
            }
+173 −99
Original line number Diff line number Diff line
@@ -36,22 +36,30 @@ import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
@@ -75,10 +83,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
        }

    @Mock private lateinit var communalViewModel: CommunalViewModel
    @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
    @Mock private lateinit var shadeInteractor: ShadeInteractor
    @Mock private lateinit var powerManager: PowerManager
    @Mock private lateinit var dialogFactory: SystemUIDialogFactory
    private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
    private lateinit var shadeInteractor: ShadeInteractor
    private lateinit var keyguardInteractor: KeyguardInteractor

    private lateinit var parentView: FrameLayout
    private lateinit var containerView: View
@@ -88,15 +97,15 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
    private lateinit var communalRepository: FakeCommunalRepository
    private lateinit var underTest: GlanceableHubContainerController

    private val bouncerShowingFlow = MutableStateFlow(false)
    private val shadeShowingFlow = MutableStateFlow(false)

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        communalInteractor = kosmos.communalInteractor
        communalRepository = kosmos.fakeCommunalRepository
        keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
        keyguardInteractor = kosmos.keyguardInteractor
        shadeInteractor = kosmos.shadeInteractor

        underTest =
            GlanceableHubContainerController(
@@ -104,16 +113,13 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                communalViewModel,
                dialogFactory,
                keyguardTransitionInteractor,
                keyguardInteractor,
                shadeInteractor,
                powerManager,
                kosmos.sceneDataSourceDelegator,
            )
        testableLooper = TestableLooper.get(this)

        whenever(keyguardTransitionInteractor.isFinishedInStateWhere(any()))
            .thenReturn(bouncerShowingFlow)
        whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow)

        overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
        overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
        overrideResource(
@@ -138,13 +144,16 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
    }

    @Test
    fun initView_calledTwice_throwsException() {
    fun initView_calledTwice_throwsException() =
        with(kosmos) {
            testScope.runTest {
                underTest =
                    GlanceableHubContainerController(
                        communalInteractor,
                        communalViewModel,
                        dialogFactory,
                        keyguardTransitionInteractor,
                        keyguardInteractor,
                        shadeInteractor,
                        powerManager,
                        kosmos.sceneDataSourceDelegator,
@@ -156,30 +165,40 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                // Second call throws.
                assertThrows(RuntimeException::class.java) { underTest.initView(context) }
            }
        }

    @Test
    fun onTouchEvent_communalClosed_doesNotIntercept() {
    fun onTouchEvent_communalClosed_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is closed.
                goToScene(CommunalScenes.Blank)

                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_openGesture_interceptsTouches() {
    fun onTouchEvent_openGesture_interceptsTouches() =
        with(kosmos) {
            testScope.runTest {
                // Communal is closed.
                goToScene(CommunalScenes.Blank)

        // Initial touch down is intercepted, and so are touches outside of the region, until an
                // Initial touch down is intercepted, and so are touches outside of the region,
                // until an
                // up event is received.
                assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
                assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_communalOpen_interceptsTouches() {
    fun onTouchEvent_communalOpen_interceptsTouches() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

@@ -188,32 +207,77 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                // User activity sent to PowerManager.
                verify(powerManager).userActivity(any(), any(), any())
            }
        }

    @Test
    fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() {
    fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

        // Touch event in the top swipe reqgion is not intercepted.
                // Touch event in the top swipe region is not intercepted.
                assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() {
    fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

        // Touch event in the bottom swipe reqgion is not intercepted.
                // Touch event in the bottom swipe region is not intercepted.
                assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() {
    fun onTouchEvent_topSwipeWhenDreaming_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

                // Device is dreaming.
                fakeKeyguardRepository.setDreaming(true)
                runCurrent()

                // Touch event in the top swipe region is not intercepted.
                assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_bottomSwipeWhenDreaming_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

                // Device is dreaming.
                fakeKeyguardRepository.setDreaming(true)
                runCurrent()

                // Touch event in the bottom swipe region is not intercepted.
                assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

                // Bouncer is visible.
        bouncerShowingFlow.value = true
                fakeKeyguardTransitionRepository.sendTransitionSteps(
                    KeyguardState.GLANCEABLE_HUB,
                    KeyguardState.PRIMARY_BOUNCER,
                    testScope
                )
                testableLooper.processAllMessages()

                // Touch events are not intercepted.
@@ -221,21 +285,28 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                // User activity is not sent to PowerManager.
                verify(powerManager, times(0)).userActivity(any(), any(), any())
            }
        }

    @Test
    fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() {
    fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

        shadeShowingFlow.value = true
                // Shade shows up.
                fakeShadeRepository.setQsExpansion(1.0f)
                testableLooper.processAllMessages()

                // Touch events are not intercepted.
                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
            }
        }

    @Test
    fun onTouchEvent_containerViewDisposed_doesNotIntercept() {
    fun onTouchEvent_containerViewDisposed_doesNotIntercept() =
        with(kosmos) {
            testScope.runTest {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

@@ -248,6 +319,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                // Touch events are not intercepted.
                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
            }
        }

    private fun initAndAttachContainerView() {
        containerView = View(context)
@@ -259,6 +331,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {

        // Attach the view so that flows start collecting.
        ViewUtils.attachView(parentView)
        // Attaching is async so processAllMessages is required for view.repeatWhenAttached to run.
        testableLooper.processAllMessages()

        // Give the view a fixed size to simplify testing for edge swipes.
        val lp =