Handle swipe down on taskbar to stash

- Moved all touch-to-stash logic to new TaskbarStashViaTouchController
  (handles both tap outside to stash instantly as well as swipe down
  inside to stash after letting go)
- This is a TouchController on TaskbarDragLayer, so it intercepts
  touches from TaskbarView before icons can be dragged during swipe down

Test: swipe up to invoke transient taskbar in an app, swipe down or
touch outside to stash
Flag: ENABLE_TRANSIENT_TASKBAR=true
Fixes: 246631710

Change-Id: I5cf64848bba34ad32fcc80a93fb4f79ebd2c10a7
diff --git a/quickstep/res/values-sw720dp/dimens.xml b/quickstep/res/values-sw720dp/dimens.xml
index f093185..0832a91 100644
--- a/quickstep/res/values-sw720dp/dimens.xml
+++ b/quickstep/res/values-sw720dp/dimens.xml
@@ -43,7 +43,7 @@
     <dimen name="transient_taskbar_icon_size">52dp</dimen>
 
     <!-- Taskbar swipe up thresholds -->
-    <dimen name="taskbar_nav_threshold">30dp</dimen>
+    <dimen name="taskbar_from_nav_threshold">30dp</dimen>
     <dimen name="taskbar_app_window_threshold">100dp</dimen>
     <dimen name="taskbar_home_overview_threshold">180dp</dimen>
     <dimen name="taskbar_catch_up_threshold">300dp</dimen>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 225bdcc..3f4f527 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -303,10 +303,12 @@
     <!-- An additional touch slop to prevent x-axis movement during the swipe up to show taskbar -->
     <dimen name="transient_taskbar_clamped_offset_bound">16dp</dimen>
     <!-- Taskbar swipe up thresholds -->
-    <dimen name="taskbar_nav_threshold">40dp</dimen>
+    <dimen name="taskbar_from_nav_threshold">40dp</dimen>
     <dimen name="taskbar_app_window_threshold">88dp</dimen>
     <dimen name="taskbar_home_overview_threshold">156dp</dimen>
     <dimen name="taskbar_catch_up_threshold">264dp</dimen>
+    <!-- Taskbar swipe down threshold -->
+    <dimen name="taskbar_to_nav_threshold">24dp</dimen>
 
     <!--  Taskbar 3 button spacing  -->
     <dimen name="taskbar_button_space_inbetween">24dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
index 6db5839..5eec6a4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleView.java
@@ -69,6 +69,10 @@
      */
     public void updateSampledRegion(Rect stashedHandleBounds) {
         getLocationOnScreen(mTmpArr);
+        // Translations are temporary due to animations, remove them for the purpose of determining
+        // the final region we want sampled.
+        mTmpArr[0] -= Math.round(getTranslationX());
+        mTmpArr[1] -= Math.round(getTranslationY());
         mSampledRegion.set(stashedHandleBounds);
         mSampledRegion.offset(mTmpArr[0], mTmpArr[1]);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 58d6244..6cc6a84 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -117,22 +117,6 @@
     }
 
     @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (mControllerCallbacks != null) {
-            mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
-        }
-        return super.onInterceptTouchEvent(ev);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        if (mControllerCallbacks != null) {
-            mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
-        }
-        return super.onTouchEvent(ev);
-    }
-
-    @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         if (mControllerCallbacks != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 7c3d14d..56be48e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -18,15 +18,12 @@
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.view.MotionEvent;
 import android.view.ViewTreeObserver;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DimensionUtils;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
 
 import java.io.PrintWriter;
@@ -40,7 +37,6 @@
     private final TaskbarActivityContext mActivity;
     private final TaskbarDragLayer mTaskbarDragLayer;
     private final int mFolderMargin;
-    private float mGestureHeightYThreshold;
 
     // Alpha properties for taskbar background.
     private final AnimatedFloat mBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
@@ -59,6 +55,7 @@
 
     // Initialized in init.
     private TaskbarControllers mControllers;
+    private TaskbarStashViaTouchController mTaskbarStashViaTouchController;
     private AnimatedFloat mOnBackgroundNavButtonColorIntensity;
 
     private float mLastSetBackgroundAlpha;
@@ -69,11 +66,11 @@
         mTaskbarDragLayer = taskbarDragLayer;
         final Resources resources = mTaskbarDragLayer.getResources();
         mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
-        updateGestureHeight();
     }
 
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        mTaskbarStashViaTouchController = new TaskbarStashViaTouchController(mControllers);
         mTaskbarDragLayer.init(new TaskbarDragLayerCallbacks());
 
         mOnBackgroundNavButtonColorIntensity = mControllers.navbarButtonsViewController
