Merge "Improve spaces for folders" into tm-qpr-dev
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index 7292c44..f5a8253 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -38,6 +38,7 @@
     name: "launcher3-quickstep-oop-tests-src",
     path: "tests",
     srcs: [
+        "tests/src/com/android/quickstep/TaskbarModeSwitchRule.java",
         "tests/src/com/android/quickstep/NavigationModeSwitchRule.java",
         "tests/src/com/android/quickstep/AbstractQuickStepTest.java",
         "tests/src/com/android/quickstep/TaplTestsQuickstep.java",
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 3225078..6e3fd32 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -288,6 +288,8 @@
     <dimen name="transient_taskbar_margin">24dp</dimen>
     <dimen name="transient_taskbar_shadow_blur">40dp</dimen>
     <dimen name="transient_taskbar_key_shadow_distance">10dp</dimen>
+    <dimen name="transient_taskbar_stashed_size">32dp</dimen>
+    <dimen name="transient_taskbar_icon_spacing">10dp</dimen>
     <!-- Taskbar swipe up thresholds -->
     <dimen name="taskbar_app_window_threshold">150dp</dimen>
     <dimen name="taskbar_home_overview_threshold">225dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index c9e42b7..89f0f98 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -26,7 +26,6 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.SparseArray;
-import android.view.MotionEvent;
 import android.view.TaskTransitionSpec;
 import android.view.WindowManagerGlobal;
 
@@ -196,15 +195,6 @@
         return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
     }
 
-    /**
-     * @param ev MotionEvent in screen coordinates.
-     * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
-     */
-    public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
-        return mControllers.taskbarViewController.isEventOverAnyItem(ev)
-                || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
-    }
-
     public boolean isDraggingItem() {
         return mControllers.taskbarDragController.isDragging();
     }
@@ -356,6 +346,11 @@
     }
 
     @Override
+    public boolean isIconAlignedWithHotseat() {
+        return mTaskbarLauncherStateController.isIconAlignedWithHotseat();
+    }
+
+    @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         super.dumpLogs(prefix, pw);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index fad9ff4..d0256c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -23,6 +23,7 @@
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
+import static com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN;
@@ -304,7 +305,8 @@
         int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_SLIPPERY
                 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
-        if (DisplayController.isTransientTaskbar(this)) {
+        if (DisplayController.isTransientTaskbar(this)
+                && !IS_RUNNING_IN_TEST_HARNESS) {
             windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
         }
@@ -858,12 +860,25 @@
     }
 
     /**
+     * Enables the auto timeout for taskbar stashing. This method should only be used for taskbar
+     * testing.
+     */
+    @VisibleForTesting
+    public void enableBlockingTimeoutDuringTests(boolean enableBlockingTimeout) {
+        mControllers.taskbarStashController.enableBlockingTimeoutDuringTests(enableBlockingTimeout);
+    }
+
+    /**
      * Unstashes the Taskbar if it is stashed. This method should only be used to unstash the
      * taskbar at the end of a test.
      */
     @VisibleForTesting
     public void unstashTaskbarIfStashed() {
-        mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
+        if (DisplayController.isTransientTaskbar(this)) {
+            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
+        } else {
+            mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
+        }
     }
 
     protected boolean isUserSetupComplete() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index abd467d..a948fb3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -44,8 +44,12 @@
     private var keyShadowDistance = 0f
     private var bottomMargin = 0
 
-    private val leftCornerRadius = context.leftCornerRadius.toFloat()
-    private val rightCornerRadius = context.rightCornerRadius.toFloat()
+    private val fullLeftCornerRadius = context.leftCornerRadius.toFloat()
+    private val fullRightCornerRadius = context.rightCornerRadius.toFloat()
+    private var leftCornerRadius = fullLeftCornerRadius
+    private var rightCornerRadius = fullRightCornerRadius
+    private val square: Path = Path()
+    private val circle: Path = Path()
     private val invertedLeftCornerPath: Path = Path()
     private val invertedRightCornerPath: Path = Path()
 
@@ -63,13 +67,29 @@
             keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
         }
 
+        setCornerRoundness(DEFAULT_ROUNDNESS)
+    }
+
+    /**
+     * Sets the roundness of the round corner above Taskbar. No effect on transient Taskkbar.
+     * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+     */
+    fun setCornerRoundness(cornerRoundness: Float) {
+        if (isTransientTaskbar && !transientBackgroundBounds.isEmpty) {
+            return
+        }
+
+        leftCornerRadius = fullLeftCornerRadius * cornerRoundness
+        rightCornerRadius = fullRightCornerRadius * cornerRoundness
+
         // Create the paths for the inverted rounded corners above the taskbar. Start with a filled
         // square, and then subtract out a circle from the appropriate corner.
-        val square = Path()
+        square.reset()
         square.addRect(0f, 0f, leftCornerRadius, leftCornerRadius, Path.Direction.CW)
-        val circle = Path()
+        circle.reset()
         circle.addCircle(leftCornerRadius, 0f, leftCornerRadius, Path.Direction.CW)
         invertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE)
+
         square.reset()
         square.addRect(0f, 0f, rightCornerRadius, rightCornerRadius, Path.Direction.CW)
         circle.reset()
@@ -121,4 +141,8 @@
 
         canvas.restore()
     }
+
+    companion object {
+        const val DEFAULT_ROUNDNESS = 1f
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 9c2d21e..12e6ba4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -23,6 +23,7 @@
 
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
+import com.android.quickstep.AnimatedFloat;
 import com.android.systemui.shared.rotation.RotationButtonController;
 
 import java.io.PrintWriter;
@@ -58,6 +59,7 @@
     public final TaskbarOverlayController taskbarOverlayController;
 
     @Nullable private LoggableTaskbarController[] mControllersToLog = null;
+    @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
 
     /** Do not store this controller, as it may change at runtime. */
     @NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT;
@@ -67,6 +69,9 @@
 
     @Nullable private TaskbarSharedState mSharedState = null;
 
+    // Roundness property for round corner above taskbar .
+    private final AnimatedFloat mCornerRoundness = new AnimatedFloat(this::updateCornerRoundness);
+
     public TaskbarControllers(TaskbarActivityContext taskbarActivityContext,
             TaskbarDragController taskbarDragController,
             TaskbarNavButtonController navButtonController,
@@ -148,6 +153,11 @@
                 taskbarAutohideSuspendController, taskbarPopupController, taskbarInsetsController,
                 voiceInteractionWindowController
         };
+        mBackgroundRendererControllers = new BackgroundRendererController[] {
+                taskbarDragLayerController, taskbarScrimViewController,
+                voiceInteractionWindowController
+        };
+        mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS);
 
         mAreAllControllersInitialized = true;
         for (Runnable postInitCallback : mPostInitCallbacks) {
@@ -164,6 +174,7 @@
 
     public void onConfigurationChanged(@Config int configChanges) {
         navbarButtonsViewController.onConfigurationChanged(configChanges);
+        taskbarDragLayerController.onConfigurationChanged();
     }
 
     /**
@@ -191,6 +202,7 @@
         taskbarRecentAppsController.onDestroy();
 
         mControllersToLog = null;
+        mBackgroundRendererControllers = null;
     }
 
     /**
@@ -224,6 +236,23 @@
         rotationButtonController.dumpLogs(prefix + "\t", pw);
     }
 
+    /**
+     * Returns a float property that animates roundness of the round corner above Taskbar.
+     */
+    public AnimatedFloat getTaskbarCornerRoundness() {
+        return mCornerRoundness;
+    }
+
+    private void updateCornerRoundness() {
+        if (mBackgroundRendererControllers == null) {
+            return;
+        }
+
+        for (BackgroundRendererController controller : mBackgroundRendererControllers) {
+            controller.setCornerRoundness(mCornerRoundness.value);
+        }
+    }
+
     @VisibleForTesting
     TaskbarActivityContext getTaskbarActivityContext() {
         // Used to mock
@@ -233,4 +262,12 @@
     protected interface LoggableTaskbarController {
         void dumpLogs(String prefix, PrintWriter pw);
     }
+
+    protected interface BackgroundRendererController {
+        /**
+         * Sets the roundness of the round corner above Taskbar.
+         * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+         */
+        void setCornerRoundness(float cornerRoundness);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 7c9a13c..2aa4687 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -116,9 +116,17 @@
     }
 
     @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 && ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
-            mControllerCallbacks.onActionOutsideEvent();
+        if (mControllerCallbacks != null) {
+            mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
         }
         return super.onTouchEvent(ev);
     }
@@ -158,6 +166,15 @@
         invalidate();
     }
 
