Compose gesture integrated fully into Launcher

- Support dismissing Compose via the reverse gesture from the appear
gesture
- Use Tony Wickham's ag/10204761 with some glue code to enable the
app below Compose panning in the same direction as the gesture as
Compose peeks in
- Add feature flag to use Compose hosted in a window (permits underlying
app panning)
- Use InterpolatingVelocityTracker to fix OtherActivityInputConsumer
processing swipes in the wrong direction ~20% of the time due to a bug
in VelocityTracker (see go/quirky-bubbles)

Change-Id: I3adbaee1763f21557fb628b60d03b0a03e7079ab
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 1f6c506..6b0d7a3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
+import static com.android.quickstep.GestureState.STATE_OVERSCROLL_WINDOW_CREATED;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
@@ -430,6 +431,6 @@
 
     @Override
     public boolean allowInterceptByParent() {
-        return !mPassedPilferInputSlop;
+        return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index c49b8f2..fb420a2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -24,12 +24,15 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 
+import static java.lang.Math.abs;
+
 import android.content.Context;
 import android.graphics.PointF;
-import android.view.GestureDetector;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseDraggingActivity;
@@ -44,24 +47,36 @@
  * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
  */
 public class OverscrollInputConsumer extends DelegateInputConsumer {
-
     private static final String TAG = "OverscrollInputConsumer";
+    private static final boolean DEBUG_LOGS_ENABLED = false;
+    private static void debugPrint(String log) {
+        if (DEBUG_LOGS_ENABLED) {
+            Log.v(TAG, log);
+        }
+    }
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     private final PointF mStartDragPos = new PointF();
     private final int mAngleThreshold;
 
-    private final float mFlingThresholdPx;
+    private final int mFlingDistanceThresholdPx;
+    private final int mFlingVelocityThresholdPx;
     private int mActivePointerId = -1;
     private boolean mPassedSlop = false;
-
+    // True if we set ourselves as active, meaning we no longer pass events to the delegate.
+    private boolean mPassedActiveThreshold = false;
+    // When a gesture crosses this length, this recognizer will attempt to interpret touch events.
     private final float mSquaredSlop;
+    // When a gesture crosses this length, this recognizer will become the sole active recognizer.
+    private final float mSquaredActiveThreshold;
+    // When a gesture crosses this length, the overscroll view should be shown.
+    private final float mSquaredFinishThreshold;
+    private boolean mThisDownIsIgnored = false;
 
     private final GestureState mGestureState;
     @Nullable
     private final OverscrollPlugin mPlugin;
-    private final GestureDetector mGestureDetector;
 
     @Nullable
     private RecentsView mRecentsView;
@@ -72,15 +87,24 @@
 
         mAngleThreshold = context.getResources()
                 .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
-        mFlingThresholdPx = context.getResources()
-            .getDimension(R.dimen.gestures_overscroll_fling_threshold);
+        mFlingDistanceThresholdPx = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_fling_threshold);
+        mFlingVelocityThresholdPx = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
         mGestureState = gestureState;
         mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
 
         mSquaredSlop = slop * slop;
-        mGestureDetector = new GestureDetector(context, new FlingGestureListener());
+
+
+        float finishGestureThreshold = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_finish_threshold);
+        mSquaredFinishThreshold = finishGestureThreshold * finishGestureThreshold;
+
+        float activeThreshold = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_active_threshold);
+        mSquaredActiveThreshold = activeThreshold * activeThreshold;
     }
 
     @Override