@@ -130,17 +127,11 @@
         return mBgOffset;
     }
 
-    private void updateGestureHeight() {
-        int gestureHeight = ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
-                mActivity.getResources());
-        mGestureHeightYThreshold = mActivity.getDeviceProfile().heightPx - gestureHeight;
-    }
-
     /**
      * Make updates when configuration changes.
      */
     public void onConfigurationChanged() {
-        updateGestureHeight();
+        mTaskbarStashViaTouchController.updateGestureHeight();
     }
 
     private void updateBackgroundAlpha() {
@@ -206,8 +197,6 @@
      */
     public class TaskbarDragLayerCallbacks {
 
-        private final int[] mTempOutLocation = new int[2];
-
         /**
          * Called to update the touchable insets.
          * @see ViewTreeObserver.InternalInsetsInfo#setTouchableInsets(int)
@@ -217,37 +206,6 @@
         }
 
         /**
-         * Listens to TaskbarDragLayer touch events and responds accordingly.
-         */
-        public void tryStashBasedOnMotionEvent(MotionEvent ev) {
-            if (!DisplayController.isTransientTaskbar(mActivity)) {
-                return;
-            }
-            if (mControllers.taskbarStashController.isStashed()) {
-                return;
-            }
-
-            boolean stashTaskbar = false;
-
-            MotionEvent screenCoordinates = MotionEvent.obtain(ev);
-            if (ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
-                stashTaskbar = true;
-            } else if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-                mTaskbarDragLayer.getLocationOnScreen(mTempOutLocation);
-                screenCoordinates.offsetLocation(mTempOutLocation[0], mTempOutLocation[1]);
-
-                if (!mControllers.taskbarViewController.isEventOverAnyItem(screenCoordinates)
-                        && screenCoordinates.getY() < mGestureHeightYThreshold) {
-                    stashTaskbar = true;
-                }
-            }
-
-            if (stashTaskbar) {
-                mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
-            }
-        }
-
-        /**
          * Called when a child is removed from TaskbarDragLayer.
          */
         public void onDragLayerViewRemoved() {
@@ -276,9 +234,12 @@
          * Returns touch controllers.
          */
         public TouchController[] getTouchControllers() {
-            return new TouchController[]{mActivity.getDragController(),
+            return new TouchController[] {
+                    mActivity.getDragController(),
                     mControllers.taskbarForceVisibleImmersiveController,
-                    mControllers.navbarButtonsViewController.getTouchController()};
+                    mControllers.navbarButtonsViewController.getTouchController(),
+                    mTaskbarStashViaTouchController,
+            };
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
new file mode 100644
index 0000000..0e5fa38
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 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.launcher3.taskbar
+
+import android.view.MotionEvent
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.anim.Interpolators.LINEAR
+import com.android.launcher3.testing.shared.ResourceUtils
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.TouchController
+import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer
+
+/**
+ * A helper [TouchController] for [TaskbarDragLayerController], specifically to handle touch events
+ * to stash Transient Taskbar. There are two cases to handle:
+ * - A touch outside of Transient Taskbar bounds will immediately stash on [MotionEvent.ACTION_DOWN]
+ *   or [MotionEvent.ACTION_OUTSIDE].
+ * - Touches inside Transient Taskbar bounds will stash if it is detected as a swipe down gesture.
+ *
+ * Note: touches to *unstash* Taskbar are handled by [TaskbarStashInputConsumer].
+ */
+class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController {
+
+    private val activity: TaskbarActivityContext = controllers.taskbarActivityContext
+    private val enabled = DisplayController.isTransientTaskbar(activity)
+    private val swipeDownDetector: SingleAxisSwipeDetector
+    private val translationCallback = controllers.taskbarTranslationController.transitionCallback
+    /** Interpolator to apply resistance as user swipes down to the bottom of the screen. */
+    private val displacementInterpolator = LINEAR
+    /** How far we can translate the TaskbarView before it's offscreen. */
+    private val maxVisualDisplacement =
+        activity.resources.getDimensionPixelSize(R.dimen.transient_taskbar_margin).toFloat()
+    /** How far the swipe could go, if user swiped from the very top of TaskbarView. */
+    private val maxTouchDisplacement = maxVisualDisplacement + activity.deviceProfile.taskbarSize
+    private val touchDisplacementToStash =
+        activity.resources.getDimensionPixelSize(R.dimen.taskbar_to_nav_threshold).toFloat()
+
+    /** The height of the system gesture region, so we don't stash when touching down there. */
+    private var gestureHeightYThreshold = 0f
+
+    init {
+        updateGestureHeight()
+        swipeDownDetector = SingleAxisSwipeDetector(activity, createSwipeListener(), VERTICAL)
+        swipeDownDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false)
+    }
+
+    fun updateGestureHeight() {
+        if (!enabled) return
+
+        val gestureHeight: Int =
+            ResourceUtils.getNavbarSize(
+                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
+                activity.resources
+            )
+        gestureHeightYThreshold = (activity.deviceProfile.heightPx - gestureHeight).toFloat()
+    }
+
+    private fun createSwipeListener() =
+        object : SingleAxisSwipeDetector.Listener {
+            private var lastDisplacement = 0f
+
+            override fun onDragStart(start: Boolean, startDisplacement: Float) {}
+
+            override fun onDrag(displacement: Float): Boolean {
+                lastDisplacement = displacement
+                if (displacement < 0) return false
+                // Apply resistance so that the visual displacement doesn't go beyond the screen.
+                translationCallback.onActionMove(
+                    Utilities.mapToRange(
+                        displacement,
+                        0f,
+                        maxTouchDisplacement,
+                        0f,
+                        maxVisualDisplacement,
+                        displacementInterpolator
+                    )
+                )
+                return false
+            }
+
+            override fun onDragEnd(velocity: Float) {
+                val isFlingDown = swipeDownDetector.isFling(velocity) && velocity > 0
+                val isSignificantDistance = lastDisplacement > touchDisplacementToStash
+                if (isFlingDown || isSignificantDistance) {
+                    // Successfully triggered stash.
+                    controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
+                }
+                translationCallback.onActionEnd()
+                swipeDownDetector.finishedScrolling()
+            }
+        }
+
+    override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
+        if (!enabled || controllers.taskbarStashController.isStashed) {
+            return false
+        }
+
+        val screenCoordinatesEv = MotionEvent.obtain(ev)
+        screenCoordinatesEv.setLocation(ev.rawX, ev.rawY)
+        if (ev.action == MotionEvent.ACTION_OUTSIDE) {
+            controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
+        } else if (controllers.taskbarViewController.isEventOverAnyItem(screenCoordinatesEv)) {
+            swipeDownDetector.onTouchEvent(ev)
+            if (swipeDownDetector.isDraggingState) {
+                return true
+            }
+        } else if (ev.action == MotionEvent.ACTION_DOWN) {
+            if (screenCoordinatesEv.y < gestureHeightYThreshold) {
+                controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
+            }
+        }
+        return false
+    }
+
+    override fun onControllerTouchEvent(ev: MotionEvent) = swipeDownDetector.onTouchEvent(ev)
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 0116e16..695c3e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -420,19 +420,9 @@
     }
 
     @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        mControllerCallbacks.onInterceptTouchEvent(ev);
-        return super.onInterceptTouchEvent(ev);
-    }
-
-    @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (mIconLayoutBounds.left <= event.getX()
-                && event.getX() <= mIconLayoutBounds.right
-                && !DisplayController.isTransientTaskbar(mActivityContext)) {
-            // Don't allow long pressing between icons, or above/below them
-            // unless its transient taskbar.
-            mControllerCallbacks.clearTouchInProgress();
+        if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) {
+            // Don't allow long pressing between icons, or above/below them.
             return true;
         }
         if (mControllerCallbacks.onTouchEvent(event)) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 4d92a9e..1560791 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -26,8 +26,6 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
 import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_REVEAL_ANIM;
