Merge "Let the bubble bar handle gestures over the bubble bar." into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 139e8f0..445b312 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -155,7 +155,7 @@
         mControllers.taskbarActivityContext.startTranslationSpring();
     }
 
-    /*
+    /**
      * @param ev MotionEvent in screen coordinates.
      * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
      */
@@ -164,6 +164,14 @@
                 || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
     }
 
+    /** Checks if the given {@link MotionEvent} is over the bubble bar stash handle. */
+    public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) {
+        return mControllers.bubbleControllers.map(
+                bubbleControllers ->
+                        bubbleControllers.bubbleStashController.isEventOverStashHandle(ev))
+                .orElse(false);
+    }
+
     /**
      * Returns true if icons should be aligned to hotseat in the current transition.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index a5ea5a9..09021ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -22,6 +22,7 @@
 import android.animation.AnimatorSet;
 import android.annotation.Nullable;
 import android.view.InsetsController;
+import android.view.MotionEvent;
 
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.StashedHandleViewController;
@@ -350,4 +351,9 @@
         return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
                 : getBubbleBarTranslationYForTaskbar();
     }
+
+    /** Checks whether the motion event is over the stash handle. */
+    public boolean isEventOverStashHandle(MotionEvent ev) {
+        return mHandleViewController.isEventOverHandle(ev);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index fbab595..c998d97 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -24,6 +24,7 @@
 import android.content.res.Resources;
 import android.graphics.Outline;
 import android.graphics.Rect;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
@@ -268,4 +269,22 @@
         });
         return revealAnim;
     }
+
+    /** Checks that the stash handle is visible and that the motion event is within bounds. */
+    public boolean isEventOverHandle(MotionEvent ev) {
+        if (mStashedHandleView.getVisibility() != VISIBLE) {
+            return false;
+        }
+
+        // the bounds of the handle only include the visible part, so we check that the Y coordinate
+        // is anywhere within the stashed taskbar height.
+        int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight;
+
+        return (int) ev.getRawY() >= top && containsX((int) ev.getRawX());
+    }
+
+    /** Checks if the given x coordinate is within the stashed handle bounds. */
+    public boolean containsX(int x) {
+        return x >= mStashedHandleBounds.left && x <= mStashedHandleBounds.right;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 3f88139..8925bd6 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -191,8 +191,11 @@
     public abstract boolean allowAllAppsFromOverview();
 
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        TaskbarUIController controller = getTaskbarController();
+        boolean isEventOverBubbleBarStashHandle =
+                controller != null && controller.isEventOverBubbleBarStashHandle(ev);
         return deviceState.isInDeferredGestureRegion(ev) || deviceState.isImeRenderingNavButtons()
-                || isTrackpadMultiFingerSwipe(ev);
+                || isTrackpadMultiFingerSwipe(ev) || isEventOverBubbleBarStashHandle;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 61c2b43..f6a9440 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -30,6 +33,7 @@
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.InputDevice;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
@@ -68,6 +72,9 @@
     private final boolean mIsTaskbarAllAppsOpen;
     private boolean mHasPassedTaskbarNavThreshold;
     private boolean mIsInBubbleBarArea;
+    private boolean mIsVerticalGestureOverBubbleBar;
+    private boolean mIsPassedBubbleBarSlop;
+    private final int mTouchSlop;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -91,6 +98,7 @@
         mOverviewCommandHelper = overviewCommandHelper;
         // TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
 
         Resources res = context.getResources();
@@ -99,8 +107,7 @@
                 taskbarActivityContext.getDeviceProfile());
         mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx
                 - mTaskbarNavThreshold;
-        mIsTaskbarAllAppsOpen =
-                mTaskbarActivityContext != null && mTaskbarActivityContext.isTaskbarAllAppsOpen();
+        mIsTaskbarAllAppsOpen = mTaskbarActivityContext.isTaskbarAllAppsOpen();
 
         mIsTransientTaskbar = DisplayController.isTransientTaskbar(context);
 