+    /**
+     * Sets the roundness of the round corner above Taskbar.
+     * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+     */
+    protected void setCornerRoundness(float cornerRoundness) {
+        mBackgroundRenderer.setCornerRoundness(cornerRoundness);
+        invalidate();
+    }
+
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 13ecf81..3834ba7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -18,10 +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.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DimensionUtils;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
@@ -32,11 +34,13 @@
 /**
  * Handles properties/data collection, then passes the results to TaskbarDragLayer to render.
  */
-public class TaskbarDragLayerController implements TaskbarControllers.LoggableTaskbarController {
+public class TaskbarDragLayerController implements TaskbarControllers.LoggableTaskbarController,
+        TaskbarControllers.BackgroundRendererController {
 
     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);
@@ -63,6 +67,7 @@
         mTaskbarDragLayer = taskbarDragLayer;
         final Resources resources = mTaskbarDragLayer.getResources();
         mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+        updateGestureHeight();
     }
 
     public void init(TaskbarControllers controllers) {
@@ -122,6 +127,19 @@
         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();
+    }
+
     private void updateBackgroundAlpha() {
         final float bgNavbar = mBgNavbar.value;
         final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
@@ -138,6 +156,11 @@
         updateNavBarDarkIntensityMultiplier();
     }
 
+    @Override
+    public void setCornerRoundness(float cornerRoundness) {
+        mTaskbarDragLayer.setCornerRoundness(cornerRoundness);
+    }
+
     private void updateNavBarDarkIntensityMultiplier() {
         // Zero out the app-requested dark intensity when we're drawing our own background.
         float effectiveBgAlpha = mLastSetBackgroundAlpha * (1 - mBgOffset.value);
@@ -158,6 +181,8 @@
      */
     public class TaskbarDragLayerCallbacks {
 
+        private final int[] mTempOutLocation = new int[2];
+
         /**
          * Called to update the touchable insets.
          * @see ViewTreeObserver.InternalInsetsInfo#setTouchableInsets(int)
@@ -167,9 +192,9 @@
         }
 
         /**
-         * Called whenever TaskbarDragLayer receives an ACTION_OUTSIDE event.
+         * Listens to TaskbarDragLayer touch events and responds accordingly.
          */
-        public void onActionOutsideEvent() {
+        public void tryStashBasedOnMotionEvent(MotionEvent ev) {
             if (!DisplayController.isTransientTaskbar(mActivity)) {
                 return;
             }
@@ -177,7 +202,24 @@
                 return;
             }
 
-            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
+            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);
+            }
         }
 
         /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index bc5bcf5..fc26f5f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -38,9 +38,8 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
@@ -74,6 +73,7 @@
 
     private TaskbarControllers mControllers;
     private AnimatedFloat mTaskbarBackgroundAlpha;
+    private AnimatedFloat mTaskbarCornerRoundness;
     private MultiProperty mIconAlphaForHome;
     private QuickstepLauncher mLauncher;
 
@@ -134,6 +134,7 @@
 
         mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
                 .getTaskbarBackgroundAlpha();
+        mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
         mIconAlphaForHome = mControllers.taskbarViewController
                 .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);
 
@@ -251,17 +252,7 @@
 
     private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
         boolean goingToLauncher = isInLauncher();
-        final float toAlignment;
-        if (goingToLauncher) {
-            boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
-            boolean willStashVisually = isInStashedState
-                    && mControllers.taskbarStashController.supportsVisualStashing();
-            boolean isTaskbarAlignedWithHotseat =
-                    mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
-            toAlignment = isTaskbarAlignedWithHotseat && !willStashVisually ? 1 : 0;
-        } else {
-            toAlignment = 0;
-        }
+        final float toAlignment = isIconAlignedWithHotseat() ? 1 : 0;
         if (DEBUG) {
             Log.d(TAG, "onStateChangeApplied - mState: " + getStateString(mState)
                     + ", changedFlags: " + getStateString(changedFlags)
@@ -327,6 +318,19 @@
                     .setDuration(duration));
         }
 
+        float cornerRoundness = goingToLauncher ? 0 : 1;
+        // Don't animate if corner roundness has reached desired value.
+        if (mTaskbarCornerRoundness.isAnimating()
+                || mTaskbarCornerRoundness.value != cornerRoundness) {
+            mTaskbarCornerRoundness.cancelAnimation();
+            if (DEBUG) {
+                Log.d(TAG, "onStateChangeApplied - taskbarCornerRoundness - "
+                        + mTaskbarCornerRoundness.value
+                        + " -> " + cornerRoundness + ": " + duration);
+            }
+            animatorSet.play(mTaskbarCornerRoundness.animateToValue(cornerRoundness));
+        }
+
         if (mIconAlignment.isAnimatingToValue(toAlignment)
                 || mIconAlignment.isSettledOnValue(toAlignment)) {
             // Already at desired value, but make sure we run the callback at the end.
@@ -344,6 +348,7 @@
             }
             animatorSet.play(iconAlignAnim);
         }
+
         animatorSet.setInterpolator(EMPHASIZED);
 
         if (start) {
@@ -357,6 +362,22 @@
         return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
     }
 
+    /**
+     * Returns if icons should be aligned to hotseat in the current transition
+     */
+    public boolean isIconAlignedWithHotseat() {
+        if (isInLauncher()) {
+            boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
+            boolean willStashVisually = isInStashedState
+                    && mControllers.taskbarStashController.supportsVisualStashing();
+            boolean isTaskbarAlignedWithHotseat =
+                    mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
+            return isTaskbarAlignedWithHotseat && !willStashVisually;
+        } else {
+            return false;
+        }
+    }
+
     private void playStateTransitionAnim(AnimatorSet animatorSet, long duration,
             boolean committed) {
         boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
@@ -395,7 +416,7 @@
                 || (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0);
 
         mControllers.taskbarViewController.setLauncherIconAlignment(
-                mIconAlignment.value, mIconAlignment.getEndValue(), mLauncher.getDeviceProfile());
+                mIconAlignment.value, mLauncher.getDeviceProfile());
         mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value);
         // Switch taskbar and hotseat in last frame
         updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java
index 1d3757f..cdc6d59 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java
@@ -69,4 +69,13 @@
         mRenderer.getPaint().setAlpha((int) (alpha * 255));
         invalidate();
     }