@@ -61,7 +59,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.ThemedIconDrawable;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
@@ -115,9 +112,6 @@
 
     private final TaskbarModelCallbacks mModelCallbacks;
 
-    // Captures swipe down action to close transient Taskbar.
-    protected @Nullable SingleAxisSwipeDetector mSwipeDownDetector;
-
     // Initialized in init.
     private TaskbarControllers mControllers;
 
@@ -154,31 +148,6 @@
             colorHSL[2] = TASKBAR_DARK_THEME_ICONS_BACKGROUND_LUMINANCE;
             mTaskbarThemedIconsBackgroundColor = ColorUtils.HSLToColor(colorHSL);
         }
-
-        if (DisplayController.isTransientTaskbar(mActivity)) {
-            mSwipeDownDetector = new SingleAxisSwipeDetector(activity,
-                    new SingleAxisSwipeDetector.Listener() {
-                        private float mLastDisplacement;
-
-                        @Override
-                        public boolean onDrag(float displacement) {
-                            mLastDisplacement = displacement;
-                            return false;
-                        }
-
-                        @Override
-                        public void onDragEnd(float velocity) {
-                            if (mLastDisplacement > 0) {
-                                mControllers.taskbarStashController
-                                        .updateAndAnimateTransientTaskbar(true);
-                            }
-                        }
-
-                        @Override
-                        public void onDragStart(boolean start, float startDisplacement) {}
-                    }, VERTICAL);
-            mSwipeDownDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
-        }
         mIsRtl = Utilities.isRtl(mTaskbarView.getResources());
     }
 
