Merge "Extract TaskbarViewCallbacks to a separate class + factory." into main
diff --git a/quickstep/res/drawable/view_carousel.xml b/quickstep/res/drawable/view_carousel.xml
new file mode 100644
index 0000000..16c8e78
--- /dev/null
+++ b/quickstep/res/drawable/view_carousel.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <group>
+    <clip-path
+        android:pathData="M0,0h24v24h-24z"/>
+    <path
+        android:pathData="M17,19H7V4H17V19ZM6,6H2V17H6V6ZM9,6H15V17H9V6ZM22,6H18V17H22V6Z"
+        android:fillColor="#ffffff"
+        android:fillType="evenOdd"/>
+  </group>
+</vector>
diff --git a/quickstep/res/layout/keyboard_quick_switch_overview.xml b/quickstep/res/layout/keyboard_quick_switch_overview.xml
index 4a9b023..30ca32d 100644
--- a/quickstep/res/layout/keyboard_quick_switch_overview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_overview.xml
@@ -40,7 +40,7 @@
             android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
             android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
             android:layout_marginBottom="8dp"
-            android:src="@drawable/ic_empty_recents"
+            android:src="@drawable/view_carousel"
             android:tint="?androidprv:attr/materialColorOnSurface"
 
             app:layout_constraintVertical_chainStyle="packed"
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 3256b0b..2bba788 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -42,7 +42,7 @@
             android:layout_width="@dimen/keyboard_quick_switch_no_recent_items_icon_size"
             android:layout_height="@dimen/keyboard_quick_switch_no_recent_items_icon_size"
             android:layout_marginBottom="@dimen/keyboard_quick_switch_no_recent_items_icon_margin"
-            android:src="@drawable/ic_empty_recents"
+            android:src="@drawable/view_carousel"
             android:tint="?androidprv:attr/materialColorOnSurface"
             android:importantForAccessibility="no"
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 2421c94..3e262e5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -32,7 +32,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
-import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SlideInRemoteTransition;
@@ -84,9 +83,7 @@
             boolean updateTasks,
             int currentFocusIndexOverride,
             boolean onDesktop) {
-        TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
-        dragLayer.addView(mKeyboardQuickSwitchView);
-        dragLayer.runOnClickOnce(v -> closeQuickSwitchView(true));
+        mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
         mOnDesktop = onDesktop;
 
         mKeyboardQuickSwitchView.applyLoadPlan(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index b8e6889..aa457ca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -24,11 +24,13 @@
 import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
 import android.os.Binder
 import android.os.IBinder
+import android.view.DisplayInfo
 import android.view.Gravity
 import android.view.InsetsFrameProvider
 import android.view.InsetsFrameProvider.SOURCE_DISPLAY
 import android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER
 import android.view.InsetsSource.FLAG_SUPPRESS_SCRIM
+import android.view.Surface
 import android.view.ViewTreeObserver
 import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME
 import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION
@@ -155,19 +157,20 @@
         }
 
         val gravity = windowLayoutParams.gravity
-        for (provider in windowLayoutParams.providedInsets) {
-            setProviderInsets(provider, gravity)
-        }
 
-        if (windowLayoutParams.paramsForRotation != null) {
+        // Pre-calculate insets for different providers across different rotations for this gravity
+        for (rotation in Surface.ROTATION_0..Surface.ROTATION_270) {
             // Add insets for navbar rotated params
-            for (layoutParams in windowLayoutParams.paramsForRotation) {
+            if (windowLayoutParams.paramsForRotation != null) {
+                val layoutParams = windowLayoutParams.paramsForRotation[rotation]
                 for (provider in layoutParams.providedInsets) {
-                    setProviderInsets(provider, layoutParams.gravity)
+                    setProviderInsets(provider, layoutParams.gravity, rotation)
                 }
             }
+            for (provider in windowLayoutParams.providedInsets) {
+                setProviderInsets(provider, gravity, rotation)
+            }
         }
-
         context.notifyUpdateLayoutParams()
     }
 
@@ -211,12 +214,12 @@
         )
     }
 