+
+    /**
+     * Sets the roundness of the round corner above Taskbar.
+     * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+     */
+    protected void setCornerRoundness(float cornerRoundness) {
+        mRenderer.setCornerRoundness(cornerRoundness);
+        invalidate();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index c3b0f57..ce191b7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -30,7 +30,8 @@
 /**
  * Handles properties/data collection, and passes the results to {@link TaskbarScrimView} to render.
  */
-public class TaskbarScrimViewController implements TaskbarControllers.LoggableTaskbarController {
+public class TaskbarScrimViewController implements TaskbarControllers.LoggableTaskbarController,
+        TaskbarControllers.BackgroundRendererController {
 
     private static final float SCRIM_ALPHA = 0.6f;
 
@@ -95,6 +96,11 @@
     }
 
     @Override
+    public void setCornerRoundness(float cornerRoundness) {
+        mScrimView.setCornerRoundness(cornerRoundness);
+    }
+
+    @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarScrimViewController:");
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index afd659f..b53e9c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -37,6 +37,7 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.launcher3.Alarm;
@@ -169,6 +170,7 @@
     private boolean mEnableManualStashingDuringTests = false;
 
     private final Alarm mTimeoutAlarm = new Alarm();
+    private boolean mEnableBlockingTimeoutDuringTests = false;
 
     // Evaluate whether the handle should be stashed
     private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
@@ -190,8 +192,13 @@
         if (isPhoneMode()) {
             // DeviceProfile's taskbar vars aren't initialized w/ the flag off
             Resources resources = mActivity.getResources();
-            mUnstashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_size);
-            mStashedHeight = resources.getDimensionPixelOffset(R.dimen.taskbar_stashed_size);
+            boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
+            mUnstashedHeight = resources.getDimensionPixelSize(isTransientTaskbar
+                    ? R.dimen.transient_taskbar_size
+                    : R.dimen.taskbar_size);
+            mStashedHeight = resources.getDimensionPixelSize(isTransientTaskbar
+                    ? R.dimen.transient_taskbar_stashed_size
+                    : R.dimen.taskbar_stashed_size);
         } else {
             mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize;
             mStashedHeight = mActivity.getDeviceProfile().stashedTaskbarSize;
@@ -262,11 +269,21 @@
      * Enables support for manual stashing. This should only be used to add this functionality
      * to Launcher specific tests.
      */
+    @VisibleForTesting
     public void enableManualStashingDuringTests(boolean enableManualStashing) {
         mEnableManualStashingDuringTests = enableManualStashing;
     }
 
     /**
+     * Enables the auto timeout for taskbar stashing. This method should only be used for taskbar
+     * testing.
+     */
+    @VisibleForTesting
+    public void enableBlockingTimeoutDuringTests(boolean enableBlockingTimeout) {
+        mEnableBlockingTimeoutDuringTests = enableBlockingTimeout;
+    }
+
+    /**
      * Sets the flag indicating setup UI is visible
      */
     protected void setSetupUIVisible(boolean isVisible) {
@@ -344,7 +361,8 @@
      * @see WindowInsets.Type#systemBars()
      */
     public int getContentHeightToReportToApps() {
-        if (isPhoneMode() && !mActivity.isThreeButtonNav()) {
+        if ((isPhoneMode() && !mActivity.isThreeButtonNav())
+                || DisplayController.isTransientTaskbar(mActivity)) {
             return getStashedHeight();
         }
 
@@ -840,12 +858,12 @@
      * Attempts to start timer to auto hide the taskbar based on time.
      */
     public void tryStartTaskbarTimeout() {
-        if (!DisplayController.isTransientTaskbar(mActivity)) {
+        if (!DisplayController.isTransientTaskbar(mActivity)
+                || mIsStashed
+                || mEnableBlockingTimeoutDuringTests) {
             return;
         }
-        if (mIsStashed) {
-            return;
-        }
+
         cancelTimeoutIfExists();
 
         mTimeoutAlarm.setOnAlarmListener(this::onTaskbarTimeout);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 2294306..1152126 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -114,6 +114,13 @@
                 || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
     }
 
+    /**
+     * Returns true if icons should be aligned to hotseat in the current transition.
+     */
+    public boolean isIconAlignedWithHotseat() {
+        return false;
+    }
+
     @CallSuper
     protected void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(String.format(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index ee87185..a73528b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -280,10 +280,9 @@
      *                       0 => not aligned
      *                       1 => fully aligned
      */
-    public void setLauncherIconAlignment(float alignmentRatio, Float endAlignment,
-            DeviceProfile launcherDp) {
+    public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) {
         if (mIconAlignControllerLazy == null) {
-            mIconAlignControllerLazy = createIconAlignmentController(launcherDp, endAlignment);
+            mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
         }
         mIconAlignControllerLazy.setPlayFraction(alignmentRatio);
         if (alignmentRatio <= 0 || alignmentRatio >= 1) {
@@ -295,8 +294,7 @@
     /**
      * Creates an animation for aligning the taskbar icons with the provided Launcher device profile
      */
-    private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp,
-            Float endAlignment) {
+    private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
         mOnControllerPreCreateCallback.run();
         PendingAnimation setter = new PendingAnimation(100);
         DeviceProfile taskbarDp = mActivity.getDeviceProfile();
@@ -322,7 +320,7 @@
         setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight(
                 anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight));
 
-        boolean isToHome = endAlignment != null && endAlignment == 1;
+        boolean isToHome = mControllers.uiController.isIconAlignedWithHotseat();
         for (int i = 0; i < mTaskbarView.getChildCount(); i++) {
             View child = mTaskbarView.getChildAt(i);
             int positionInHotseat;
diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
index 837af58..a033507 100644
--- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
@@ -14,7 +14,8 @@
  * Controls Taskbar behavior while Voice Interaction Window (assistant) is showing.
  */
 class VoiceInteractionWindowController(val context: TaskbarActivityContext)
-    : TaskbarControllers.LoggableTaskbarController {
+    : TaskbarControllers.LoggableTaskbarController,
+        TaskbarControllers.BackgroundRendererController {
 
     private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context)
 
@@ -111,6 +112,11 @@
         }
     }
 
+    override fun setCornerRoundness(cornerRoundness: Float) {
+        taskbarBackgroundRenderer.setCornerRoundness(cornerRoundness)
+        separateWindowForTaskbarBackground.invalidate()
+    }
+
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
         pw.println(prefix + "VoiceInteractionWindowController:")
         pw.println("$prefix\tisVoiceInteractionWindowVisible=$isVoiceInteractionWindowVisible")
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b228fdb..9813c8a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -219,6 +219,8 @@
 
         mEnableWidgetDepth = ENABLE_WIDGET_PICKER_DEPTH.get()
                 && SystemProperties.getBoolean("ro.launcher.depth.widget", true);
+        getWorkspace().addOverlayCallback(progress ->
+                onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX));
     }
 
     @Override
@@ -594,17 +596,6 @@
         recentsView.finishRecentsAnimation(true /* toRecents */, null);
     }
 
-    /**
-     * {@code LauncherOverlayCallbacks} scroll amount.
-     * Indicates transition progress to -1 screen.
-     * @param progress From 0 to 1.
-     */
-    @Override
-    public void onScrollChanged(float progress) {
-        super.onScrollChanged(progress);
-        onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX);
-    }
-
     @Override
     public void onAllAppsTransition(float progress) {
         super.onAllAppsTransition(progress);
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index b06b894..5ab3c58 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -98,15 +98,6 @@
         }
     }
 
-    /**
-     * Starts the animation.
-     */
-    public void startAnimation() {
-        if (mValueAnimator != null) {
-            mValueAnimator.start();
-        }
-    }
-
     public void cancelAnimation() {
         if (mValueAnimator != null) {
             mValueAnimator.cancel();
@@ -119,10 +110,6 @@
         }
     }
 
-    public ObjectAnimator getCurrentAnimation() {
-        return mValueAnimator;
-    }
-
     public boolean isAnimating() {
         return mValueAnimator != null;
     }
@@ -140,11 +127,4 @@
     public boolean isSettledOnValue(float endValue) {
         return !isAnimating() && value == endValue;
     }
-
-    /**
-     * Returns the value we are animating to, or {@code null} if we are not currently animating.
-     */
-    public Float getEndValue() {
-        return mEndValue;
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index de150e1..a622326 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -30,6 +30,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -501,45 +502,43 @@
                         "setRecentsAttachedToAppWindow: exiting early"));
                 return;
             }
-            mIsAttachedToWindow = attached;
-            RecentsView recentsView = mActivity.getOverviewPanel();
-            if (attached) {
-                mHasEverAttachedToWindow = true;
-            }
-            Animator fadeAnim = mActivity.getStateManager()
-                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
-
-            float fromTranslation = attached ? 1 : 0;
-            float toTranslation = attached ? 0 : 1;
+            mActivity.getStateManager()
+                    .cancelStateElementAnimation(INDEX_RECENTS_FADE_ANIM);
             mActivity.getStateManager()
                     .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
-            if (!recentsView.isShown() && animate) {
-                ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, fromTranslation);
-                ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                        "setRecentsAttachedToAppWindow: recents view not shown, setting ")
-                        .append("ADJACENT_PAGE_HORIZONTAL_OFFSET to ")
-                        .append(Float.toString(fromTranslation)));
-            } else {
-                fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(recentsView);
-                ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                        "setRecentsAttachedToAppWindow: updating fromTranslation to ")
-                        .append(Float.toString(fromTranslation)));
-            }
+
+            AnimatorSet animatorSet = new AnimatorSet();
+            animatorSet.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    super.onAnimationStart(animation);
+                    mIsAttachedToWindow = attached;
+                    if (attached) {
+                        mHasEverAttachedToWindow = true;
+                    }
+                }});
+
+            long animationDuration = animate ? RECENTS_ATTACH_DURATION : 0;
+            Animator fadeAnim = mActivity.getStateManager()
+                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+            fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+            fadeAnim.setDuration(animationDuration);
+            animatorSet.play(fadeAnim);
+
+            float fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(
+                    mActivity.getOverviewPanel());
+            float toTranslation = attached ? 0 : 1;
             ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
                     "setRecentsAttachedToAppWindow: fromTranslation=")
                     .append(Float.toString(fromTranslation))
                     .append(", toTranslation=")
                     .append(Float.toString(toTranslation)));
