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

Commit 6b73ee3e authored by Bryce Lee's avatar Bryce Lee
Browse files

Allow fullscreen swipes for shade and bouncer.

This changelist adds support for fullscreen swipes on the dream
and glanceable hub for bringing down the notification shade and
dragging up the keyguard bouncer.

Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_notInitiatedWhenNotAvailable
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_initiatedWhenAvailable
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_motionCancelResetsTouchState
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_motionUpResetsTouchState
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_initiatedWhenAvailable
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_notInitiatedWhenNotAvailable
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_resetsTouchStateOnCancell
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_resetsTouchStateOnUp
Test: atest CommunalViewModelTest#glanceableTouchAvailable_availableWhenNestedScrollingWithoutConsumption
Fixes: 340177049
Flag: com.android.systemui.hubmode_fullscreen_vertical_swipe
Change-Id: Ib65987789785b0c3b5edca8cb8cfb858f22fc0a7
parent 9131cabb
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -1205,6 +1205,13 @@ flag {
  bug: "352600066"
}

flag {
  name: "hubmode_fullscreen_vertical_swipe"
  namespace: "systemui"
  description: "Enables fullscreen vertical swiping in hub mode to bring up and down the bouncer and shade"
  bug: "340177049"
}

flag {
   namespace: "systemui"
   name: "remove_update_listener_in_qs_icon_view_impl"
+45 −0
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -112,6 +113,11 @@ import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -138,6 +144,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.layout.WindowMetricsCalculator
@@ -204,13 +211,51 @@ fun CommunalHub(
        ScrollOnUpdatedLiveContentEffect(communalContent, gridState)
    }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Begin tracking nested scrolling
                viewModel.onNestedScrolling()
                return super.onPreScroll(available, source)
            }
        }
    }

    Box(
        modifier =
            modifier
                .semantics { testTagsAsResourceId = true }
                .testTag(COMMUNAL_HUB_TEST_TAG)
                .fillMaxSize()
                .nestedScroll(nestedScrollConnection)
                .pointerInput(gridState, contentOffset, contentListState) {
                    awaitPointerEventScope {
                        while (true) {
                            var event = awaitFirstDown(requireUnconsumed = false)
                            // Reset touch on first event.
                            viewModel.onResetTouchState()

                            // Process down event in case it's consumed immediately
                            if (event.isConsumed) {
                                viewModel.onHubTouchConsumed()
                            }

                            do {
                                var event = awaitPointerEvent()
                                for (change in event.changes) {
                                    if (change.isConsumed) {
                                        // Signal touch consumption on any consumed event.
                                        viewModel.onHubTouchConsumed()
                                    }
                                }
                            } while (
                                !event.changes.fastAll {
                                    it.changedToUp() || it.changedToUpIgnoreConsumed()
                                }
                            )
                        }
                    }

                    // If not in edit mode, don't allow selecting items.
                    if (!viewModel.isEditMode) return@pointerInput
                    observeTaps { offset ->
+246 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.ambient.touch;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.animation.ValueAnimator;
import android.content.pm.UserInfo;
import android.graphics.Rect;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.internal.logging.UiEventLogger;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.ambient.touch.scrim.ScrimController;
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.FakeUserTracker;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.wm.shell.animation.FlingAnimationUtils;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.util.Collections;
import java.util.Optional;

@SmallTest
@RunWith(AndroidJUnit4.class)
@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
@DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN)
public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
    private KosmosJavaAdapter mKosmos;

    @Mock
    CentralSurfaces mCentralSurfaces;

    @Mock
    ScrimManager mScrimManager;

    @Mock
    ScrimController mScrimController;

    @Mock
    NotificationShadeWindowController mNotificationShadeWindowController;

    @Mock
    FlingAnimationUtils mFlingAnimationUtils;

    @Mock
    FlingAnimationUtils mFlingAnimationUtilsClosing;

    @Mock
    TouchHandler.TouchSession mTouchSession;

    BouncerSwipeTouchHandler mTouchHandler;

    @Mock
    BouncerSwipeTouchHandler.ValueAnimatorCreator mValueAnimatorCreator;

    @Mock
    ValueAnimator mValueAnimator;

    @Mock
    BouncerSwipeTouchHandler.VelocityTrackerFactory mVelocityTrackerFactory;

    @Mock
    VelocityTracker mVelocityTracker;

    @Mock
    UiEventLogger mUiEventLogger;

    @Mock
    LockPatternUtils mLockPatternUtils;

    @Mock
    ActivityStarter mActivityStarter;

    @Mock
    CommunalViewModel mCommunalViewModel;

    FakeUserTracker mUserTracker;

    private static final float TOUCH_REGION = .3f;
    private static final float MIN_BOUNCER_HEIGHT = .05f;

    private static final Rect SCREEN_BOUNDS = new Rect(0, 0, 1024, 100);
    private static final UserInfo CURRENT_USER_INFO = new UserInfo(
            10,
            /* name= */ "user10",
            /* flags= */ 0
    );

    @Before
    public void setup() {
        mKosmos = new KosmosJavaAdapter(this);
        MockitoAnnotations.initMocks(this);
        mUserTracker = new FakeUserTracker();
        mTouchHandler = new BouncerSwipeTouchHandler(
                mKosmos.getTestScope(),
                mScrimManager,
                Optional.of(mCentralSurfaces),
                mNotificationShadeWindowController,
                mValueAnimatorCreator,
                mVelocityTrackerFactory,
                mLockPatternUtils,
                mUserTracker,
                mCommunalViewModel,
                mFlingAnimationUtils,
                mFlingAnimationUtilsClosing,
                TOUCH_REGION,
                MIN_BOUNCER_HEIGHT,
                mUiEventLogger,
                mActivityStarter);

        when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
        when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
        when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
        when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE);
        when(mTouchSession.getBounds()).thenReturn(SCREEN_BOUNDS);
        when(mLockPatternUtils.isSecure(CURRENT_USER_INFO.id)).thenReturn(true);

        mUserTracker.set(Collections.singletonList(CURRENT_USER_INFO), 0);
    }

    /**
     * Ensures expansion does not happen for full vertical swipes when touch is not available.
     */
    @Test
    public void testFullSwipe_notInitiatedWhenNotAvailable() {
        mTouchHandler.onGlanceableTouchAvailable(false);
        mTouchHandler.onSessionStart(mTouchSession);
        ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
                ArgumentCaptor.forClass(OnGestureListener.class);
        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());

        // A touch within range at the bottom of the screen should trigger listening
        assertThat(gestureListenerCaptor.getValue()
                .onScroll(Mockito.mock(MotionEvent.class),
                        Mockito.mock(MotionEvent.class),
                        1,
                        2)).isFalse();
    }

    /**
     * Ensures expansion only happens for full vertical swipes when touch is available.
     */
    @Test
    public void testFullSwipe_initiatedWhenAvailable() {
        mTouchHandler.onGlanceableTouchAvailable(true);
        mTouchHandler.onSessionStart(mTouchSession);
        ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
                ArgumentCaptor.forClass(OnGestureListener.class);
        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());

        // A touch within range at the bottom of the screen should trigger listening
        assertThat(gestureListenerCaptor.getValue()
                .onScroll(Mockito.mock(MotionEvent.class),
                        Mockito.mock(MotionEvent.class),
                        1,
                        2)).isTrue();
    }

    @Test
    public void testFullSwipe_motionUpResetsTouchState() {
        mTouchHandler.onGlanceableTouchAvailable(true);
        mTouchHandler.onSessionStart(mTouchSession);
        ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
                ArgumentCaptor.forClass(OnGestureListener.class);
        ArgumentCaptor<InputChannelCompat.InputEventListener> inputListenerCaptor =
                ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
        verify(mTouchSession).registerInputListener(inputListenerCaptor.capture());

        // A touch within range at the bottom of the screen should trigger listening
        assertThat(gestureListenerCaptor.getValue()
                .onScroll(Mockito.mock(MotionEvent.class),
                        Mockito.mock(MotionEvent.class),
                        1,
                        2)).isTrue();

        MotionEvent upEvent = Mockito.mock(MotionEvent.class);
        when(upEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
        inputListenerCaptor.getValue().onInputEvent(upEvent);
        verify(mCommunalViewModel).onResetTouchState();
    }

    @Test
    public void testFullSwipe_motionCancelResetsTouchState() {
        mTouchHandler.onGlanceableTouchAvailable(true);
        mTouchHandler.onSessionStart(mTouchSession);
        ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
                ArgumentCaptor.forClass(OnGestureListener.class);
        ArgumentCaptor<InputChannelCompat.InputEventListener> inputListenerCaptor =
                ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
        verify(mTouchSession).registerInputListener(inputListenerCaptor.capture());

        // A touch within range at the bottom of the screen should trigger listening
        assertThat(gestureListenerCaptor.getValue()
                .onScroll(Mockito.mock(MotionEvent.class),
                        Mockito.mock(MotionEvent.class),
                        1,
                        2)).isTrue();

        MotionEvent upEvent = Mockito.mock(MotionEvent.class);
        when(upEvent.getAction()).thenReturn(MotionEvent.ACTION_CANCEL);
        inputListenerCaptor.getValue().onInputEvent(upEvent);
        verify(mCommunalViewModel).onResetTouchState();
    }
}
+10 −1
Original line number Diff line number Diff line
@@ -50,6 +50,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.ambient.touch.scrim.ScrimController;
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.FakeUserTracker;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
@@ -72,7 +74,9 @@ import java.util.Optional;

