Merge "Double tap to expand PiP." into oc-mr1-dev
am: 2a6f25b995
Change-Id: I75a3dd9befd6bb1bb2aefb11ea1239ea22072e0f
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 901b0b0..90f7b8d 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.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 @@
}
};
+ private PipTouchState mTouchState;
private PointF mDownPosition = new PointF();
private PointF mDownDelta = new PointF();
private ViewConfiguration mViewConfig;
@@ -175,6 +177,13 @@
// 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 @@
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 e898a51..34666fb 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 @@
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 3181481..2b48e0f 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 @@
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 @@
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 @@
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 686b3bb..b9369d3 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 @@
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 @@
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 @@
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 @@
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 @@
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 0000000..b8c946d
--- /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