-            if (!animate) {
-                ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, toTranslation);
-            } else {
-                mActivity.getStateManager().createStateElementAnimation(
-                        INDEX_RECENTS_TRANSLATE_X_ANIM,
-                        fromTranslation, toTranslation).start();
-            }
 
-            fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
-            fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
+            Animator translationAnimator = mActivity.getStateManager().createStateElementAnimation(
+                    INDEX_RECENTS_TRANSLATE_X_ANIM, fromTranslation, toTranslation);
+            translationAnimator.setDuration(animationDuration);
+            animatorSet.play(translationAnimator);
+            animatorSet.start();
         }
 
         @Override
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 9621ce6..177ecf5 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -120,6 +120,18 @@
                                 .getCurrentActivityContext()
                                 .getTaskbarAllAppsTopPadding());
             }
+
+            case TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT:
+                runOnTISBinder(tisBinder -> {
+                    enableBlockingTimeout(tisBinder, true);
+                });
+                return response;
+
+            case TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT:
+                runOnTISBinder(tisBinder -> {
+                    enableBlockingTimeout(tisBinder, false);
+                });
+                return response;
         }
 
         return super.call(method, arg, extras);
@@ -149,6 +161,13 @@
                 enable);
     }
 
+    private void enableBlockingTimeout(
+            TouchInteractionService.TISBinder tisBinder, boolean enable) {
+        // Allow null-pointer to catch illegal states.
+        tisBinder.getTaskbarManager().getCurrentActivityContext().enableBlockingTimeoutDuringTests(
+                enable);
+    }
+
     /**
      * Runs the given command on the UI thread, after ensuring we are connected to
      * TouchInteractionService.
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 7bcc661..d46565b 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -23,6 +23,8 @@
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
+import android.app.TaskInfo;
+import android.content.ComponentName;
 import android.os.Build;
 import android.os.Process;
 import android.os.RemoteException;
@@ -324,8 +326,14 @@
         writer.println(prefix + "  mChangeId=" + mChangeId);
         writer.println(prefix + "  mResultsUi=[id=" + mResultsUi.mRequestId + ", tasks=");
         for (GroupTask task : mResultsUi) {
-            writer.println(prefix + "    t1=" + task.task1.key.id
-                    + " t2=" + (task.hasMultipleTasks() ? task.task2.key.id : "-1"));
+            Task task1 = task.task1;
+            Task task2 = task.task2;
+            ComponentName cn1 = task1.getTopComponent();
+            ComponentName cn2 = task2 != null ? task2.getTopComponent() : null;
+            writer.println(prefix + "    t1: (id=" + task1.key.id
+                    + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
+                    + " t2: (id=" + (task2 != null ? task2.key.id : "-1")
+                    + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
         }
         writer.println(prefix + "  ]");
         int currentUserId = Process.myUserHandle().getIdentifier();
@@ -333,8 +341,14 @@
                 mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
         writer.println(prefix + "  rawTasks=[");
         for (GroupedRecentTaskInfo task : rawTasks) {
-            writer.println(prefix + "    t1=" + task.getTaskInfo1().taskId
-                    + " t2=" + (task.getTaskInfo2() != null ? task.getTaskInfo2().taskId : "-1"));
+            TaskInfo taskInfo1 = task.getTaskInfo1();
+            TaskInfo taskInfo2 = task.getTaskInfo2();
+            ComponentName cn1 = taskInfo1.topActivity;
+            ComponentName cn2 = taskInfo2 != null ? taskInfo2.topActivity : null;
+            writer.println(prefix + "    t1: (id=" + taskInfo1.taskId
+                    + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
+                    + " t2: (id=" + (taskInfo2 != null ? taskInfo2.taskId : "-1")
+                    + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
         }
         writer.println(prefix + "  ]");
     }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 4cdb5f8..450774b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -55,7 +55,6 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.InputEvent;
@@ -138,9 +137,6 @@
 
     private static final String TAG = "TouchInteractionService";
 
-    private static final boolean BUBBLES_HOME_GESTURE_ENABLED =
-            SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", true);
-
     private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
 
     private final TISBinder mTISBinder = new TISBinder();
@@ -809,27 +805,9 @@
             if (mDeviceState.isBubblesExpanded()) {
                 reasonString = newCompoundString(reasonPrefix)
                         .append(SUBSTRING_PREFIX)
-                        .append("bubbles expanded");
-                if (BUBBLES_HOME_GESTURE_ENABLED) {
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("bubbles can handle the home gesture")
-                            .append(", trying to use default input consumer");
-                    // Bubbles can handle home gesture itself.
-                    base = getDefaultInputConsumer(reasonString);
-                } else {
-                    // If Bubbles is expanded, use the overlay input consumer, which will close
-                    // Bubbles instead of going all the way home when a swipe up is detected.
-                    // Notification panel can be expanded on top of expanded bubbles. Bubbles remain
-                    // expanded in the back. Make sure swipe up is not passed to bubbles in this
-                    // case.
-                    if (!mDeviceState.isNotificationPanelExpanded()) {
-                        reasonString = newCompoundString(reasonPrefix)
-                                .append(SUBSTRING_PREFIX)
-                                .append("using SysUiOverlayInputConsumer");
-                        base = new SysUiOverlayInputConsumer(
-                                getBaseContext(), mDeviceState, mInputMonitorCompat);
-                    }
-                }
+                        .append("bubbles expanded, trying to use default input consumer");
+                // Bubbles can handle home gesture itself.
+                base = getDefaultInputConsumer(reasonString);
             }
 
             if (mDeviceState.isSystemUiDialogShowing()) {
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 7ff576e..8986c05 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -135,7 +135,6 @@
             } catch (URISyntaxException e) {
                 Log.e(LOG_TAG, "Failed to parse system nav settings intent", e);
             }
-            finish();
         });
 
         findViewById(R.id.hint).setAccessibilityDelegate(new SkipButtonAccessibilityDelegate());
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index e2774c0..2c5825f 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
@@ -39,6 +39,7 @@
     protected TestRule getRulesInsideActivityMonitor() {
         return RuleChain.
                 outerRule(new NavigationModeSwitchRule(mLauncher)).
+                around(new TaskbarModeSwitchRule(mLauncher)).
                 around(super.getRulesInsideActivityMonitor());
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index e5e2cf3..eded1c9 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -51,7 +51,7 @@
 
 /**
  * Test rule that allows executing a test with Quickstep on and then Quickstep off.
- * The test should be annotated with @QuickstepOnOff.
+ * The test should be annotated with @NavigationModeSwitch.
  */
 public class NavigationModeSwitchRule implements TestRule {
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index 9337cb5..0b8bc10 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -17,6 +17,8 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
+
 import static junit.framework.TestCase.assertEquals;
 
 import android.content.Intent;
@@ -27,6 +29,7 @@
 import com.android.launcher3.tapl.Taskbar;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
 import org.junit.After;
 import org.junit.Assume;
@@ -53,21 +56,25 @@
         TaplTestsLauncher3.initialize(this);
 
         startAppFast(CALCULATOR_APP_PACKAGE);
+        mLauncher.enableBlockTimeout(true);
         mLauncher.showTaskbarIfHidden();
     }
 
     @After
     public void tearDown() {
         mLauncher.useDefaultWorkspaceLayoutOnReload();
+        mLauncher.enableBlockTimeout(false);
     }
 
     @Test