-    private fun setProviderInsets(provider: InsetsFrameProvider, gravity: Int) {
+    private fun setProviderInsets(provider: InsetsFrameProvider, gravity: Int, endRotation: Int) {
         val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps
         val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
         val res = context.resources
         if (provider.type == navigationBars() || provider.type == mandatorySystemGestures()) {
-            provider.insetsSize = getInsetsForGravity(contentHeight, gravity)
+            provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation)
         } else if (provider.type == tappableElement()) {
             provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
         } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) {
@@ -275,6 +278,30 @@
     }
 
     /**
+     * Calculate the [Insets] for taskbar after a rotation, specifically for any potential cutouts
+     * in the screen that can come from the camera.
+     */
+    private fun getInsetsForGravityWithCutout(inset: Int, gravity: Int, rot: Int): Insets {
+        val display = context.display
+        // If there is no cutout, fall back to the original method of calculating insets
+        val cutout = display.cutout ?: return getInsetsForGravity(inset, gravity)
+        val rotation = display.rotation
+        val info = DisplayInfo()
+        display.getDisplayInfo(info)
+        val rotatedCutout = cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, rot)
+
+        if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
+            return Insets.of(0, 0, 0, maxOf(inset, rotatedCutout.safeInsetBottom))
+        }
+
+        // TODO(b/230394142): seascape
+        val isSeascape = (gravity and Gravity.START) == Gravity.START
+        val leftInset = if (isSeascape) maxOf(inset, rotatedCutout.safeInsetLeft) else 0
+        val rightInset = if (isSeascape) 0 else maxOf(inset, rotatedCutout.safeInsetRight)
+        return Insets.of(leftInset, 0, rightInset, 0)
+    }
+
+    /**
      * @return [Insets] where the [inset] is either used as a bottom inset or right/left inset if
      *   using 3 button nav
      */
@@ -309,9 +336,11 @@
             controllers.bubbleControllers.isPresent &&
                 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
         var insetsIsTouchableRegion = true
