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

Commit 69a87b54 authored by William Xiao's avatar William Xiao
Browse files

Capture touches during hub transition, not only when fully open

Our current touch handling logic only intercepts touches when the
scene is on communal, but this means that if a user swipes the hub open
then interacts with other parts of the screen quickly, touches go to
whatever is underneath the hub. This can lead to strange interactions
where you might accidentally dismiss the dream while the hub open
animation is still running.

This CL changes our logic to intercept touches during any point of the
hub transition, in addition to when idle on the hub.

Bug: 338072097
Test: atest GlanceableHubContainerControllerTest
      manually tested by swiping the hub open and closed quickly
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: Ic4b0695c6463fd31aa32a40809d33bbbe4610b40
parent 479067ae
Loading
Loading
Loading
Loading
+11 −57
Original line number Diff line number Diff line
@@ -70,7 +70,6 @@ constructor(
    private val communalInteractor: CommunalInteractor,
    private val communalViewModel: CommunalViewModel,
    private val dialogFactory: SystemUIDialogFactory,
    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
    private val keyguardInteractor: KeyguardInteractor,
    private val shadeInteractor: ShadeInteractor,
    private val powerManager: PowerManager,
@@ -103,12 +102,9 @@ constructor(
    private var rightEdgeSwipeRegionWidth: Int = 0

    /**
     * True if we are currently tracking a gesture for opening the hub that started in the edge
     * swipe region.
     * True if we are currently tracking a touch intercepted by the hub, either because the hub is
     * open or being opened.
     */
    private var isTrackingOpenGesture = false

    /** True if we are currently tracking a touch on the hub while it's open. */
    private var isTrackingHubTouch = false

    /**
@@ -154,7 +150,7 @@ constructor(
    /**
     * Creates the container view containing the glanceable hub UI.
     *
     * @throws RuntimeException if [isEnabled] is false or the view is already initialized
     * @throws RuntimeException if the view is already initialized
     */
    fun initView(
        context: Context,
@@ -229,7 +225,7 @@ constructor(

        // BouncerSwipeTouchHandler has a larger gesture area than we want, set an exclusion area so
        // the gesture area doesn't overlap with widgets.
        // TODO(b/323035776): adjust gesture areaa for portrait mode
        // TODO(b/323035776): adjust gesture area for portrait mode
        containerView.repeatWhenAttached {
            // Run when the touch handling lifecycle is RESUMED, meaning the hub is visible and not
            // occluded.
@@ -263,7 +259,7 @@ constructor(
        )
        collectFlow(
            containerView,
            communalInteractor.isCommunalShowing,
            communalInteractor.isCommunalVisible,
            {
                hubShowing = it
                updateTouchHandlingState()
@@ -331,67 +327,25 @@ 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

        val hubOccluded = anyBouncerShowing || shadeShowing

        if (isDown && !hubOccluded) {
            // Only intercept down events if the hub isn't occluded by the bouncer or
            // notification shade.
            isTrackingHubTouch = true
        }

        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
            // may return false from dispatchTouchEvent.
            return true
        }

        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 (isDown && !hubOccluded) {
            val x = ev.rawX
            val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
            if (inOpeningSwipeRegion) {
                isTrackingOpenGesture = true
            if (inOpeningSwipeRegion || hubShowing) {
                // Steal touch events when the hub is open, or if the touch started in the opening
                // gesture region.
                isTrackingHubTouch = true
            }
        }

        if (isTrackingOpenGesture) {
        if (isTrackingHubTouch) {
            if (isUp || isCancel) {
                isTrackingOpenGesture = false
                isTrackingHubTouch = false
            }
            dispatchTouchEvent(view, ev)
            // Return true regardless of dispatch result as some touches at the start of a gesture
+29 −6
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
@@ -43,7 +44,6 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
@@ -55,6 +55,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -117,7 +118,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                    communalInteractor,
                    communalViewModel,
                    dialogFactory,
                    keyguardTransitionInteractor,
                    keyguardInteractor,
                    shadeInteractor,
                    powerManager,
@@ -160,7 +160,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                        communalInteractor,
                        communalViewModel,
                        dialogFactory,
                        keyguardTransitionInteractor,
                        keyguardInteractor,
                        shadeInteractor,
                        powerManager,
@@ -205,6 +204,32 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
            }
        }

    @Test
    fun onTouchEvent_communalTransitioning_interceptsTouches() =
        with(kosmos) {
            testScope.runTest {
                // Communal is opening.
                communalRepository.setTransitionState(
                    flowOf(
                        ObservableTransitionState.Transition(
                            fromScene = CommunalScenes.Blank,
                            toScene = CommunalScenes.Communal,
                            currentScene = flowOf(CommunalScenes.Blank),
                            progress = flowOf(0.5f),
                            isInitiatedByUserInput = true,
                            isUserInputOngoing = flowOf(true)
                        )
                    )
                )
                testableLooper.processAllMessages()

                // Touch events are intercepted.
                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
                // User activity sent to PowerManager.
                verify(powerManager).userActivity(any(), any(), any())
            }
        }

    @Test
    fun onTouchEvent_communalOpen_interceptsTouches() =
        with(kosmos) {
@@ -212,7 +237,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                // Communal is open.
                goToScene(CommunalScenes.Communal)

                // Touch events are intercepted outside of any gesture areas.
                // Touch events are intercepted.
                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
                // User activity sent to PowerManager.
                verify(powerManager).userActivity(any(), any(), any())
@@ -279,7 +304,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                    communalInteractor,
                    communalViewModel,
                    dialogFactory,
                    keyguardTransitionInteractor,
                    keyguardInteractor,
                    shadeInteractor,
                    powerManager,
@@ -299,7 +323,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() {
                    communalInteractor,
                    communalViewModel,
                    dialogFactory,
                    keyguardTransitionInteractor,
                    keyguardInteractor,
                    shadeInteractor,
                    powerManager,
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ class FakeCommunalRepository(
) : CommunalRepository {
    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
        this.currentScene.value = toScene
        this._transitionState.value = flowOf(ObservableTransitionState.Idle(toScene))
    }

    private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)