+    @TaskbarModeSwitch(mode = PERSISTENT)
     public void testHideShowTaskbar() {
         getTaskbar().hide();
         mLauncher.getLaunchedAppState().showTaskbar();
     }
 
     @Test
+    @TaskbarModeSwitch(mode = PERSISTENT)
     public void testHideTaskbarPersistsOnRecreate() {
         getTaskbar().hide();
         mLauncher.recreateTaskbar();
@@ -75,16 +82,19 @@
     }
 
     @Test
+    @TaskbarModeSwitch
     public void testLaunchApp() throws Exception {
         getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
     }
 
     @Test
+    @TaskbarModeSwitch
     public void testOpenMenu() throws Exception {
         getTaskbar().getAppIcon(TEST_APP_NAME).openMenu();
     }
 
     @Test
+    @TaskbarModeSwitch
     public void testLaunchShortcut() throws Exception {
         getTaskbar().getAppIcon(TEST_APP_NAME)
                 .openDeepShortcutMenu()
@@ -95,6 +105,7 @@
     @Test
     @ScreenRecord // b/231615831
     @PortraitLandscape
+    @TaskbarModeSwitch
     public void testLaunchAppInSplitscreen() throws Exception {
         getTaskbar().getAppIcon(TEST_APP_NAME).dragToSplitscreen(
                 TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE);
@@ -103,6 +114,7 @@
     @Test
     @ScreenRecord // b/231615831
     @PortraitLandscape
+    @TaskbarModeSwitch
     public void testLaunchShortcutInSplitscreen() throws Exception {
         getTaskbar().getAppIcon(TEST_APP_NAME)
                 .openDeepShortcutMenu()
@@ -111,16 +123,19 @@
     }
 
     @Test
+    @TaskbarModeSwitch
     public void testLaunchApp_FromTaskbarAllApps() throws Exception {
         getTaskbar().openAllApps().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
     }
 
     @Test
+    @TaskbarModeSwitch
     public void testOpenMenu_FromTaskbarAllApps() throws Exception {
         getTaskbar().openAllApps().getAppIcon(TEST_APP_NAME).openMenu();
     }
 
     @Test
+    @TaskbarModeSwitch
     public void testLaunchShortcut_FromTaskbarAllApps() throws Exception {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
@@ -132,6 +147,7 @@
     @Test
     @ScreenRecord // b/231615831
     @PortraitLandscape
+    @TaskbarModeSwitch
     public void testLaunchAppInSplitscreen_FromTaskbarAllApps() throws Exception {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
@@ -141,6 +157,7 @@
     @Test
     @ScreenRecord // b/231615831
     @PortraitLandscape
+    @TaskbarModeSwitch
     public void testLaunchShortcutInSplitscreen_FromTaskbarAllApps() throws Exception {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
diff --git a/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java
new file mode 100644
index 0000000..8cc812b
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2022 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.quickstep;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.quickstep.TaskbarModeSwitchRule.Mode.ALL;
+import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
+import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.rule.FailureWatcher;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Test rule that allows executing a test multiple times with different conditions
+ * ie. with transient taskbar enabled and disabled.
+ * The test should be annotated with @TaskbarModeSwitch.
+ */
+public class TaskbarModeSwitchRule implements TestRule {
+
+    static final String TAG = "TaskbarModeSwitchRule";
+
+    public static final int WAIT_TIME_MS = 10000;
+
+    public enum Mode {
+        TRANSIENT, PERSISTENT, ALL
+    }
+
+    // Annotation for tests that need to be run with quickstep enabled and disabled.
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface TaskbarModeSwitch {
+        Mode mode() default ALL;
+    }
+
+    private final LauncherInstrumentation mLauncher;
+
+    public TaskbarModeSwitchRule(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        if (TestHelpers.isInLauncherProcess()
+                && description.getAnnotation(TaskbarModeSwitch.class) != null) {
+            Mode mode = description.getAnnotation(TaskbarModeSwitch.class).mode();
+            return new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    mLauncher.enableDebugTracing();
+                    final boolean wasTransientTaskbarMode = isTaskbarTransientMode();
+                    try {
+                        if (mode == TRANSIENT || mode == ALL) {
+                            evaluateWithTransientTaskbar();
+                        }
+                        if (mode == PERSISTENT || mode == ALL) {
+                            evaluateWithPersistentTaskbar();
+                        }
+                    } catch (Throwable e) {
+                        Log.e(TAG, "Error", e);
+                        throw e;
+                    } finally {
+                        Log.d(TAG, "In Finally block");
+                        setTaskbarMode(mLauncher, wasTransientTaskbarMode, description);
+                    }
+                }
+
+                private void evaluateWithPersistentTaskbar() throws Throwable {
+                    setTaskbarMode(mLauncher, false, description);
+                    base.evaluate();
+                }
+
+                private void evaluateWithTransientTaskbar() throws Throwable {
+                    setTaskbarMode(mLauncher, true, description);
+                    base.evaluate();
+                }
+            };
+        } else {
+            return base;
+        }
+    }
+
+    private static void enableTransientTaskbar(LauncherInstrumentation launcher, Context c,
+            boolean enable, Description description) {
+        boolean success = c.getSharedPreferences(FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+                .edit()
+                .putBoolean(FeatureFlags.ENABLE_TRANSIENT_TASKBAR.key, enable)
+                .commit();
+
+        if (!success) {
+            final AssertionError assertionError = new AssertionError(
+                    "Unable to save state to SharedPrefs transientTaskbar=" + enable);
+            if (description != null) {
+                FailureWatcher.onError(launcher, description, assertionError);
+            }
+            throw assertionError;
+        }
+
+        FeatureFlags.initialize(c);
+    }
+
+    private static boolean isTaskbarTransientMode() {
+        return FeatureFlags.ENABLE_TRANSIENT_TASKBAR.get();
+    }
+
+    public static void setTaskbarMode(LauncherInstrumentation launcher,
+            boolean expectTransientTaskbar, Description description) throws Exception {
+
+        Log.d(TAG, "setTaskbarMode: isTransientTaskbar=" + expectTransientTaskbar + "...");
+
+        enableTransientTaskbar(launcher, getInstrumentation().getTargetContext(),
+                expectTransientTaskbar, description);
+
+        launcher.recreateTaskbar();
+
+        assertTrue(launcher, "Couldn't set taskbar=" + expectTransientTaskbar,
+                isTaskbarTransientMode() == expectTransientTaskbar, description);
+
+        AbstractLauncherUiTest.checkDetectedLeaks(launcher);
+    }
+
+    private static void assertTrue(LauncherInstrumentation launcher, String message,
+            boolean condition, Description description) {
+        launcher.checkForAnomaly(true, true);
+        if (!condition) {
+            final AssertionError assertionError = new AssertionError(message);
+            if (description != null) {
+                FailureWatcher.onError(launcher, description, assertionError);
+            }
+            throw assertionError;
+        }
+    }
+}
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 9e2d9a4..8b944fe 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -124,7 +124,7 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> instalatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> deskargatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> instalatzeko zain"</string>
-    <string name="dialog_update_title" msgid="114234265740994042">"Aplikazioa eguneratu behar da"</string>
+    <string name="dialog_update_title" msgid="114234265740994042">"Aplikazioa eguneratu egin behar da"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Ikonoaren aplikazioa ez dago eguneratuta. Lasterbidea berriro gaitzeko, eskuz egunera dezakezu aplikazioa. Bestela, kendu ikonoa."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Eguneratu"</string>
     <string name="dialog_remove" msgid="6510806469849709407">"Kendu"</string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 8e065e2..033346a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -367,7 +367,9 @@
     <dimen name="transient_taskbar_margin">0dp</dimen>
     <dimen name="transient_taskbar_shadow_blur">0dp</dimen>
     <dimen name="transient_taskbar_key_shadow_distance">0dp</dimen>
-    <dimen name="transient_taskbar_icon_spacing">10dp</dimen>
+    <dimen name="transient_taskbar_stashed_size">0dp</dimen>
+    <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
+    <dimen name="transient_taskbar_icon_spacing">0dp</dimen>
     <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
     <dimen name="taskbar_icon_spacing">8dp</dimen>
     <dimen name="taskbar_nav_buttons_size">0dp</dimen>
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 6777080..2103593 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -403,7 +403,6 @@
         mCountY = y;
         mOccupied = new GridOccupancy(mCountX, mCountY);
         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
-        mTempRectStack.clear();
         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
                 mBorderSpace);
         requestLayout();
@@ -1248,21 +1247,6 @@
                 result, resultSpan);
     }
 
-    private final Stack<Rect> mTempRectStack = new Stack<>();
-    private void lazyInitTempRectStack() {
-        if (mTempRectStack.isEmpty()) {
-            for (int i = 0; i < mCountX * mCountY; i++) {
-                mTempRectStack.push(new Rect());
-            }
-        }
-    }
-
-    private void recycleTempRects(Stack<Rect> used) {
-        while (!used.isEmpty()) {
-            mTempRectStack.push(used.pop());
-        }
-    }
-
     /**
      * Find a vacant area that will fit the given bounds nearest the requested
      * cell location. Uses Euclidean distance to score multiple vacant areas.
@@ -1282,8 +1266,6 @@
      */
     private int[] findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY,
             int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) {
-        lazyInitTempRectStack();
-
         // For items with a spanX / spanY > 1, the passed in point (relativeXPos, relativeYPos)
         // corresponds to the center of the item, but we are searching based on the top-left cell,
         // so we translate the point over to correspond to the top-left.
@@ -1353,9 +1335,6 @@
                         hitMaxY |= ySize >= spanY;
                         incX = !incX;
                     }