-        if (context.isPhoneButtonNavMode &&
-                (!controllers.navbarButtonsViewController.isImeVisible
-                        || !controllers.navbarButtonsViewController.isImeRenderingNavButtons)) {
+        if (
+            context.isPhoneButtonNavMode &&
+                (!controllers.navbarButtonsViewController.isImeVisible ||
+                    !controllers.navbarButtonsViewController.isImeRenderingNavButtons)
+        ) {
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
             insetsIsTouchableRegion = false
         } else if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index f3db0ee..8db343f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -802,6 +802,11 @@
     private void addJankMonitorListener(
             AnimatorSet animator, boolean expanding, @StashAnimation int animationType) {
         View v = mControllers.taskbarActivityContext.getDragLayer();
+        if (!v.isAttachedToWindow()) {
+            // If the task bar drag layer is not attached to window, we don't need to monitor jank
+            // (actually we can't pass in an unattached view either).
+            return;
+        }
         int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
                 InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
         animator.addListener(new AnimatorListenerAdapter() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index 432d272..9c3e8af 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -40,7 +40,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 /** Root drag layer for the Taskbar overlay window. */
 public class TaskbarOverlayDragLayer extends
@@ -48,28 +47,6 @@
         ViewTreeObserver.OnComputeInternalInsetsListener {
 
     private SafeCloseable mViewCaptureCloseable;
-    private final List<OnClickListener> mOnClickListeners = new CopyOnWriteArrayList<>();
-    private final TouchController mClickListenerTouchController = new TouchController() {
-        @Override
-        public boolean onControllerTouchEvent(MotionEvent ev) {
-            if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
-                for (OnClickListener listener : mOnClickListeners) {
-                    listener.onClick(TaskbarOverlayDragLayer.this);
-                }
-            }
-            return false;
-        }
-
-        @Override
-        public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-            for (int i = 0; i < getChildCount(); i++) {
-                if (isEventOverView(getChildAt(i), ev)) {
-                    return false;
-                }
-            }
-            return true;
-        }
-    };
     private final List<TouchController> mTouchControllers = new ArrayList<>();
 
     TaskbarOverlayDragLayer(Context context) {
@@ -98,9 +75,6 @@
         List<TouchController> controllers = new ArrayList<>();
         controllers.add(mActivity.getDragController());
         controllers.addAll(mTouchControllers);
-        if (!mOnClickListeners.isEmpty()) {
-            controllers.add(mClickListenerTouchController);
-        }
         mControllers = controllers.toArray(new TouchController[0]);
     }
 
@@ -152,51 +126,6 @@
         mActivity.getOverlayController().maybeCloseWindow();
     }
 
-    /**
-     * Adds the given callback to clicks to this drag layer.
-     * <p>
-     * Clicks are only accepted on this drag layer if they fall within this drag layer's bounds and
-     * outside the bounds of all child views.
-     * <p>
-     * If the click falls within the bounds of a child view, then this callback does not run and
-     * that child can optionally handle it.
-     */
-    private void addOnClickListener(@NonNull OnClickListener listener) {
-        boolean wasEmpty = mOnClickListeners.isEmpty();
-        mOnClickListeners.add(listener);
-        if (wasEmpty) {
-            recreateControllers();
-        }
-    }
-
-    /**
-     * Removes the given on click callback.
-     * <p>
-     * No-op if the callback was never added.
-     */
-    private void removeOnClickListener(@NonNull OnClickListener listener) {
-        boolean wasEmpty = mOnClickListeners.isEmpty();
-        mOnClickListeners.remove(listener);
-        if (!wasEmpty && mOnClickListeners.isEmpty()) {
-            recreateControllers();
-        }
-    }
-
-    /**
-     * Queues the given callback on the next click on this drag layer.
-     * <p>
-     * Once run, this callback is immediately removed.
-     */
-    public void runOnClickOnce(@NonNull OnClickListener listener) {
-        addOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                listener.onClick(v);
-                removeOnClickListener(this);
-            }
-        });
-    }
-
     /** Adds a {@link TouchController} to this drag layer. */
     public void addTouchController(@NonNull TouchController touchController) {
         mTouchControllers.add(touchController);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 0650f9d..72218bf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -55,14 +55,12 @@
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.animation.ValueAnimator;
-import android.util.Log;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
@@ -96,8 +94,6 @@
     @Override
     public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
             StateAnimationConfig config) {
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "creating animation fromState: "
-                + fromState + " toState: " + toState);
         RecentsView overview = mActivity.getOverviewPanel();
         if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) {
             overview.switchToScreenshot(() ->
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 879312d..2341e4c 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -425,11 +425,14 @@
             if (activity == null) {
                 return null;
             }
+            RecentsView recentsView = activity.getOverviewPanel();
             STATE_TYPE state = stateFromGestureEndTarget(endTarget);
             ScrimView scrimView = activity.getScrimView();
             ObjectAnimator anim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
                     getOverviewScrimColorForState(activity, state));
             anim.setDuration(duration);
+            anim.setInterpolator(recentsView == null || !recentsView.isKeyboardTaskFocusPending()
+                    ? LINEAR : INSTANT);
             return anim;
         }
         return null;
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 9bb9775a..a27875a 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -31,7 +31,6 @@
 import android.content.Context;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
