diff options
5 files changed, 249 insertions, 11 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java index 901b0b0a4e85..90f7b8db1c59 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java @@ -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) -> { - if (mMenuState == MENU_STATE_CLOSE) { - showPipMenu(); - } else { - expandPip(); + 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 { + // 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); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java index e898a51de33c..34666fb30689 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index 31814818303b..2b48e0fb32bd 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -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) { - mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), - mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()); + 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(); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java index 686b3bb9d127..b9369d39dd51 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java @@ -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(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/pip/phone/PipTouchStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/pip/phone/PipTouchStateTest.java new file mode 100644 index 000000000000..b8c946d2988f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/pip/phone/PipTouchStateTest.java @@ -0,0 +1,129 @@ +/* + * 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 |