@@ -649,8 +618,6 @@
         private float mDownX, mDownY;
         private boolean mCanceledStashHint;
 
-        private boolean mTouchInProgress;
-
         public View.OnClickListener getIconOnClickListener() {
             return mActivity.getItemOnClickListener();
         }
@@ -672,75 +639,39 @@
         }
 
         /**
-         * Simply listens to all intercept touch events passed to TaskbarView.
-         */
-        public void onInterceptTouchEvent(MotionEvent ev) {
-            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-                mTouchInProgress = true;
-            }
-
-            if (mTouchInProgress && mSwipeDownDetector != null) {
-                mSwipeDownDetector.onTouchEvent(ev);
-            }
-
-            if (ev.getAction() == MotionEvent.ACTION_UP
-                    || ev.getAction() == MotionEvent.ACTION_CANCEL) {
-                clearTouchInProgress();
-            }
-        }
-
-        /**
          * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
          * consume the touch so TaskbarView treats it as an ACTION_CANCEL.
+         * TODO(b/270395798): We can remove this entirely once we remove the Transient Taskbar flag.
          */
         public boolean onTouchEvent(MotionEvent motionEvent) {
-            boolean shouldConsumeTouch = false;
-            boolean clearTouchInProgress = false;
-
             final float x = motionEvent.getRawX();
             final float y = motionEvent.getRawY();
             switch (motionEvent.getAction()) {
                 case MotionEvent.ACTION_DOWN:
-                    mTouchInProgress = true;
                     mDownX = x;
                     mDownY = y;
                     mControllers.taskbarStashController.startStashHint(/* animateForward = */ true);
                     mCanceledStashHint = false;
                     break;
                 case MotionEvent.ACTION_MOVE:
-                    if (mTouchInProgress
-                            && !mCanceledStashHint
+                    if (!mCanceledStashHint
                             && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
                         mControllers.taskbarStashController.startStashHint(
                                 /* animateForward= */ false);
                         mCanceledStashHint = true;
-                        shouldConsumeTouch = true;
+                        return true;
                     }
                     break;
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL:
-                    if (mTouchInProgress && !mCanceledStashHint) {
+                    if (!mCanceledStashHint) {
                         mControllers.taskbarStashController.startStashHint(
                                 /* animateForward= */ false);
                     }
-                    clearTouchInProgress = true;
                     break;
             }
 
-            if (mTouchInProgress && mSwipeDownDetector != null) {
-                mSwipeDownDetector.onTouchEvent(motionEvent);
-            }
-            if (clearTouchInProgress) {
-                clearTouchInProgress();
-            }
-            return shouldConsumeTouch;
-        }
-
-        /**
-         * Ensures that we do not pass any more touch events to the SwipeDetector.
-         */
-        public void clearTouchInProgress() {
-            mTouchInProgress = false;
+            return false;
         }
 
         /**
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
index 1ddb855..87559fb 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -74,7 +74,7 @@
 
         Resources res = context.getResources();
         mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
-        mTaskbarNavThreshold = res.getDimensionPixelSize(R.dimen.taskbar_nav_threshold);
+        mTaskbarNavThreshold = res.getDimensionPixelSize(R.dimen.taskbar_from_nav_threshold);
         mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx
                 - mTaskbarNavThreshold;
         mIsTaskbarAllAppsOpen =
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 8ff6888..823003c 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -18,6 +18,7 @@
 
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_OUTSIDE;
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
@@ -260,7 +261,10 @@
             mTouchCompleteListener = null;
         }
 
-        if (mActiveController != null) {
+        if (mActiveController != null && ev.getAction() != ACTION_OUTSIDE) {
+            // For some reason, once we intercept touches and have an mActiveController, we won't
+            // get onInterceptTouchEvent() for ACTION_OUTSIDE. Thus, we must recalculate a new
+            // TouchController (if any) to handle the ACTION_OUTSIDE here in onTouchEvent() as well.
             return mActiveController.onControllerTouchEvent(ev);
         } else {
             // In case no child view handled the touch event, we may not get onIntercept anymore