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

Commit 20fab06b authored by Winson Chung's avatar Winson Chung Committed by android-build-merger
Browse files

Merge "Double tap to expand PiP." into oc-mr1-dev

am: 2a6f25b9

Change-Id: I75a3dd9befd6bb1bb2aefb11ea1239ea22072e0f
parents 4a25d72e 2a6f25b9
Loading
Loading
Loading
Loading
+30 −5
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
@@ -119,6 +120,7 @@ public class PipMenuActivity extends Activity {
                }
            };

    private PipTouchState mTouchState;
    private PointF mDownPosition = new PointF();
    private PointF mDownDelta = new PointF();
    private ViewConfiguration mViewConfig;
@@ -175,6 +177,13 @@ public class PipMenuActivity extends Activity {
        // Set the flags to allow us to watch for outside touches and also hide the menu and start
        // manipulating the PIP in the same touch gesture
        mViewConfig = ViewConfiguration.get(this);
        mTouchState = new PipTouchState(mViewConfig, mHandler, () -> {
            if (mMenuState == MENU_STATE_CLOSE) {
                showPipMenu();
            } else {
                expandPip();
            }
        });
        getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | LayoutParams.FLAG_SLIPPERY);

        super.onCreate(savedInstanceState);
@@ -186,12 +195,28 @@ public class PipMenuActivity extends Activity {
        mViewRoot.setBackground(mBackgroundDrawable);
        mMenuContainer = findViewById(R.id.menu_container);
        mMenuContainer.setAlpha(0);
        mMenuContainer.setOnClickListener((v) -> {
        mMenuContainer.setOnTouchListener((v, event) -> {
            mTouchState.onTouchEvent(event);
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    if (mTouchState.isDoubleTap() || mMenuState == MENU_STATE_FULL) {
                        // Expand to fullscreen if this is a double tap or we are already expanded
                        expandPip();
                    } else if (!mTouchState.isWaitingForDoubleTap()) {
                        // User has stalled long enough for this not to be a drag or a double tap,
                        // just expand the menu if necessary
                        if (mMenuState == MENU_STATE_CLOSE) {
                            showPipMenu();
                        }
                    } else {
                expandPip();
                        // Next touch event _may_ be the second tap for the double-tap, schedule a
                        // fallback runnable to trigger the menu if no touch event occurs before the
                        // next tap
                        mTouchState.scheduleDoubleTapTimeoutCallback();
                    }
                    break;
            }
            return true;
        });
        mDismissButton = findViewById(R.id.dismiss);
        mDismissButton.setAlpha(0);
+4 −0
Original line number Diff line number Diff line
@@ -211,6 +211,10 @@ public class PipMenuActivityController {
        EventBus.getDefault().register(this);
    }

    public boolean isMenuActivityVisible() {
        return mToActivityMessenger != null;
    }

    public void onActivityPinned() {
        if (mMenuState == MENU_STATE_NONE) {
            // If the menu is not visible, then re-register the input consumer if it is not already
+18 −4
Original line number Diff line number Diff line
@@ -187,13 +187,15 @@ public class PipTouchHandler {
        mMenuController.addListener(mMenuListener);
        mDismissViewController = new PipDismissViewController(context);
        mSnapAlgorithm = new PipSnapAlgorithm(mContext);
        mTouchState = new PipTouchState(mViewConfig);
        mFlingAnimationUtils = new FlingAnimationUtils(context, 2.5f);
        mGestures = new PipTouchGesture[] {
                mDefaultMovementGesture
        };
        mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mMenuController,
                mSnapAlgorithm, mFlingAnimationUtils);
        mTouchState = new PipTouchState(mViewConfig, mHandler,
                () -> mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
                        mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()));

        Resources res = context.getResources();
        mExpandedShortestEdgeSize = res.getDimensionPixelSize(
@@ -429,7 +431,7 @@ public class PipTouchHandler {
                final float distance = bounds.bottom - target;
                fraction = Math.min(distance / bounds.height(), 1f);
            }
            if (Float.compare(fraction, 0f) != 0 || mMenuState != MENU_STATE_NONE) {
            if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) {
                // Update if the fraction > 0, or if fraction == 0 and the menu was already visible
                mMenuController.setDismissFraction(fraction);
            }
@@ -730,8 +732,20 @@ public class PipTouchHandler {
                        null /* animatorListener */);
                setMinimizedStateInternal(false);
            } else if (mMenuState != MENU_STATE_FULL) {
                if (mTouchState.isDoubleTap()) {
                    // Expand to fullscreen if this is a double tap
                    mMotionHelper.expandPip();
                } else if (!mTouchState.isWaitingForDoubleTap()) {
                    // User has stalled long enough for this not to be a drag or a double tap, just
                    // expand the menu
                    mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
                            mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
                } else {
                    // Next touch event _may_ be the second tap for the double-tap, schedule a
                    // fallback runnable to trigger the menu if no touch event occurs before the
                    // next tap
                    mTouchState.scheduleDoubleTapTimeoutCallback();
                }
            } else {
                mMenuController.hideMenu();
                mMotionHelper.expandPip();
+68 −2
Original line number Diff line number Diff line
@@ -17,11 +17,15 @@
package com.android.systemui.pip.phone;

import android.graphics.PointF;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;

import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;

/**
@@ -31,9 +35,17 @@ public class PipTouchState {
    private static final String TAG = "PipTouchHandler";
    private static final boolean DEBUG = false;

    private ViewConfiguration mViewConfig;
    @VisibleForTesting
    static final long DOUBLE_TAP_TIMEOUT = 200;

    private final Handler mHandler;
    private final ViewConfiguration mViewConfig;
    private final Runnable mDoubleTapTimeoutCallback;

    private VelocityTracker mVelocityTracker;
    private long mDownTouchTime = 0;
    private long mLastDownTouchTime = 0;
    private long mUpTouchTime = 0;
    private final PointF mDownTouch = new PointF();
    private final PointF mDownDelta = new PointF();
    private final PointF mLastTouch = new PointF();
@@ -41,13 +53,22 @@ public class PipTouchState {
    private final PointF mVelocity = new PointF();
    private boolean mAllowTouches = true;
    private boolean mIsUserInteracting = false;
    // Set to true only if the multiple taps occur within the double tap timeout
    private boolean mIsDoubleTap = false;
    // Set to true only if a gesture
    private boolean mIsWaitingForDoubleTap = false;
    private boolean mIsDragging = false;
    // The previous gesture was a drag
    private boolean mPreviouslyDragging = false;
    private boolean mStartedDragging = false;
    private boolean mAllowDraggingOffscreen = false;
    private int mActivePointerId;

    public PipTouchState(ViewConfiguration viewConfig) {
    public PipTouchState(ViewConfiguration viewConfig, Handler handler,
            Runnable doubleTapTimeoutCallback) {
        mViewConfig = viewConfig;
        mHandler = handler;
        mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
    }

    /**
@@ -81,6 +102,14 @@ public class PipTouchState {
                mDownTouch.set(mLastTouch);
                mAllowDraggingOffscreen = true;
                mIsUserInteracting = true;
                mDownTouchTime = ev.getEventTime();
                mIsDoubleTap = !mPreviouslyDragging &&
                        (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
                mIsWaitingForDoubleTap = false;
                mLastDownTouchTime = mDownTouchTime;
                if (mDoubleTapTimeoutCallback != null) {
                    mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
@@ -155,7 +184,11 @@ public class PipTouchState {
                    break;
                }

                mUpTouchTime = ev.getEventTime();
                mLastTouch.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
                mPreviouslyDragging = mIsDragging;
                mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging &&
                        (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;

                // Fall through to clean up
            }
@@ -251,6 +284,39 @@ public class PipTouchState {
        return mAllowDraggingOffscreen;
    }

    /**
     * @return whether this gesture is a double-tap.
     */
    public boolean isDoubleTap() {
        return mIsDoubleTap;
    }

    /**
     * @return whether this gesture will potentially lead to a following double-tap.
     */
    public boolean isWaitingForDoubleTap() {
        return mIsWaitingForDoubleTap;
    }

    /**
     * Schedules the callback to run if the next double tap does not occur.  Only runs if
     * isWaitingForDoubleTap() is true.
     */
    public void scheduleDoubleTapTimeoutCallback() {
        if (mIsWaitingForDoubleTap) {
            long delay = getDoubleTapTimeoutCallbackDelay();
            mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
            mHandler.postDelayed(mDoubleTapTimeoutCallback, delay);
        }
    }

    @VisibleForTesting long getDoubleTapTimeoutCallbackDelay() {
        if (mIsWaitingForDoubleTap) {
            return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
        }
        return -1;
    }

    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.pip.phone;

import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.pip.phone.PipTouchState;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class PipTouchStateTest extends SysuiTestCase {

    private Handler mHandler;
    private HandlerThread mHandlerThread;
    private PipTouchState mTouchState;
    private CountDownLatch mDoubleTapCallbackTriggeredLatch;

    @Before
    public void setUp() throws Exception {
        mHandlerThread = new HandlerThread("PipTouchStateTestThread");
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());

        mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1);
        mTouchState = new PipTouchState(ViewConfiguration.get(getContext()),
                mHandler, () -> {
            mDoubleTapCallbackTriggeredLatch.countDown();
        });
        assertFalse(mTouchState.isDoubleTap());
        assertFalse(mTouchState.isWaitingForDoubleTap());
    }

    @Test
    public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() {
        final long currentTime = SystemClock.uptimeMillis();

        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0));
        assertFalse(mTouchState.isDoubleTap());
        assertFalse(mTouchState.isWaitingForDoubleTap());
        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
    }

    @Test
    public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception {
        final long currentTime = SystemClock.uptimeMillis();

        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
        assertFalse(mTouchState.isDoubleTap());
        assertTrue(mTouchState.isWaitingForDoubleTap());

        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10);
        mTouchState.scheduleDoubleTapTimeoutCallback();
        mDoubleTapCallbackTriggeredLatch.await(1, TimeUnit.SECONDS);
        assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0);
    }

    @Test
    public void testDoubleTapDrag_doubleTapCanceled() {
        final long currentTime = SystemClock.uptimeMillis();

        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500));
        assertTrue(mTouchState.isDragging());
        assertFalse(mTouchState.isDoubleTap());
        assertFalse(mTouchState.isWaitingForDoubleTap());
        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
    }

    @Test
    public void testDoubleTap_doubleTapRegistered() {
        final long currentTime = SystemClock.uptimeMillis();

        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN,
                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0));
        mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
                currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
        assertTrue(mTouchState.isDoubleTap());
        assertFalse(mTouchState.isWaitingForDoubleTap());
        assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
    }

    private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) {
        return MotionEvent.obtain(0, eventTime, action, x, y, 0);
    }
}
 No newline at end of file