@@ -132,12 +139,8 @@
         if (mState != STATE_ACTIVE) {
             boolean isStashedTaskbarHovered = isMouseEvent(ev)
                     && isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
-            if (!isStashedTaskbarHovered) {
-                mDelegate.onMotionEvent(ev);
-            }
-
             // Only show the transient task bar if the touch events are on the screen.
-            if (mTaskbarActivityContext != null && !isTrackpadMotionEvent(ev)) {
+            if (!isTrackpadMotionEvent(ev)) {
                 final float x = ev.getRawX();
                 final float y = ev.getRawY();
                 switch (ev.getAction()) {
@@ -193,14 +196,28 @@
                         }
                         mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
 
+                        float dX = mLastPos.x - mDownPos.x;
+                        float dY = mLastPos.y - mDownPos.y;
+
+                        if (!mIsPassedBubbleBarSlop && mIsInBubbleBarArea) {
+                            boolean passedSlop =
+                                    Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
+                            if (passedSlop) {
+                                mIsPassedBubbleBarSlop = true;
+                                mIsVerticalGestureOverBubbleBar = Math.abs(dY) > Math.abs(dX);
+                                if (mIsVerticalGestureOverBubbleBar) {
+                                    setActive(ev);
+                                }
+                            }
+                        }
+
                         if (mIsTransientTaskbar) {
-                            float dY = mLastPos.y - mDownPos.y;
                             boolean passedTaskbarNavThreshold = dY < 0
                                     && Math.abs(dY) >= mTaskbarNavThreshold;
 
                             if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
                                 mHasPassedTaskbarNavThreshold = true;
-                                if (mIsInBubbleBarArea) {
+                                if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) {
                                     mTaskbarActivityContext.onSwipeToOpenBubblebar();
                                 } else {
                                     mTaskbarActivityContext.onSwipeToUnstashTaskbar();
@@ -217,17 +234,7 @@
                         break;
                     case MotionEvent.ACTION_UP:
                     case MotionEvent.ACTION_CANCEL:
-                        if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
-                            mTaskbarActivityContext.startTaskbarUnstashHint(
-                                    /* animateForward = */ false);
-                        }
-                        mTaskbarActivityContext.setAutohideSuspendFlag(
-                                FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
-                        if (mTransitionCallback != null) {
-                            mTransitionCallback.onActionEnd();
-                        }
-                        mHasPassedTaskbarNavThreshold = false;
-                        mIsInBubbleBarArea = false;
+                        cleanupAfterMotionEvent();
                         break;
                     case MotionEvent.ACTION_BUTTON_RELEASE:
                         if (isStashedTaskbarHovered) {
@@ -236,9 +243,61 @@
                         break;
                 }
             }
+            boolean isMovingInBubbleBarArea = mIsInBubbleBarArea && ev.getAction() == ACTION_MOVE;
+            if (!isStashedTaskbarHovered) {
+                // if we're moving in the bubble bar area but we haven't passed the slop yet, don't
+                // propagate to the delegate, until we can determine the direction of the gesture.
+                if (!isMovingInBubbleBarArea || mIsPassedBubbleBarSlop) {
+                    mDelegate.onMotionEvent(ev);
+                }
+            }
+        } else if (mIsVerticalGestureOverBubbleBar) {
+            // if we get here then this gesture is a vertical swipe over the bubble bar.
+            // we're also active and there's no need to delegate any additional motion events. the
+            // rest of the gesture will be handled here.
+            switch (ev.getAction()) {
+                case ACTION_MOVE:
+                    int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                    if (pointerIndex == INVALID_POINTER_ID) {
+                        break;
+                    }
+                    mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                    float dY = mLastPos.y - mDownPos.y;
+
+                    // bubble bar swipe gesture uses the same threshold as the taskbar.
+                    boolean passedTaskbarNavThreshold = dY < 0
+                            && Math.abs(dY) >= mTaskbarNavThreshold;
+
+                    if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
+                        mHasPassedTaskbarNavThreshold = true;
+                        mTaskbarActivityContext.onSwipeToOpenBubblebar();
+                    }
+                    break;
+                case ACTION_UP:
+                case ACTION_CANCEL:
+                    cleanupAfterMotionEvent();
+                    break;
+            }
         }
     }
 
+    private void cleanupAfterMotionEvent() {
+        if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
+            mTaskbarActivityContext.startTaskbarUnstashHint(
+                    /* animateForward = */ false);
+        }
+        mTaskbarActivityContext.setAutohideSuspendFlag(
+                FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
+        if (mTransitionCallback != null) {
+            mTransitionCallback.onActionEnd();
+        }
+        mHasPassedTaskbarNavThreshold = false;
+        mIsInBubbleBarArea = false;
+        mIsVerticalGestureOverBubbleBar = false;
+        mIsPassedBubbleBarSlop = false;
+    }
+
     private boolean isInTaskbarArea(float x) {
         float areaFromMiddle = mUnstashArea / 2.0f;
         float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
@@ -246,13 +305,19 @@
     }
 
     private boolean isInBubbleBarArea(float x) {
-        if (mTaskbarActivityContext != null && mIsTransientTaskbar) {
-            BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
-            if (controllers == null) return false;
+        if (mTaskbarActivityContext == null || !mIsTransientTaskbar) {
+            return false;
+        }
+        BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
+        if (controllers == null) {
+            return false;
+        }
+        if (controllers.bubbleStashController.isStashed()) {
+            return controllers.bubbleStashedHandleViewController.containsX((int) x);
+        } else {
             Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds();
             return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right;
         }
-        return false;
     }
 
     private void onLongPressDetected(MotionEvent motionEvent) {