@@ -45,7 +44,6 @@
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.PendingSplitSelectInfo;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -187,8 +185,6 @@
     @Override
     public void setOverviewStateEnabled(boolean enabled) {
         super.setOverviewStateEnabled(enabled);
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "overview state enabled state has changed: "
-                + enabled);
         if (enabled) {
             LauncherState state = mActivity.getStateManager().getState();
             boolean hasClearAllButton = (state.getVisibleElements(mActivity)
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
index 87cbdd1..02a49a3 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
@@ -1,12 +1,12 @@
 package com.android.launcher3.taskbar.navbutton
 
+import android.content.res.Configuration
 import android.content.res.Resources
 import android.view.Surface
 import android.view.Surface.ROTATION_270
 import android.view.Surface.Rotation
 import android.view.View
 import android.view.ViewGroup
-import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.Space
@@ -14,7 +14,9 @@
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION
-import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_CONTEXTUAL_BUTTONS
+import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_NAV_BUTTONS
+import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_START_CONTEXTUAL_BUTTONS
 import com.android.systemui.shared.rotation.RotationButton
 import java.lang.IllegalStateException
 import org.junit.Assume.assumeTrue
@@ -40,6 +42,7 @@
     private val mockRotationButton: RotationButton = mock()
     private val mockA11yButton: ImageView = mock()
     private val mockSpace: Space = mock()
+    private val mockConfiguration: Configuration = mock();
 
     private var surfaceRotation = Surface.ROTATION_0
 
@@ -52,12 +55,15 @@
         whenever(mockNavLayout.findViewById<View>(R.id.recent_apps)).thenReturn(mockRecentsButton)
 
         // Init top level layout
-        whenever(mockParentButtonContainer.findViewById<LinearLayout>(R.id.end_nav_buttons))
+        whenever(mockParentButtonContainer.requireViewById<LinearLayout>(ID_END_NAV_BUTTONS))
             .thenReturn(mockNavLayout)
-        whenever(mockParentButtonContainer.findViewById<ViewGroup>(R.id.end_contextual_buttons))
+        whenever(mockParentButtonContainer.requireViewById<ViewGroup>(ID_END_CONTEXTUAL_BUTTONS))
             .thenReturn(mockEndContextualLayout)
-        whenever(mockParentButtonContainer.findViewById<ViewGroup>(R.id.start_contextual_buttons))
+        whenever(mockParentButtonContainer.requireViewById<ViewGroup>(ID_START_CONTEXTUAL_BUTTONS))
             .thenReturn(mockStartContextualLayout)
+        whenever(mockBackButton.resources).thenReturn(mockResources)
+        whenever(mockResources.configuration).thenReturn(mockConfiguration)
+        whenever(mockConfiguration.layoutDirection).thenReturn(0)
     }
 
     @Test
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 496cb4e..2b203e1 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -258,7 +258,7 @@
 import com.android.systemui.plugins.LauncherOverlayPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.shared.LauncherOverlayManager;
-import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy;
 import com.android.wm.shell.Flags;
 
 import java.io.FileDescriptor;
@@ -2810,7 +2810,7 @@
     /**
      * Call this after onCreate to set or clear overlay.
      */
-    public void setLauncherOverlay(LauncherOverlay overlay) {
+    public void setLauncherOverlay(LauncherOverlayTouchProxy overlay) {
         mWorkspace.setLauncherOverlay(overlay);
     }
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index ca83245..1fede56 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1140,7 +1140,7 @@
             mEdgeGlowLeft.onPullDistance(0f, 1f - displacement);
         }
         if (!mEdgeGlowRight.isFinished()) {
-            mEdgeGlowRight.onPullDistance(0f, displacement);
+            mEdgeGlowRight.onPullDistance(0f, displacement, ev);
         }
     }
 
@@ -1320,10 +1320,10 @@
                     int consumed = 0;
                     if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) {
                         consumed = Math.round(size *
-                                mEdgeGlowRight.onPullDistance(delta / size, displacement));
+                                mEdgeGlowRight.onPullDistance(delta / size, displacement, ev));
                     } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) {
                         consumed = Math.round(-size *
-                                mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement));
+                                mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement, ev));
                     }
                     delta -= consumed;
                 }
@@ -1341,14 +1341,14 @@
                         final float pulledToX = oldScroll + delta;
 
                         if (pulledToX < mMinScroll) {
-                            mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement);
+                            mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement, ev);
                             if (!mEdgeGlowRight.isFinished()) {
-                                mEdgeGlowRight.onRelease();
+                                mEdgeGlowRight.onRelease(ev);
                             }
                         } else if (pulledToX > mMaxScroll) {
-                            mEdgeGlowRight.onPullDistance(delta / size, displacement);
+                            mEdgeGlowRight.onPullDistance(delta / size, displacement, ev);
                             if (!mEdgeGlowLeft.isFinished()) {
-                                mEdgeGlowLeft.onRelease();
+                                mEdgeGlowLeft.onRelease(ev);
                             }
                         }
 
@@ -1356,7 +1356,6 @@
                             postInvalidateOnAnimation();
                         }
                     }
-
                 } else {
                     awakenScrollBars();
                 }