@@ -90,12 +114,26 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
+        if (mPlugin == null) {
+            return;
+        }
+
+        debugPrint("got event, underlying activity is " + getUnderlyingActivity());
         switch (ev.getActionMasked()) {
             case ACTION_DOWN: {
+                debugPrint("ACTION_DOWN");
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
-
+                if (mPlugin.blockOtherGestures()) {
+                    debugPrint("mPlugin.blockOtherGestures(), becoming active on ACTION_DOWN");
+                    // Otherwise, if an appear gesture is performed when the Activity is visible,
+                    // the Activity will dismiss its keyboard.
+                    mPassedActiveThreshold = true;
+                    mPassedSlop = true;
+                    mStartDragPos.set(mLastPos.x, mLastPos.y);
+                    setActive(ev);
+                }
                 break;
             }
             case ACTION_POINTER_DOWN: {
@@ -121,57 +159,61 @@
                 if (mState == STATE_DELEGATE_ACTIVE) {
                     break;
                 }
+
                 if (!mDelegate.allowInterceptByParent()) {
                     mState = STATE_DELEGATE_ACTIVE;
                     break;
                 }
+
+                // Update last touch position.
                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
                 if (pointerIndex == -1) {
                     break;
                 }
                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
 
-                if (!mPassedSlop) {
-                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
-                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
-                            > mSquaredSlop) {
-
-                        mPassedSlop = true;
-                        mStartDragPos.set(mLastPos.x, mLastPos.y);
-                        if (isOverscrolled()) {
-                            setActive(ev);
-
-                            if (mPlugin != null) {
-                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
-                            }
-                        } else {
-                            mState = STATE_DELEGATE_ACTIVE;
-                        }
-                    }
+                float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+                if ((!mPassedSlop) && (squaredDist > mSquaredSlop)) {
+                    mPassedSlop = true;
+                    mStartDragPos.set(mLastPos.x, mLastPos.y);
+                    mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED);
                 }
 
-                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
-                        && mPlugin != null) {
-                    mPlugin.onTouchTraveled(getDistancePx());
+                boolean becomeActive = mPassedSlop && !mPassedActiveThreshold && isOverscrolled()
+                        && (squaredDist > mSquaredActiveThreshold);
+                if (becomeActive) {
+                    debugPrint("Past slop and past threshold, set active");
+                    mPassedActiveThreshold = true;
+                    setActive(ev);
+                }
+
+                if (mPassedActiveThreshold) {
+                    debugPrint("ACTION_MOVE Relaying touch event");
+                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
+                            (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx,
+                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                 }
 
                 break;
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
-                    mPlugin.onTouchEnd(getDistancePx());
+                debugPrint("ACTION_UP");
+                if (mPassedActiveThreshold) {
+                    debugPrint("ACTION_UP Relaying touch event");
+
+                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
+                            (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx,
+                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                 }
 
+
                 mPassedSlop = false;
+                mPassedActiveThreshold = false;
                 mState = STATE_INACTIVE;
                 break;
         }
 
-        if (mState != STATE_DELEGATE_ACTIVE) {
-            mGestureDetector.onTouchEvent(ev);
-        }
-
         if (mState != STATE_ACTIVE) {
             mDelegate.onMotionEvent(ev);
         }
@@ -192,15 +234,20 @@
             maxIndex = 1;
         }
 
-        boolean atRightMostApp = (mRecentsView == null
-                || mRecentsView.getRunningTaskIndex() <= maxIndex);
+        boolean atRightMostApp = mRecentsView == null
+                || (mRecentsView.getRunningTaskIndex() <= maxIndex);
 
         // Check if the gesture is within our angle threshold of horizontal
-        float deltaY = Math.abs(mLastPos.y - mDownPos.y);
-        float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left
-        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
+        float deltaY = abs(mLastPos.y - mDownPos.y);
+        float deltaX = mLastPos.x - mDownPos.x;
 
-        return atRightMostApp && angleInBounds;
+        boolean angleInBounds = (Math.toDegrees(Math.atan2(deltaY, abs(deltaX))) < mAngleThreshold);
+
+        boolean overscrollVisible = mPlugin.blockOtherGestures();
+        boolean overscrollInvisibleAndLeftSwipe = !overscrollVisible && deltaX < 0;
+        boolean gestureDirectionMatchesVisibility = overscrollVisible
+                || overscrollInvisibleAndLeftSwipe;
+        return atRightMostApp && angleInBounds && gestureDirectionMatchesVisibility;
     }
 
     private String getDeviceState() {
@@ -219,35 +266,22 @@
         return deviceState;
     }
 
-    private int getDistancePx() {
-        return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+    private int getHorizontalDistancePx() {
+        return (int) (mLastPos.x - mDownPos.x);
     }
 
-    private String getUnderlyingActivity() {
+    private int getVerticalDistancePx() {
+        return (int) (mLastPos.y - mDownPos.y);
+    }
+
+    private @NonNull String getUnderlyingActivity() {
+        // Overly defensive, got guidance on code review that something in the chain of
+        // `mGestureState.getRunningTask().topActivity` can be null and thus cause a null pointer
+        // exception to be thrown, but we aren't sure which part can be null.
+        if ((mGestureState == null) || (mGestureState.getRunningTask() == null)
+                || (mGestureState.getRunningTask().topActivity == null)) {
+            return "";
+        }
         return mGestureState.getRunningTask().topActivity.flattenToString();
     }
-
-    private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener {
-        @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
-            if (isValidAngle(velocityX, -velocityY)
-                    && getDistancePx() >= mFlingThresholdPx
-                    && mState != STATE_DELEGATE_ACTIVE) {
-
-                if (mPlugin != null) {
-                    mPlugin.onFling(-velocityX);
-                }
-            }
-            return true;
-        }
-
-        private boolean isValidAngle(float deltaX, float deltaY) {
-            float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
-            // normalize so that angle is measured clockwise from horizontal in the bottom right
-            // corner and counterclockwise from horizontal in the bottom left corner
-
-            angle = angle > 90 ? 180 - angle : angle;
-            return (angle < mAngleThreshold);
-        }
-    }
 }
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b06dc6b..8586804 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -79,6 +79,8 @@
 
     <!-- Overscroll Gesture -->
     <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