-                    incX = true;
-                    hitMaxX = xSize >= spanX;
-                    hitMaxY = ySize >= spanY;
                 }
                 final int[] cellXY = mTmpPoint;
                 cellToCenterPoint(x, y, cellXY);
@@ -1363,8 +1342,7 @@
                 // We verify that the current rect is not a sub-rect of any of our previous
                 // candidates. In this case, the current rect is disqualified in favour of the
                 // containing rect.
-                Rect currentRect = mTempRectStack.pop();
-                currentRect.set(x, y, x + xSize, y + ySize);
+                Rect currentRect = new Rect(x, y, x + xSize, y + ySize);
                 boolean contained = false;
                 for (Rect r : validRegions) {
                     if (r.contains(currentRect)) {
@@ -1394,7 +1372,6 @@
             bestXY[0] = -1;
             bestXY[1] = -1;
         }
-        recycleTempRects(validRegions);
         return bestXY;
     }
 
@@ -2545,21 +2522,21 @@
         if (result == null) {
             result = new int[]{-1, -1};
         }
-        ItemConfiguration finalSolution;
-        // When we are checking drop validity or actually dropping, we don't recompute the
-        // direction vector, since we want the solution to match the preview, and it's possible
-        // that the exact position of the item has changed to result in a new reordering outcome.
-        if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP)
-                && mPreviousSolution != null) {
+
+        ItemConfiguration finalSolution = null;
+        // We want the solution to match the animation of the preview and to match the drop so we
+        // only recalculate in mode MODE_SHOW_REORDER_HINT because that the first one to run in the
+        // reorder cycle.
+        if (mode == MODE_SHOW_REORDER_HINT || mPreviousSolution == null) {
+            finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
+                    dragView);
+            mPreviousSolution = finalSolution;
+        } else {
             finalSolution = mPreviousSolution;
             // We reset this vector after drop
             if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
                 mPreviousSolution = null;
             }
-        } else {
-            finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
-                    dragView);
-            mPreviousSolution = finalSolution;
         }
 
         if (finalSolution == null || !finalSolution.isSolution) {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 1616ae9..c91e3eb 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -316,10 +316,14 @@
         }
 
         if (isTaskbarPresent) {
-            taskbarSize = DisplayController.isTransientTaskbar(context)
-                    ? res.getDimensionPixelSize(R.dimen.transient_taskbar_size)
-                    : res.getDimensionPixelSize(R.dimen.taskbar_size);
-            stashedTaskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+            if (DisplayController.isTransientTaskbar(context)) {
+                taskbarSize = res.getDimensionPixelSize(R.dimen.transient_taskbar_size);
+                stashedTaskbarSize =
+                        res.getDimensionPixelSize(R.dimen.transient_taskbar_stashed_size);
+            } else {
+                taskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_size);
+                stashedTaskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+            }
         }
 
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 9426ae9..c73e077 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -53,6 +53,8 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP;
@@ -183,7 +185,6 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
-import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.ActivityResultInfo;
 import com.android.launcher3.util.ActivityTracker;
@@ -222,7 +223,6 @@
 import com.android.systemui.plugins.shared.LauncherExterns;
 import com.android.systemui.plugins.shared.LauncherOverlayManager;
 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
-import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -242,7 +242,7 @@
  */
 public class Launcher extends StatefulActivity<LauncherState>
         implements LauncherExterns, Callbacks, InvariantDeviceProfile.OnIDPChangeListener,
-        PluginListener<LauncherOverlayPlugin>, LauncherOverlayCallbacks {
+        PluginListener<LauncherOverlayPlugin> {
     public static final String TAG = "Launcher";
 
     public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
@@ -696,17 +696,9 @@
      */
     @Override
     public void setLauncherOverlay(LauncherOverlay overlay) {
-        if (overlay != null) {
-            overlay.setOverlayCallbacks(this);
-        }
         mWorkspace.setLauncherOverlay(overlay);
     }
 
-    @Override
-    public void runOnOverlayHidden(Runnable runnable) {
-        getWorkspace().runOnOverlayHidden(runnable);
-    }
-
     public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
         mLauncherCallbacks = callbacks;
         return true;
@@ -1214,18 +1206,6 @@
     }
 
     /**
-     * {@code LauncherOverlayCallbacks} scroll amount.
-     * Indicates transition progress to -1 screen.
-     * @param progress From 0 to 1.
-     */
-    @Override
-    public void onScrollChanged(float progress) {
-        if (mWorkspace != null) {
-            mWorkspace.onOverlayScrollChanged(progress);
-        }
-    }
-
-    /**
      * Restores the previous state, if it exists.
      *
      * @param savedState The previous state.
@@ -2895,7 +2875,16 @@
     /**
      * Informs us that the overlay (-1 screen, typically), has either become visible or invisible.
      */
-    public void onOverlayVisibilityChanged(boolean visible) {}
+    public void onOverlayVisibilityChanged(boolean visible) {
+        getStatsLogManager().logger()
+                .withSrcState(LAUNCHER_STATE_HOME)
+                .withDstState(LAUNCHER_STATE_HOME)
+                .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                        .setWorkspace(WorkspaceContainer.newBuilder()
+                                .setPageIndex(visible ? 0 : -1))
+                        .build())
+                .log(visible ? LAUNCHER_SWIPELEFT : LAUNCHER_SWIPERIGHT);
+    }
 
     /**
      * Informs us that the page transition has ended, so that we can react to the newly selected
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b6eb589..27e1ba1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -26,7 +26,6 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
-import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
@@ -58,7 +57,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.Toast;
@@ -119,6 +117,7 @@
 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 java.util.ArrayList;
 import java.util.Iterator;
@@ -136,7 +135,7 @@
 public class Workspace<T extends View & PageIndicator> extends PagedView<T>
         implements DropTarget, DragSource, View.OnTouchListener,
         DragController.DragListener, Insettable, StateHandler<LauncherState>,
-        WorkspaceLayoutManager, LauncherBindableItemsContainer {
+        WorkspaceLayoutManager, LauncherBindableItemsContainer, LauncherOverlayCallbacks {
 
     /** The value that {@link #mTransitionProgress} must be greater than for
      * {@link #transitionStateShouldAllowDrop()} to return true. */
@@ -254,14 +253,12 @@
 
     // State related to Launcher Overlay
     private OverlayEdgeEffect mOverlayEdgeEffect;