@@ -1456,10 +1455,11 @@
                     }
                     invalidate();
                 }
+                mEdgeGlowLeft.onFlingVelocity(velocity);
+                mEdgeGlowRight.onFlingVelocity(velocity);
             }
-
-            mEdgeGlowLeft.onRelease();
-            mEdgeGlowRight.onRelease();
+            mEdgeGlowLeft.onRelease(ev);
+            mEdgeGlowRight.onRelease(ev);
             // End any intermediate reordering states
             resetTouchState();
             break;
@@ -1468,8 +1468,8 @@
             if (mIsBeingDragged) {
                 runOnPageScrollsInitialized(this::snapToDestination);
             }
-            mEdgeGlowLeft.onRelease();
-            mEdgeGlowRight.onRelease();
+            mEdgeGlowLeft.onRelease(ev);
+            mEdgeGlowRight.onRelease(ev);
             resetTouchState();
             break;
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index ac0d7ce..86f31a1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -125,8 +125,8 @@
 import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
 import com.android.launcher3.widget.util.WidgetSizes;
-import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -1237,7 +1237,7 @@
         mLauncher.onPageEndTransition();
     }
 
-    public void setLauncherOverlay(LauncherOverlay overlay) {
+    public void setLauncherOverlay(LauncherOverlayTouchProxy overlay) {
         final EdgeEffectCompat newEffect;
         if (overlay == null) {
             newEffect = new EdgeEffectCompat(getContext());
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index e8b3066..e6e7133 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -38,7 +38,6 @@
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
 import com.android.launcher3.states.StateAnimationConfig.AnimationPropertyFlags;
-import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -52,6 +51,8 @@
 public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
 
     public static final String TAG = "StateManager";
+    // b/279059025
+    private static final boolean DEBUG = true;
 
     private final AnimationState mConfig = new AnimationState();
     private final Handler mUiHandler;
@@ -231,15 +232,18 @@
 
     private void goToState(
             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
-        String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
-        String truncatedTrace =
-                Arrays.stream(stackTrace.split("\\n"))
-                    .limit(5)
-                    .skip(1) // Removes the line "java.lang.Exception: tracing state transition"
-                    .filter(traceLine -> !traceLine.contains("StateManager.goToState"))
-                    .collect(Collectors.joining("\n"));
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME,
-                "go to state " + state + " partial trace:\n" + truncatedTrace);
+        if (DEBUG) {
+            String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
+            String truncatedTrace =
+                    Arrays.stream(stackTrace.split("\\n"))
+                            .limit(5)
+                            .skip(1) // Removes the line "java.lang.Exception: tracing state
+                            // transition"
+                            .filter(traceLine -> !traceLine.contains("StateManager.goToState"))
+                            .collect(Collectors.joining("\n"));
+            Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state
+                    + ", partial trace:\n" + truncatedTrace);
+        }
 
         animated &= areAnimatorsEnabled();
         if (mActivity.isInState(state)) {
@@ -324,6 +328,20 @@
      */
     public AnimatorSet createAtomicAnimation(
             STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
+        if (DEBUG) {
+            String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
+            String truncatedTrace =
+                    Arrays.stream(stackTrace.split("\\n"))
+                            .limit(5)
+                            .skip(1) // Removes the line "java.lang.Exception: tracing state
+                            // transition"
+                            .filter(traceLine -> !traceLine.contains(
+                                    "StateManager.createAtomicAnimation"))
+                            .collect(Collectors.joining("\n"));
+            Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState
+                    + ", partial trace:\n" + truncatedTrace);
+        }
+
         PendingAnimation builder = new PendingAnimation(config.duration);
         prepareForAtomicAnimation(fromState, toState, config);
 
@@ -395,8 +413,9 @@
         mState = state;
         mActivity.onStateSetStart(mState);
 
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying listeners for state transition start"
-                + " to state: " + state.toString());
+        if (DEBUG) {
+            Log.d(TAG, "onStateTransitionStart - state: " + state);
+        }
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionStart(state);
         }
@@ -414,8 +433,9 @@
             setRestState(null);
         }
 
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying " + mListeners.size() + " listeners "
-                + "for end transition for state: " + state.toString());
+        if (DEBUG) {
+            Log.d(TAG, "onStateTransitionEnd - state: " + state);
+        }
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionComplete(state);
         }