+    <dimen name="gestures_overscroll_active_threshold">80dp</dimen>
+    <dimen name="gestures_overscroll_finish_threshold">136dp</dimen>
 
     <!-- Tips Gesture Tutorial -->
     <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index f06e1a6..5adcc2e 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -107,6 +107,10 @@
     public static final int STATE_RECENTS_ANIMATION_ENDED =
             getFlagForIndex("STATE_RECENTS_ANIMATION_ENDED");
 
+    // Called when we create an overscroll window when swiping right to left on the most recent app
+    public static final int STATE_OVERSCROLL_WINDOW_CREATED =
+            getFlagForIndex("STATE_OVERSCROLL_WINDOW_CREATED");
+
     // Called when RecentsView stops scrolling and settles on a TaskView.
     public static final int STATE_RECENTS_SCROLLING_FINISHED =
             getFlagForIndex("STATE_RECENTS_SCROLLING_FINISHED");
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 78d194b..6919339 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -110,6 +110,9 @@
     public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag(
             "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
 
+    public static final BooleanFlag ENABLE_QUICK_CAPTURE_WINDOW = getDebugFlag(
+            "ENABLE_QUICK_CAPTURE_WINDOW", false, "Use window to host quick capture");
+
     public static final BooleanFlag FORCE_LOCAL_OVERSCROLL_PLUGIN = getDebugFlag(
             "FORCE_LOCAL_OVERSCROLL_PLUGIN", false,
             "Use a launcher-provided OverscrollPlugin if available");
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 28a9193..a434d07 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.plugins;
 
+import android.view.MotionEvent;
+
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 /**
@@ -28,7 +30,7 @@
 public interface OverscrollPlugin extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
-    int VERSION = 3;
+    int VERSION = 4;
 
     String DEVICE_STATE_LOCKED = "Locked";
     String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -41,33 +43,33 @@
     boolean isActive();
 
     /**
-     * Called when a touch is down and has been recognized as an overscroll gesture.
-     * A call of this method will always result in `onTouchUp` being called, and possibly
-     * `onFling` as well.
-     *
+     * Called when a touch has been recognized as an overscroll gesture.
+     * @param horizontalDistancePx Horizontal distance from the last finger location to the finger
+     *                               location when it first touched the screen.
+     * @param verticalDistancePx Horizontal distance from the last finger location to the finger
+     *                             location when it first touched the screen.
+     * @param thresholdPx Minimum distance for gesture.
+     * @param flingDistanceThresholdPx Minimum distance for gesture by fling.
+     * @param flingVelocityThresholdPx Minimum velocity for gesture by fling.
      * @param deviceState String representing the current device state
      * @param underlyingActivity String representing the currently active Activity
      */
-    void onTouchStart(String deviceState, String underlyingActivity);
+    void onTouchEvent(MotionEvent event,
+                      int horizontalDistancePx,
+                      int verticalDistancePx,
+                      int thresholdPx,
+                      int flingDistanceThresholdPx,
+                      int flingVelocityThresholdPx,
+                      String deviceState,
+                      String underlyingActivity);
 
     /**
-     * Called when a touch that was previously recognized has moved.
-     *
-     * @param px distance between the position of touch on this update and the position of the
-     * touch when it was initially recognized.
+     * @return `true` if overscroll gesture handling should override all other gestures.
      */
-    void onTouchTraveled(int px);
+    boolean blockOtherGestures();
 
     /**
-     * Called when a touch that was previously recognized has ended.
-     *
-     * @param px distance between the position of touch on this update and the position of the
-     * touch when it was initially recognized.
+     * @return `true` if the overscroll gesture can pan the underlying app.
      */
-    void onTouchEnd(int px);
-
-    /**
-     * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
-     */
-    void onFling(float velocity);
+    boolean allowsUnderlyingActivityOverscroll();
 }