-    boolean mOverlayShown = false;
-    private Runnable mOnOverlayHiddenCallback;
+    private boolean mOverlayShown = false;
+    private float mOverlayProgress; // 1 -> overlay completely visible, 0 -> home visible
+    private final List<LauncherOverlayCallbacks> mOverlayCallbacks = new ArrayList<>();
 
     private boolean mForceDrawAdjacentPages = false;
 
-    // Total over scrollX in the overlay direction.
-    private float mOverlayTranslation;
-
     // Handles workspace state transitions
     private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
 
@@ -1151,9 +1148,15 @@
     }
 
     public void setLauncherOverlay(LauncherOverlay overlay) {
-        mOverlayEdgeEffect = overlay == null ? null : new OverlayEdgeEffect(getContext(), overlay);
-        EdgeEffectCompat newEffect = overlay == null
-                ? new EdgeEffectCompat(getContext()) : mOverlayEdgeEffect;
+        final EdgeEffectCompat newEffect;
+        if (overlay == null) {
+            newEffect = new EdgeEffectCompat(getContext());
+            mOverlayEdgeEffect = null;
+        } else {
+            newEffect = mOverlayEdgeEffect = new OverlayEdgeEffect(getContext(), overlay);
+            overlay.setOverlayCallbacks(this);
+        }
+
         if (mIsRtl) {
             mEdgeGlowRight = newEffect;
         } else {
@@ -1203,132 +1206,46 @@
     @Override
     protected boolean shouldFlingForVelocity(int velocityX) {
         // When the overlay is moving, the fling or settle transition is controlled by the overlay.
-        return Float.compare(Math.abs(mOverlayTranslation), 0) == 0 &&
-                super.shouldFlingForVelocity(velocityX);
+        return Float.compare(Math.abs(mOverlayProgress), 0) == 0
+                && super.shouldFlingForVelocity(velocityX);
     }
 
     /**
      * The overlay scroll is being controlled locally, just update our overlay effect
      */
+    @Override
     public void onOverlayScrollChanged(float scroll) {
-        if (Float.compare(scroll, 1f) == 0) {
+        mOverlayProgress = Utilities.boundToRange(scroll, 0, 1);
+        if (Float.compare(mOverlayProgress, 1f) == 0) {
             if (!mOverlayShown) {
-                mLauncher.getStatsLogManager().logger()
-                        .withSrcState(LAUNCHER_STATE_HOME)
-                        .withDstState(LAUNCHER_STATE_HOME)
-                        .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
-                                .setWorkspace(
-                                        LauncherAtom.WorkspaceContainer.newBuilder()
-                                                .setPageIndex(0))
-                                .build())
-                        .log(LAUNCHER_SWIPELEFT);
+                mOverlayShown = true;
+                mLauncher.onOverlayVisibilityChanged(true);
             }
-            mOverlayShown = true;
-
-            // Let the Launcher activity know that the overlay is now visible.
-            mLauncher.onOverlayVisibilityChanged(mOverlayShown);
-
-            // Not announcing the overlay page for accessibility since it announces itself.
-        } else if (Float.compare(scroll, 0f) == 0) {
+        } else if (Float.compare(mOverlayProgress, 0f) == 0) {
             if (mOverlayShown) {
-                // TODO: this is logged unnecessarily on home gesture.
-                mLauncher.getStatsLogManager().logger()
-                        .withSrcState(LAUNCHER_STATE_HOME)
-                        .withDstState(LAUNCHER_STATE_HOME)
-                        .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
-                                .setWorkspace(
-                                        LauncherAtom.WorkspaceContainer.newBuilder()
-                                                .setPageIndex(-1))
-                                .build())
-                        .log(LAUNCHER_SWIPERIGHT);
-            } else if (Float.compare(mOverlayTranslation, 0f) != 0) {
-                // When arriving to 0 overscroll from non-zero overscroll, announce page for
-                // accessibility since default announcements were disabled while in overscroll
-                // state.
-                // Not doing this if mOverlayShown because in that case the accessibility service
-                // will announce the launcher window description upon regaining focus after
-                // switching from the overlay screen.
-                announcePageForAccessibility();
+                mOverlayShown = false;
+                mLauncher.onOverlayVisibilityChanged(false);
             }
-            mOverlayShown = false;
-
-            // Let the Launcher activity know that the overlay is no longer visible.
-            mLauncher.onOverlayVisibilityChanged(mOverlayShown);
-
-            tryRunOverlayCallback();
         }
-
-        float offset = 0f;
-
-        scroll = Math.max(scroll - offset, 0);
-        scroll = Math.min(1, scroll / (1 - offset));
-
-        float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(scroll);
-        float transX = mLauncher.getDragLayer().getMeasuredWidth() * scroll;
-
-        if (mIsRtl) {
-            transX = -transX;
+        int count = mOverlayCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            mOverlayCallbacks.get(i).onOverlayScrollChanged(mOverlayProgress);
         }
-        mOverlayTranslation = transX;
-
-        // TODO(adamcohen): figure out a final effect here. We may need to recommend
-        // different effects based on device performance. On at least one relatively high-end
-        // device I've tried, translating the launcher causes things to get quite laggy.
-        mLauncher.getDragLayer().setTranslationX(transX);
-        mLauncher.getDragLayer().getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha);
     }
 
     /**
-     * @return false if the callback is still pending
+     * Adds a callback for receiving overlay progress
      */
-    private boolean tryRunOverlayCallback() {
-        if (mOnOverlayHiddenCallback == null) {
-            // Return true as no callback is pending. This is used by OnWindowFocusChangeListener
-            // to remove itself if multiple focus handles were added.
-            return true;
-        }
-        if (mOverlayShown || !hasWindowFocus()) {
-            return false;
-        }
-
-        mOnOverlayHiddenCallback.run();
-        mOnOverlayHiddenCallback = null;
-        return true;
+    public void addOverlayCallback(LauncherOverlayCallbacks callback) {
+        mOverlayCallbacks.add(callback);
+        callback.onOverlayScrollChanged(mOverlayProgress);
     }
 
     /**
-     * Runs the given callback when the minus one overlay is hidden. Specifically, it is run
-     * when launcher's window has focus and the overlay is no longer being shown. If a callback
-     * is already present, the new callback will chain off it so both are run.
-     *
-     * @return Whether the callback was deferred.
+     * Removes a previously added overlay progress callback
      */
-    public boolean runOnOverlayHidden(Runnable callback) {
-        if (mOnOverlayHiddenCallback == null) {
-            mOnOverlayHiddenCallback = callback;
-        } else {
-            // Chain the new callback onto the previous callback(s).
-            Runnable oldCallback = mOnOverlayHiddenCallback;
-            mOnOverlayHiddenCallback = () -> {
-                oldCallback.run();
-                callback.run();
-            };
-        }
-        if (!tryRunOverlayCallback()) {
-            ViewTreeObserver observer = getViewTreeObserver();
-            if (observer != null && observer.isAlive()) {
-                observer.addOnWindowFocusChangeListener(
-                        new ViewTreeObserver.OnWindowFocusChangeListener() {
-                            @Override
-                            public void onWindowFocusChanged(boolean hasFocus) {
-                                if (tryRunOverlayCallback() && observer.isAlive()) {
-                                    observer.removeOnWindowFocusChangeListener(this);
-                                }
-                            }});
-            }
-            return true;
-        }
-        return false;
+    public void removeOverlayCallback(LauncherOverlayCallbacks callback) {
+        mOverlayCallbacks.remove(callback);
     }
 
     @Override
@@ -2467,23 +2384,20 @@
         final View child = (mDragInfo == null) ? null : mDragInfo.cell;
         int reorderX = mTargetCell[0];
         int reorderY = mTargetCell[1];
-        if (!nearestDropOccupied) {
-            mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
-                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
-                    child, mTargetCell, new int[2], CellLayout.MODE_SHOW_REORDER_HINT);
-            mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
-                    item.spanX, item.spanY, d);
-        } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
-                && !mReorderAlarm.alarmPending()
+        if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                 && (mLastReorderX != reorderX || mLastReorderY != reorderY)
                 && targetCellDistance < mDragTargetLayout.getReorderRadius(mTargetCell, item.spanX,
                 item.spanY)) {
-
-            int[] resultSpan = new int[2];
             mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
-                    child, mTargetCell, resultSpan, CellLayout.MODE_SHOW_REORDER_HINT);
+                    child, mTargetCell, new int[2], CellLayout.MODE_SHOW_REORDER_HINT);
+        }
 