@@ -453,7 +473,9 @@
      * Cancels the current animation.
      */
     public void cancelAnimation() {
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "current animation cancelled");
+        if (DEBUG && mConfig.currentAnimation != null) {
+            Log.d(TAG, "cancelAnimation - with ongoing animation");
+        }
         mConfig.reset();
         // It could happen that a new animation is set as a result of an endListener on the
         // existing animation.
@@ -485,7 +507,6 @@
      * @param toState The state we are animating towards.
      */
     public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "setting animation to " + toState.toString());
         cancelAnimation();
         setCurrentAnimation(anim);
         anim.addListener(createStateAnimationListener(toState));
diff --git a/src/com/android/launcher3/util/EdgeEffectCompat.java b/src/com/android/launcher3/util/EdgeEffectCompat.java
index 491582b..ca37259 100644
--- a/src/com/android/launcher3/util/EdgeEffectCompat.java
+++ b/src/com/android/launcher3/util/EdgeEffectCompat.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.util;
 
 import android.content.Context;
+import android.view.MotionEvent;
 import android.widget.EdgeEffect;
 
 import com.android.launcher3.Utilities;
@@ -43,4 +44,14 @@
             return deltaDistance;
         }
     }
+
+    public float onPullDistance(float deltaDistance, float displacement, MotionEvent ev) {
+        return onPullDistance(deltaDistance, displacement);
+    }
+
+    public void onFlingVelocity(int velocity) { }
+
+    public void onRelease(MotionEvent ev) {
+        onRelease();
+    }
 }
diff --git a/src/com/android/launcher3/util/OverlayEdgeEffect.java b/src/com/android/launcher3/util/OverlayEdgeEffect.java
index 2ef1e1f..d09d801 100644
--- a/src/com/android/launcher3/util/OverlayEdgeEffect.java
+++ b/src/com/android/launcher3/util/OverlayEdgeEffect.java
@@ -17,10 +17,13 @@
 
 import android.content.Context;
 import android.graphics.Canvas;
+import android.os.SystemClock;
+import android.view.MotionEvent;
 import android.widget.EdgeEffect;
 