@SmallTest
@RunWith(AndroidJUnit4.class)
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
    private KosmosJavaAdapter mKosmos;
    @Mock
    CentralSurfaces mCentralSurfaces;

@@ -120,6 +124,9 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
    @Mock
    Region mRegion;

    @Mock
    CommunalViewModel mCommunalViewModel;

    @Captor
    ArgumentCaptor<Rect> mRectCaptor;

@@ -139,9 +146,11 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {

    @Before
    public void setup() {
        mKosmos = new KosmosJavaAdapter(this);
        MockitoAnnotations.initMocks(this);
        mUserTracker = new FakeUserTracker();
        mTouchHandler = new BouncerSwipeTouchHandler(
                mKosmos.getTestScope(),
                mScrimManager,
                Optional.of(mCentralSurfaces),
                mNotificationShadeWindowController,
@@ -149,6 +158,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
                mVelocityTrackerFactory,
                mLockPatternUtils,
                mUserTracker,
                mCommunalViewModel,
                mFlingAnimationUtils,
                mFlingAnimationUtilsClosing,
                TOUCH_REGION,
@@ -201,7 +211,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
                        2)).isTrue();
    }


    /**
     * Ensures expansion only happens when touch down happens in valid part of the screen.
     */
+67 −3
Original line number Diff line number Diff line
@@ -26,8 +26,10 @@ import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.ambient.touch.TouchHandler.TouchSession
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.system.InputChannelCompat
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -50,11 +52,11 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class ShadeTouchHandlerTest : SysuiTestCase() {
    private var kosmos = testKosmos()

    private var mCentralSurfaces = mock<CentralSurfaces>()
    private var mShadeViewController = mock<ShadeViewController>()
    private var mDreamManager = mock<DreamManager>()
    private var mTouchSession = mock<TouchSession>()
    private var communalViewModel = mock<CommunalViewModel>()

    private lateinit var mTouchHandler: ShadeTouchHandler

@@ -65,9 +67,11 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
    fun setup() {
        mTouchHandler =
            ShadeTouchHandler(
                kosmos.testScope,
                Optional.of(mCentralSurfaces),
                mShadeViewController,
                mDreamManager,
                communalViewModel,
                kosmos.communalSettingsInteractor,
                TOUCH_HEIGHT
            )
@@ -75,6 +79,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {

    // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
    @Test
    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testSwipeDown_captured() {
        val captured = swipe(Direction.DOWN)
        Truth.assertThat(captured).isTrue()
@@ -82,6 +87,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {

    // Verifies that a swipe in the upward direction is not captured.
    @Test
    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testSwipeUp_notCaptured() {
        val captured = swipe(Direction.UP)

@@ -91,6 +97,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {

    // Verifies that a swipe down forwards captured touches to central surfaces for handling.
    @Test
    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
    fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
        kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -103,7 +110,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {

    // Verifies that a swipe down forwards captured touches to the shade view for handling.
    @Test
    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
    @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testSwipeDown_communalDisabled_sentToShadeView() {
        swipe(Direction.DOWN)

@@ -114,6 +121,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
    // Verifies that a swipe down while dreaming forwards captured touches to the shade view for
    // handling.
    @Test
    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testSwipeDown_dreaming_sentToShadeView() {
        whenever(mDreamManager.isDreaming).thenReturn(true)
        swipe(Direction.DOWN)
@@ -124,6 +132,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {

    // Verifies that a swipe up is not forwarded to central surfaces.
    @Test
    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
    fun testSwipeUp_communalEnabled_touchesNotSent() {
        kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -137,7 +146,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {

    // Verifies that a swipe up is not forwarded to the shade view.
    @Test
    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
    @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testSwipeUp_communalDisabled_touchesNotSent() {
        swipe(Direction.UP)

@@ -147,6 +156,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
    }

    @Test
    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testCancelMotionEvent_popsTouchSession() {
        swipe(Direction.DOWN)
        val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
@@ -154,6 +164,60 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
        verify(mTouchSession).pop()
    }

    @Test
    @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testFullVerticalSwipe_initiatedWhenAvailable() {
        // Indicate touches are available
        mTouchHandler.onGlanceableTouchAvailable(true)

        // Verify swipe is handled
        val captured = swipe(Direction.DOWN)
        Truth.assertThat(captured).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testFullVerticalSwipe_notInitiatedWhenNotAvailable() {
        // Indicate touches aren't available
        mTouchHandler.onGlanceableTouchAvailable(false)

        // Verify swipe is not handled
        val captured = swipe(Direction.DOWN)
        Truth.assertThat(captured).isFalse()
    }

    @Test
    @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testFullVerticalSwipe_resetsTouchStateOnUp() {
        // Indicate touches are available
        mTouchHandler.onGlanceableTouchAvailable(true)

        // Verify swipe is handled
        swipe(Direction.DOWN)

        val upEvent: MotionEvent = mock()
        whenever(upEvent.action).thenReturn(MotionEvent.ACTION_UP)
        mInputListenerCaptor.lastValue.onInputEvent(upEvent)

        verify(communalViewModel).onResetTouchState()
    }

    @Test
    @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
    fun testFullVerticalSwipe_resetsTouchStateOnCancel() {
        // Indicate touches are available
        mTouchHandler.onGlanceableTouchAvailable(true)

        // Verify swipe is handled
        swipe(Direction.DOWN)

        val upEvent: MotionEvent = mock()
        whenever(upEvent.action).thenReturn(MotionEvent.ACTION_CANCEL)
        mInputListenerCaptor.lastValue.onInputEvent(upEvent)

        verify(communalViewModel).onResetTouchState()
    }

    /**
     * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
     * touch handler's gesture listener.
Loading