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

Commit 92748079 authored by Bryce Lee's avatar Bryce Lee
Browse files

Add Touch Initiation Regions.

This changelist adds touch initiation regions for DreamTouchHandler.
Regions allow handlers to specify where the initial input event must
occur in for a session directed at it to start. In turn, this allows
for an accurate understanding of active sessions at any given time as
the touch manager can preemptively prune non participating sessions.

This changelist also updates the BouncerSwipeTouchHandler to now
specify a region rather than check the first event. A new method has
been added to touch session to query the number of active sessions.

Bug: 214292772
Test: atest DreamOverlayTouchMonitorTest BouncerSwipeTouchHandlerTest
Change-Id: Iaa5fe71d0375c8ee8e74e12eeffdcba3f51187ec
parent caf89ec2
Loading
Loading
Loading
Loading
+26 −34
Original line number Diff line number Diff line
@@ -21,6 +21,9 @@ import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_
import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION;

import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.InputEvent;
@@ -75,6 +78,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
    private final FlingAnimationUtils mFlingAnimationUtils;
    private final FlingAnimationUtils mFlingAnimationUtilsClosing;

    private final DisplayMetrics mDisplayMetrics;

    private Boolean mCapture;

    private TouchSession mTouchSession;
@@ -85,40 +90,9 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {

    private final GestureDetector.OnGestureListener mOnGestureListener =
            new  GestureDetector.SimpleOnGestureListener() {
                boolean mTrack;
                boolean mBouncerPresent;

                @Override
                public boolean onDown(MotionEvent e) {
                    // We only consider gestures that originate from the lower portion of the
                    // screen.
                    final float displayHeight = mStatusBar.getDisplayHeight();

                    mBouncerPresent = mStatusBar.isBouncerShowing();

                    // The target zone is either at the top or bottom of the screen, dependent on
                    // whether the bouncer is present.
                    final float zonePercentage =
                            Math.abs(e.getY() - (mBouncerPresent ? 0 : displayHeight))
                                    / displayHeight;

                    mTrack =  zonePercentage < mBouncerZoneScreenPercentage;

                    // Never capture onDown. While this might lead to some false positive touches
                    // being sent to other windows/layers, this is necessary to make sure the
                    // proper touch event sequence is received by others in the event we do not
                    // consume the sequence here.
                    return false;
                }

                @Override
                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
                        float distanceY) {
                    // Do not handle scroll gestures if not tracking touch events.
                    if (!mTrack) {
                        return false;
                    }

                    if (mCapture == null) {
                        // If the user scrolling favors a vertical direction, begin capturing
                        // scrolls.
@@ -141,8 +115,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
                    // (0).
                    final float screenTravelPercentage =
                            Math.abs((e1.getY() - e2.getY()) / mStatusBar.getDisplayHeight());
                    setPanelExpansion(
                            mBouncerPresent ? screenTravelPercentage : 1 - screenTravelPercentage);
                    setPanelExpansion(mStatusBar.isBouncerShowing()
                            ? screenTravelPercentage : 1 - screenTravelPercentage);

                    return true;
                }
@@ -155,6 +129,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {

    @Inject
    public BouncerSwipeTouchHandler(
            DisplayMetrics displayMetrics,
            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
            StatusBar statusBar,
            NotificationShadeWindowController notificationShadeWindowController,
@@ -165,6 +140,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
            @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
                    FlingAnimationUtils flingAnimationUtilsClosing,
            @Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage) {
        mDisplayMetrics = displayMetrics;
        mStatusBar = statusBar;
        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
        mNotificationShadeWindowController = notificationShadeWindowController;
@@ -175,6 +151,21 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
        mVelocityTrackerFactory = velocityTrackerFactory;
    }

    @Override
    public void getTouchInitiationRegion(Region region) {
        if (mStatusBar.isBouncerShowing()) {
            region.op(new Rect(0, 0, mDisplayMetrics.widthPixels,
                    Math.round(mDisplayMetrics.heightPixels * mBouncerZoneScreenPercentage)),
                    Region.Op.UNION);
        } else {
            region.op(new Rect(0,
                    Math.round(mDisplayMetrics.heightPixels * (1 - mBouncerZoneScreenPercentage)),
                    mDisplayMetrics.widthPixels,
                    mDisplayMetrics.heightPixels),
                    Region.Op.UNION);
        }
    }

    @Override
    public void onSessionStart(TouchSession session) {
        mVelocityTracker = mVelocityTrackerFactory.obtain();
@@ -202,7 +193,9 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
        final MotionEvent motionEvent = (MotionEvent) event;

        switch(motionEvent.getAction()) {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mTouchSession.pop();
                // If we are not capturing any input, there is no need to consider animating to
                // finish transition.
                if (mCapture == null || !mCapture) {
@@ -226,7 +219,6 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
                if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
                    mStatusBarKeyguardViewManager.reset(false);
                }
                mTouchSession.pop();
                break;
            default:
                mVelocityTracker.addMovement(motionEvent);
+39 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.dreams.touch;

import android.graphics.Region;
import android.view.GestureDetector;
import android.view.InputEvent;
import android.view.MotionEvent;
@@ -34,6 +35,7 @@ import com.android.systemui.shared.system.InputChannelCompat;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -100,6 +102,10 @@ public class DreamOverlayTouchMonitor {
        });
    }

    private int getSessionCount() {
        return mActiveTouchSessions.size();
    }

    /**
     * {@link TouchSessionImpl} implements {@link DreamTouchHandler.TouchSession} for
     * {@link DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners
@@ -146,6 +152,11 @@ public class DreamOverlayTouchMonitor {
            return mTouchMonitor.pop(this);
        }

        @Override
        public int getActiveSessionCount() {
            return mTouchMonitor.getSessionCount();
        }

        /**
         * Returns the active listeners to receive touch events.
         */
@@ -229,12 +240,39 @@ public class DreamOverlayTouchMonitor {
        public void onInputEvent(InputEvent ev) {
            // No Active sessions are receiving touches. Create sessions for each listener
            if (mActiveTouchSessions.isEmpty()) {
                final HashMap<DreamTouchHandler, DreamTouchHandler.TouchSession> sessionMap =
                        new HashMap<>();

                for (DreamTouchHandler handler : mHandlers) {
                    final Region initiationRegion = Region.obtain();
                    handler.getTouchInitiationRegion(initiationRegion);

                    if (!initiationRegion.isEmpty()) {
                        // Initiation regions require a motion event to determine pointer location
                        // within the region.
                        if (!(ev instanceof MotionEvent)) {
                            continue;
                        }

                        final MotionEvent motionEvent = (MotionEvent) ev;

                        // If the touch event is outside the region, then ignore.
                        if (!initiationRegion.contains(Math.round(motionEvent.getX()),
                                Math.round(motionEvent.getY()))) {
                            continue;
                        }
                    }

                    final TouchSessionImpl sessionStack =
                            new TouchSessionImpl(DreamOverlayTouchMonitor.this, null);
                    mActiveTouchSessions.add(sessionStack);
                    handler.onSessionStart(sessionStack);
                    sessionMap.put(handler, sessionStack);
                }

                // Informing handlers of new sessions is delayed until we have all created so the
                // final session is correct.
                sessionMap.forEach((dreamTouchHandler, touchSession)
                        -> dreamTouchHandler.onSessionStart(touchSession));
            }

            // Find active sessions and invoke on InputEvent.
+14 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.dreams.touch;

import android.graphics.Region;
import android.view.GestureDetector;

import com.android.systemui.shared.system.InputChannelCompat;
@@ -71,6 +72,19 @@ public interface DreamTouchHandler {
         * if the popped {@link TouchSession} was the initial session or has already been popped.
         */
        ListenableFuture<TouchSession> pop();

        /**
         * Returns the number of currently active sessions.
         */
        int getActiveSessionCount();
    }

    /**
     * Returns the region the touch handler is interested in. By default, no region is specified,
     * indicating the entire screen should be considered.
     * @param region A {@link Region} that is passed in to the target entry touch region.
     */
    default void getTouchInitiationRegion(Region region) {
    }

    /**
+27 −41
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.systemui.dreams.touch;

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


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -27,12 +26,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;

import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
@@ -51,8 +52,6 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.util.Random;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@@ -89,13 +88,20 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
    @Mock
    VelocityTracker mVelocityTracker;

    final DisplayMetrics mDisplayMetrics = new DisplayMetrics();

    private static final float TOUCH_REGION = .3f;
    private static final float SCREEN_HEIGHT_PX = 100;
    private static final int SCREEN_WIDTH_PX = 1024;
    private static final int SCREEN_HEIGHT_PX = 100;

    @Before
    public void setup() {
        mDisplayMetrics.widthPixels = SCREEN_WIDTH_PX;
        mDisplayMetrics.heightPixels = SCREEN_HEIGHT_PX;

        MockitoAnnotations.initMocks(this);
        mTouchHandler = new BouncerSwipeTouchHandler(
                mDisplayMetrics,
                mStatusBarKeyguardViewManager,
                mStatusBar,
                mNotificationShadeWindowController,
@@ -104,23 +110,29 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
                mFlingAnimationUtils,
                mFlingAnimationUtilsClosing,
                TOUCH_REGION);
        when(mStatusBar.getDisplayHeight()).thenReturn(SCREEN_HEIGHT_PX);

        when(mStatusBar.getDisplayHeight()).thenReturn((float) SCREEN_HEIGHT_PX);
        when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
        when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
    }

    private static void beginValidSwipe(GestureDetector.OnGestureListener listener) {
        listener.onDown(MotionEvent.obtain(0, 0,
                MotionEvent.ACTION_DOWN, 0,
                SCREEN_HEIGHT_PX - (.5f * TOUCH_REGION * SCREEN_HEIGHT_PX), 0));
    }

    /**
     * Ensures expansion only happens when touch down happens in valid part of the screen.
     */
    @FlakyTest
    @Test
    public void testSessionStart() {
        final Region region = Region.obtain();
        mTouchHandler.getTouchInitiationRegion(region);

        final Rect bounds = region.getBounds();

        final Rect expected = new Rect();

        expected.set(0, Math.round(SCREEN_HEIGHT_PX * (1 - TOUCH_REGION)), SCREEN_WIDTH_PX,
                SCREEN_HEIGHT_PX);

        assertThat(bounds).isEqualTo(expected);

        mTouchHandler.onSessionStart(mTouchSession);
        verify(mNotificationShadeWindowController).setForcePluginOpen(eq(true), any());
        ArgumentCaptor<InputChannelCompat.InputEventListener> eventListenerCaptor =
@@ -130,34 +142,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
        verify(mTouchSession).registerInputListener(eventListenerCaptor.capture());

        final Random random = new Random(System.currentTimeMillis());

        // If an initial touch down meeting criteria has been met, scroll behavior should be
        // ignored.
        assertThat(gestureListenerCaptor.getValue()
                .onScroll(Mockito.mock(MotionEvent.class),
                        Mockito.mock(MotionEvent.class),
                        random.nextFloat(),
                        random.nextFloat())).isFalse();

        // A touch at the top of the screen should also not trigger listening.
        final MotionEvent touchDownEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN,
                0, 0, 0);

        gestureListenerCaptor.getValue().onDown(touchDownEvent);
        assertThat(gestureListenerCaptor.getValue()
                .onScroll(Mockito.mock(MotionEvent.class),
                        Mockito.mock(MotionEvent.class),
                        random.nextFloat(),
                        random.nextFloat())).isFalse();

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

    /**
@@ -170,8 +160,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());

        beginValidSwipe(gestureListenerCaptor.getValue());

        final float scrollAmount = .3f;
        final float distanceY = SCREEN_HEIGHT_PX * scrollAmount;

@@ -203,8 +191,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {

        when(mVelocityTracker.getYVelocity()).thenReturn(velocityY);

        beginValidSwipe(gestureListenerCaptor.getValue());

        final float distanceY = SCREEN_HEIGHT_PX * position;

        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+78 −0
Original line number Diff line number Diff line
@@ -21,10 +21,13 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
import android.view.GestureDetector;
@@ -136,6 +139,81 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase {
        }
    }

    @Test
    public void testEntryTouchZone() {
        final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
        final Rect touchArea = new Rect(4, 4, 8 , 8);

        doAnswer(invocation -> {
            final Region region = (Region) invocation.getArguments()[0];
            region.set(touchArea);
            return null;
        }).when(touchHandler).getTouchInitiationRegion(any());

        final Environment environment = new Environment(Stream.of(touchHandler)
                .collect(Collectors.toCollection(HashSet::new)));

        // Ensure touch outside specified region is not delivered.
        final MotionEvent initialEvent = Mockito.mock(MotionEvent.class);
        when(initialEvent.getX()).thenReturn(0.0f);
        when(initialEvent.getY()).thenReturn(1.0f);
        environment.publishInputEvent(initialEvent);
        verify(touchHandler, never()).onSessionStart(any());

        // Make sure touch inside region causes session start.
        when(initialEvent.getX()).thenReturn(5.0f);
        when(initialEvent.getY()).thenReturn(5.0f);
        environment.publishInputEvent(initialEvent);
        verify(touchHandler).onSessionStart(any());
    }

    @Test
    public void testSessionCount() {
        final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
        final Rect touchArea = new Rect(4, 4, 8 , 8);

        final DreamTouchHandler unzonedTouchHandler = Mockito.mock(DreamTouchHandler.class);
        doAnswer(invocation -> {
            final Region region = (Region) invocation.getArguments()[0];
            region.set(touchArea);
            return null;
        }).when(touchHandler).getTouchInitiationRegion(any());

        final Environment environment = new Environment(Stream.of(touchHandler, unzonedTouchHandler)
                .collect(Collectors.toCollection(HashSet::new)));

        // Ensure touch outside specified region is delivered to unzoned touch handler.
        final MotionEvent initialEvent = Mockito.mock(MotionEvent.class);
        when(initialEvent.getX()).thenReturn(0.0f);
        when(initialEvent.getY()).thenReturn(1.0f);
        environment.publishInputEvent(initialEvent);

        ArgumentCaptor<DreamTouchHandler.TouchSession> touchSessionCaptor = ArgumentCaptor.forClass(
                DreamTouchHandler.TouchSession.class);

        // Make sure only one active session.
        {
            verify(unzonedTouchHandler).onSessionStart(touchSessionCaptor.capture());
            final DreamTouchHandler.TouchSession touchSession = touchSessionCaptor.getValue();
            assertThat(touchSession.getActiveSessionCount()).isEqualTo(1);
            touchSession.pop();
            environment.executeAll();
        }

        // Make sure touch inside the touch region.
        when(initialEvent.getX()).thenReturn(5.0f);
        when(initialEvent.getY()).thenReturn(5.0f);
        environment.publishInputEvent(initialEvent);

        // Make sure there are two active sessions.
        {
            verify(touchHandler).onSessionStart(touchSessionCaptor.capture());
            final DreamTouchHandler.TouchSession touchSession = touchSessionCaptor.getValue();
            assertThat(touchSession.getActiveSessionCount()).isEqualTo(2);
            touchSession.pop();
        }
    }

    @Test
    public void testInputEventPropagation() {
        final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);