Compose overscroll gesture updates

Two changes for the latest Compose prototype:
1. Pass the identity of the underlying app to the Overscroll plugin.
2. Enable the Compose gesture over an app when there's a Recents extra
card plugin active (otherwise the current app won't count as the
rightmost one).

Some changes to the gesture:
- Angle decreased from 35° to 25° to remove overlap with
Assistant gesture
- Distance increased from 8 to 110 dp. 110 dp is 2x the Assistant
gesture and roughly the same as scrubbing into an app from Home.
- Fling detection added; uses same distance threshold, 110 dp.
- If a touch was recognized as another gesture, the touch will not be
reinterpreted as a Compose gesture, no matter what touch movement occurs
- Fixes issue where Assistant + Compose could both be triggered
- Fixes issue where scrubbing apps to the left, then back to the right,
would bring in Compose. i.e. if a touch down + touch movement starts
bringing in Assistant UI elements, then, the user moves their touch
below the Assistant angle, the Compose gesture will not start
being recognized
- Gesture length required for fling lowered from 110 dp to 40 dp, per
tuning with PM.

Bug: b/146508473
Change-Id: I414573d1a92684d1d992837a5f1df522346ec211
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index bafb2ef..29df5cc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -484,7 +484,9 @@
                 base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat);
             }
 
-            if (mOverscrollPlugin != null) {
+            if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()
+                    && (mOverscrollPlugin != null)
+                    && mOverscrollPlugin.isActive()) {
                 // Put the overscroll gesture as higher priority than the Assistant or base gestures
                 base = new OverscrollInputConsumer(this, newGestureState, base, mInputMonitorCompat,
                         mOverscrollPlugin);
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 e3da98b..0a21413 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
@@ -26,14 +26,17 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -47,12 +50,12 @@
 
     private static final String TAG = "OverscrollInputConsumer";
 
-    private static final int ANGLE_THRESHOLD = 35; // Degrees
-
     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 int mActivePointerId = -1;
     private boolean mPassedSlop = false;
 
@@ -60,19 +63,28 @@
 
     private final Context mContext;
     private final GestureState mGestureState;
-    @Nullable private final OverscrollPlugin mPlugin;
+    @Nullable
+    private final OverscrollPlugin mPlugin;
+    private final GestureDetector mGestureDetector;
 
     private RecentsView mRecentsView;
 
     public OverscrollInputConsumer(Context context, GestureState gestureState,
             InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
         super(delegate, inputMonitor);
+
+        mAngleThreshold = context.getResources()
+                .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+        mFlingThresholdPx = context.getResources()
+            .getDimension(R.dimen.gestures_overscroll_fling_threshold);
         mContext = context;
         mGestureState = gestureState;
         mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+
         mSquaredSlop = slop * slop;
+        mGestureDetector = new GestureDetector(context, new FlingGestureListener());
 
         gestureState.getActivityInterface().createActivityInitListener(this::onActivityInit)
                 .register();
@@ -139,21 +151,29 @@
 
                         mPassedSlop = true;
                         mStartDragPos.set(mLastPos.x, mLastPos.y);
-
                         if (isOverscrolled()) {
                             setActive(ev);
+
+                            if (mPlugin != null) {
+                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
+                            }
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
                         }
                     }
                 }
 
+                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
+                        && mPlugin != null) {
+                    mPlugin.onTouchTraveled(getDistancePx());
+                }
+
                 break;
             }
             case ACTION_CANCEL:
             case ACTION_UP:
                 if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
-                    mPlugin.onOverscroll(getDeviceState());
+                    mPlugin.onTouchEnd(getDistancePx());
                 }
 
                 mPassedSlop = false;
@@ -161,6 +181,10 @@
                 break;
         }
 
+        if (mState != STATE_DELEGATE_ACTIVE) {
+            mGestureDetector.onTouchEvent(ev);
+        }
+
         if (mState != STATE_ACTIVE) {
             mDelegate.onMotionEvent(ev);
         }
@@ -168,12 +192,19 @@
 
     private boolean isOverscrolled() {
         // Make sure there isn't an app to quick switch to on our right
-        boolean atRightMostApp = (mRecentsView == null || mRecentsView.getRunningTaskIndex() <= 0);
+        int maxIndex = 0;
+        if ((mRecentsView instanceof LauncherRecentsView)
+                && ((LauncherRecentsView) mRecentsView).hasRecentsExtraCard()) {
+            maxIndex = 1;
+        }
+
+        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)) < ANGLE_THRESHOLD;
+        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
 
         return atRightMostApp && angleInBounds;
     }
@@ -193,4 +224,36 @@
 
         return deviceState;
     }
+
+    private int getDistancePx() {
+        return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+    }
+
+    private String getUnderlyingActivity() {
+        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/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 82fbbc6..1bbb3f5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -378,6 +378,11 @@
     }
 
     @Override
+    public boolean hasRecentsExtraCard() {
+        return mRecentsExtraViewContainer != null;
+    }
+
+    @Override
     public void setContentAlpha(float alpha) {
         super.setContentAlpha(alpha);
         if (mRecentsExtraViewContainer != null) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index bcaa126..47bc31a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -830,6 +830,11 @@
 
     public abstract void startHome();
 
+    /** `true` if there is a +1 space available in overview. */
+    public boolean hasRecentsExtraCard() {
+        return false;
+    }
+
     public void reset() {
         setCurrentTask(-1);
         mIgnoreResetTaskId = -1;
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9ff1350..24be859 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -77,4 +77,7 @@
 
     <!-- Distance to move elements when swiping up to go home from launcher -->
     <dimen name="home_pullback_distance">28dp</dimen>
+
+    <!-- Overscroll Gesture -->
+    <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
 </resources>
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 80c7056..58e36f2 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -114,7 +114,7 @@
             "ENABLE_PREDICTION_DISMISS", false, "Allow option to dimiss apps from predicted list");
 
     public static final TogglableFlag ENABLE_QUICK_CAPTURE_GESTURE = new TogglableFlag(
-            "ENABLE_QUICK_CAPTURE_GESTURE", false, "Swipe from right to left to quick capture");
+            "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
 
     public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
             "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 60eb304..28a9193 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -24,11 +24,11 @@
  * the user to a more recent app).
  */
 @ProvidesInterface(action = com.android.systemui.plugins.OverscrollPlugin.ACTION,
-        version = com.android.systemui.plugins.OverlayPlugin.VERSION)
+        version = com.android.systemui.plugins.OverscrollPlugin.VERSION)
 public interface OverscrollPlugin extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
-    int VERSION = 1;
+    int VERSION = 3;
 
     String DEVICE_STATE_LOCKED = "Locked";
     String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -36,9 +36,38 @@
     String DEVICE_STATE_UNKNOWN = "Unknown";
 
     /**
-     * Called when the user completed a right to left swipe in the gesture area.
-     *
-     * @param deviceState One of the DEVICE_STATE_* constants.
+     * @return true if the plugin is active and will accept overscroll gestures
      */
-    void onOverscroll(String deviceState);
+    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.
+     *
+     * @param deviceState String representing the current device state
+     * @param underlyingActivity String representing the currently active Activity
+     */
+    void onTouchStart(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.
+     */
+    void onTouchTraveled(int px);
+
+    /**
+     * 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.
+     */
+    void onTouchEnd(int px);
+
+    /**
+     * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
+     */
+    void onFling(float velocity);
 }