+        if (!nearestDropOccupied) {
+            mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
+                    item.spanX, item.spanY, d);
+        } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
+                && !mReorderAlarm.alarmPending()) {
             // Otherwise, if we aren't adding to or creating a folder and there's no pending
             // reorder, then we schedule a reorder
             ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
@@ -3470,7 +3384,7 @@
     protected boolean canAnnouncePageDescription() {
         // Disable announcements while overscrolling potentially to overlay screen because if we end
         // up on the overlay screen, it will take care of announcing itself.
-        return Float.compare(mOverlayTranslation, 0f) == 0;
+        return Float.compare(mOverlayProgress, 0f) == 0;
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 4bea0ad..1ee7fc1 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -47,7 +47,9 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.SpringProperty;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
@@ -56,13 +58,14 @@
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
 
 import java.util.ArrayList;
 
 /**
  * A ViewGroup that coordinates dragging across its descendants
  */
-public class DragLayer extends BaseDragLayer<Launcher> {
+public class DragLayer extends BaseDragLayer<Launcher> implements LauncherOverlayCallbacks {
 
     public static final int ALPHA_INDEX_OVERLAY = 0;
     private static final int ALPHA_CHANNEL_COUNT = 1;
@@ -70,6 +73,8 @@
     public static final int ANIMATION_END_DISAPPEAR = 0;
     public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
 
+    private final boolean mIsRtl;
+
     private DragController mDragController;
 
     // Variables relating to animation of views after drop
@@ -100,6 +105,7 @@
         setChildrenDrawingOrderEnabled(true);
 
         mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
+        mIsRtl = Utilities.isRtl(getResources());
     }
 
     /**
@@ -109,6 +115,7 @@
         mDragController = dragController;
         recreateControllers();
         mWorkspaceDragScrim = new Scrim(this);
+        workspace.addOverlayCallback(this);
     }
 
     @Override
@@ -476,4 +483,16 @@
             controller.onOneHandedModeStateChanged(activated);
         }
     }
+
+    @Override
+    public void onOverlayScrollChanged(float progress) {
+        float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(progress);
+        float transX = getMeasuredWidth() * progress;
+
+        if (mIsRtl) {
+            transX = -transX;
+        }
+        setTranslationX(transX);
+        getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha);
+    }
 }
diff --git a/src/com/android/launcher3/testing/shared/TestProtocol.java b/src/com/android/launcher3/testing/shared/TestProtocol.java
index 3fbce88..90b8bfa 100644
--- a/src/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/src/com/android/launcher3/testing/shared/TestProtocol.java
@@ -84,6 +84,8 @@
     public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list";
     public static final String REQUEST_ENABLE_MANUAL_TASKBAR_STASHING = "enable-taskbar-stashing";
     public static final String REQUEST_DISABLE_MANUAL_TASKBAR_STASHING = "disable-taskbar-stashing";
+    public static final String REQUEST_ENABLE_BLOCK_TIMEOUT = "enable-block-timeout";
+    public static final String REQUEST_DISABLE_BLOCK_TIMEOUT = "disable-block-timeout";
     public static final String REQUEST_UNSTASH_TASKBAR_IF_STASHED = "unstash-taskbar-if-stashed";
     public static final String REQUEST_STASHED_TASKBAR_HEIGHT = "stashed-taskbar-height";
     public static final String REQUEST_RECREATE_TASKBAR = "recreate-taskbar";
diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java b/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
index 13e4999..173b454 100644
--- a/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
@@ -40,10 +40,4 @@
      * Sets the overlay on the target activity
      */
     void setLauncherOverlay(LauncherOverlay overlay);
-
-    /**
-     * Executes the command, next time the overlay is hidden
-     */
-    void runOnOverlayHidden(Runnable runnable);
-
 }
diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
index ac02ba4..582ab23 100644
--- a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
@@ -93,6 +93,6 @@
 
     interface LauncherOverlayCallbacks {
 
-        void onScrollChanged(float progress);
+        void onOverlayScrollChanged(float progress);
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 978e84c..50e0990 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -335,6 +335,7 @@
     @IwTest(focusArea="launcher")
     @Test
     @PortraitLandscape
+    @ScreenRecord // b/256898879
     public void testDragAppIcon() throws Throwable {
         // 1. Open all apps and wait for load complete.
         // 2. Drag icon to homescreen.
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 6bbdf48..b0cf20f 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL;
+import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
+
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -37,6 +40,9 @@
  * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
  */
 public abstract class AllApps extends LauncherInstrumentation.VisibleContainer {
+    // Defer updates flag used to defer all apps updates by a test's request.
+    private static final int DEFER_UPDATES_TEST = 1 << 1;
+
     private static final int MAX_SCROLL_ATTEMPTS = 40;
 
     private final int mHeight;
@@ -292,12 +298,16 @@
      */
     public void unfreeze() {
         mLauncher.getTestInfo(TestProtocol.REQUEST_UNFREEZE_APP_LIST);
-        verifyNotFrozen("All apps freeze flags upon unfreezing");
     }
 
     private void verifyNotFrozen(String message) {
+        mLauncher.assertEquals(message, 0, getFreezeFlags() & DEFER_UPDATES_TEST);
+        mLauncher.assertTrue(message, mLauncher.waitAndGet(() -> getFreezeFlags() == 0,
+                WAIT_TIME_MS, DEFAULT_POLL_INTERVAL));
+    }
+
+    private int getFreezeFlags() {
         final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS);
-        if (testInfo == null) return;
-        mLauncher.assertEquals(message, 0, testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
+        return testInfo == null ? 0 : testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 4b02ecc..0a00480 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -17,7 +17,9 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING;
+import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_HEIGHT;
 
@@ -88,6 +90,7 @@
      */
     public Taskbar showTaskbar() {
         mLauncher.getTestInfo(REQUEST_ENABLE_MANUAL_TASKBAR_STASHING);
+        mLauncher.getTestInfo(REQUEST_ENABLE_BLOCK_TIMEOUT);
 
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
@@ -114,6 +117,7 @@
             }
         } finally {
             mLauncher.getTestInfo(REQUEST_DISABLE_MANUAL_TASKBAR_STASHING);
+            mLauncher.getTestInfo(REQUEST_DISABLE_BLOCK_TIMEOUT);
         }
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 1c5c5fa..477d5ad 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -83,6 +83,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.TimeoutException;
+import java.util.function.BooleanSupplier;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -181,6 +182,7 @@
     static final String TASKBAR_RES_ID = "taskbar_view";
     private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
     public static final int WAIT_TIME_MS = 30000;
+    static final long DEFAULT_POLL_INTERVAL = 1000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
     private static final String ANDROID_PACKAGE = "android";
 
@@ -1822,6 +1824,13 @@
         getTestInfo(TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED);
     }
 
+    /** Blocks the taskbar from automatically stashing based on time. */
+    public void enableBlockTimeout(boolean enable) {
+        getTestInfo(enable
+                ? TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT
+                : TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT);
+    }
+
     /**
      * Recreates the taskbar (outside of tests this is done for certain configuration changes).
      * The expected behavior is that the taskbar retains its current state after being recreated.
@@ -1991,4 +2000,21 @@
             mCallbackAtRunPoint.accept(runPoint);
         }
     }
+
+    /**
+     * Waits until a particular condition is true. Based on WaitMixin.
+     */
+    boolean waitAndGet(BooleanSupplier condition, long timeout, long interval) {
+        long startTime = SystemClock.uptimeMillis();
+
+        boolean result = condition.getAsBoolean();
+        for (long elapsedTime = 0; !result; elapsedTime = SystemClock.uptimeMillis() - startTime) {
+            if (elapsedTime >= timeout) {
+                break;
+            }
+            SystemClock.sleep(interval);
+            result = condition.getAsBoolean();
+        }
+        return result;
+    }
 }