+import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Utilities;
-import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy;
 
 /**
  * Extension of {@link EdgeEffect} which shows the Launcher overlay
@@ -28,11 +31,11 @@
 public class OverlayEdgeEffect extends EdgeEffectCompat {
 
     protected float mDistance;
-    protected final LauncherOverlay mOverlay;
+    protected final LauncherOverlayTouchProxy mOverlay;
     protected boolean mIsScrolling;
     protected final boolean mIsRtl;
 
-    public OverlayEdgeEffect(Context context, LauncherOverlay overlay) {
+    public OverlayEdgeEffect(Context context, LauncherOverlayTouchProxy overlay) {
         super(context);
         mOverlay = overlay;
         mIsRtl = Utilities.isRtl(context.getResources());
@@ -44,12 +47,30 @@
     }
 
     public float onPullDistance(float deltaDistance, float displacement) {
+        // Fallback implementation, will never actually get called
+        if (BuildConfig.IS_DEBUG_DEVICE) {
+            throw new RuntimeException("Wrong method called");
+        }
+        MotionEvent mv = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+                MotionEvent.ACTION_MOVE, displacement, 0, 0);
+        try {
+            return onPullDistance(deltaDistance, displacement, mv);
+        } finally {
+            mv.recycle();
+        }
+    }
+
+    @Override
+    public float onPullDistance(float deltaDistance, float displacement, MotionEvent ev) {
         mDistance = Math.max(0f, deltaDistance + mDistance);
         if (!mIsScrolling) {
-            mOverlay.onScrollInteractionBegin();
+            int originalAction = ev.getAction();
+            ev.setAction(MotionEvent.ACTION_DOWN);
+            mOverlay.onOverlayMotionEvent(ev, 0);
+            ev.setAction(originalAction);
             mIsScrolling = true;
         }
-        mOverlay.onScrollChange(mDistance, mIsRtl);
+        mOverlay.onOverlayMotionEvent(ev, mDistance);
         return mDistance > 0 ? deltaDistance : 0;
     }
 
@@ -63,9 +84,30 @@
 
     @Override
     public void onRelease() {
+        // Fallback implementation, will never actually get called
+        if (BuildConfig.IS_DEBUG_DEVICE) {
+            throw new RuntimeException("Wrong method called");
+        }
+        MotionEvent mv = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+                MotionEvent.ACTION_UP, mDistance, 0, 0);
+        onRelease(mv);
+        mv.recycle();
+    }
+
+    @Override
+    public void onFlingVelocity(int velocity) {
+        mOverlay.onFlingVelocity(velocity);
+    }
+
+    @Override
+    public void onRelease(MotionEvent ev) {
         if (mIsScrolling) {
+            int originalAction = ev.getAction();
+            ev.setAction(MotionEvent.ACTION_UP);
+            mOverlay.onOverlayMotionEvent(ev, mDistance);
+            ev.setAction(originalAction);
+
             mDistance = 0;
-            mOverlay.onScrollInteractionEnd();
             mIsScrolling = false;
         }
     }
diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
index 54cc0bc..a940774 100644
--- a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.plugins.shared;
 
+import android.view.MotionEvent;
+
 import java.io.PrintWriter;
 
 /**
@@ -47,7 +49,11 @@
 
     default void onActivityDestroyed() { }
 
-    interface LauncherOverlay {
+    /**
+     * @deprecated use LauncherOverlayTouchProxy directly
+     */
+    @Deprecated
+    interface LauncherOverlay extends LauncherOverlayTouchProxy {
 
         /**
          * Touch interaction leading to overscroll has begun
@@ -70,6 +76,38 @@
          * @param callbacks A set of callbacks provided by Launcher in relation to the overlay
          */
         void setOverlayCallbacks(LauncherOverlayCallbacks callbacks);
+
+        @Override
+        default void onFlingVelocity(float velocity) { }
+
+        @Override
+        default void onOverlayMotionEvent(MotionEvent ev, float scrollProgress) {
+            switch (ev.getAction()) {
+                case MotionEvent.ACTION_DOWN ->  onScrollInteractionBegin();
+                case MotionEvent.ACTION_MOVE -> onScrollChange(scrollProgress, false);
+                case MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> onScrollInteractionEnd();
+            }
+
+        }
+    }
+
+    interface LauncherOverlayTouchProxy {
+
+        /**
+         * Called just before finishing scroll interaction to indicate the fling velocity
+         */
+        void onFlingVelocity(float velocity);
+
+        /**
+         * Called to dispatch various motion events to the overlay
+         */
+        void onOverlayMotionEvent(MotionEvent ev, float scrollProgress);
+
+        /**
+         * Called when the launcher is ready to use the overlay
+         * @param callbacks A set of callbacks provided by Launcher in relation to the overlay
+         */
+        default void setOverlayCallbacks(LauncherOverlayCallbacks callbacks) { }
     }
 
     interface LauncherOverlayCallbacks {
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 8d40ff2..c750c7e 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -167,7 +167,6 @@
     public static final String TWO_NEXUS_LAUNCHER_ACTIVITY_WHILE_UNLOCKING = "b/273347463";
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
     public static final String ICON_MISSING = "b/282963545";
-    public static final String OVERVIEW_OVER_HOME = "b/279059025";
 
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 8eebdb2..5ef63da 100644
--- a/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -14,6 +14,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
 
 public class PortraitLandscapeRunner implements TestRule {
     private static final String TAG = "PortraitLandscapeRunner";
@@ -49,7 +50,8 @@
 
                     mTest.mDevice.pressHome();
                     mTest.waitForLauncherCondition("Launcher activity wasn't created",
-                            launcher -> launcher != null);
+                            launcher -> launcher != null,
+                            TimeUnit.SECONDS.toMillis(20));
 
                     mTest.executeOnLauncher(launcher ->
                             launcher.getRotationHelper().forceAllowRotationForTesting(