Merge "Add CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS" into tm-qpr-dev
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 02b83fe..951be4e 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -43,7 +43,8 @@
     <!-- for rotating surface by arbitrary degree -->
     <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-    
+    <uses-permission android:name="android.permission.READ_HOME_APP_SEARCH_DATA" />
+
     <!--
     Permissions required for read/write access to the workspace data. These permission name
     should not conflict with that defined in other apps, as such an app should embed its package
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
index a91ff44..bdac88a 100644
--- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.testing;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.app.Activity;
 import android.app.Application;
@@ -248,6 +249,9 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_MODEL_QUEUE_CLEARED:
+                return getFromExecutorSync(MODEL_EXECUTOR, Bundle::new);
+
             default:
                 return super.call(method, arg, extras);
         }
diff --git a/quickstep/res/drawable/ic_floating_task_button.xml b/quickstep/res/drawable/ic_floating_task_button.xml
deleted file mode 100644
index 63b2fd8..0000000
--- a/quickstep/res/drawable/ic_floating_task_button.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-  <group
-      android:pivotY="12"
-      android:pivotX="12"
-      android:scaleX=".75"
-      android:scaleY=".75">
-    <path
-        android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z"
-        android:fillColor="#636C6F"/>
-    <path
-        android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z"
-        android:fillColor="#636C6F"
-        android:fillType="evenOdd"/>
-  </group>
-</vector>
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index e493ac9..d5e3670 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -26,4 +26,5 @@
     android:importantForAccessibility="noHideDescendants"
     android:textColor="?priv-android:attr/textColorOnAccent"
     android:textSize="14sp"
-    android:autoSizeTextType="uniform"/>
\ No newline at end of file
+    android:autoSizeTextType="uniform"
+    android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_floating_task_button.xml b/quickstep/res/layout/taskbar_floating_task_button.xml
deleted file mode 100644
index b5beded..0000000
--- a/quickstep/res/layout/taskbar_floating_task_button.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<com.android.launcher3.views.IconButtonView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/taskbar_icon_touch_size"
-    android:layout_height="@dimen/taskbar_icon_touch_size"
-    android:icon="@drawable/ic_floating_task_button"
-    />
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 45a2cf8..6e3fd32 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -288,6 +288,13 @@
     <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>
+    <dimen name="taskbar_catch_up_threshold">300dp</dimen>
+    <dimen name="taskbar_nav_threshold">40dp</dimen>
 
     <!--  Taskbar 3 button spacing  -->
     <dimen name="taskbar_button_space_inbetween">24dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 880aa6f..95a94ec 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -55,7 +55,7 @@
  * reference to the runner, leaving only the weak ref from the runner.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
+public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat {
 
     private static final RemoteAnimationFactory DEFAULT_FACTORY =
             (transit, appTargets, wallpaperTargets, nonAppTargets, result) ->
@@ -99,22 +99,6 @@
         }
     }
 
-    // Called only in R platform
-    @BinderThread
-    public void onAnimationStart(RemoteAnimationTarget[] appTargets,
-            RemoteAnimationTarget[] wallpaperTargets, Runnable runnable) {
-        onAnimationStart(0 /* transit */, appTargets, wallpaperTargets,
-                new RemoteAnimationTarget[0], runnable);
-    }
-
-    // Called only in Q platform
-    @BinderThread
-    @Deprecated
-    public void onAnimationStart(RemoteAnimationTarget[] appTargets, Runnable runnable) {
-        onAnimationStart(appTargets, new RemoteAnimationTarget[0], runnable);
-    }
-
-
     private RemoteAnimationFactory getFactory() {
         RemoteAnimationFactory factory = mFactory.get();
         return factory != null ? factory : DEFAULT_FACTORY;
@@ -133,7 +117,7 @@
      */
     @BinderThread
     @Override
-    public void onAnimationCancelled() {
+    public void onAnimationCancelled(boolean isKeyguardOccluded) {
         postAsyncCallback(mHandler, () -> {
             finishExistingAnimation();
             getFactory().onAnimationCancelled();
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 938aa5e..1b47939 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -16,11 +16,19 @@
 
 package com.android.launcher3;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
@@ -81,6 +89,8 @@
 import android.util.Pair;
 import android.util.Size;
 import android.view.CrossWindowBlurListeners;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -90,6 +100,8 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -134,10 +146,7 @@
 import com.android.systemui.shared.system.BlurUtils;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
-import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
 
 import java.util.ArrayList;
@@ -216,7 +225,7 @@
     private RemoteAnimationFactory mKeyguardGoingAwayRunner;
 
     private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
-    private RemoteTransitionCompat mLauncherOpenTransition;
+    private RemoteTransition mLauncherOpenTransition;
 
     private LauncherBackAnimationController mBackAnimationController;
     private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
@@ -293,12 +302,10 @@
 
         long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
                 - STATUS_BAR_TRANSITION_PRE_DELAY;
-        RemoteAnimationAdapterCompat adapterCompat =
-                new RemoteAnimationAdapterCompat(runner, duration, statusBarTransitionDelay,
-                        mLauncher.getIApplicationThread());
         ActivityOptions options = ActivityOptions.makeRemoteAnimation(
-                adapterCompat.getWrapped(),
-                adapterCompat.getRemoteTransition().getTransition());
+                new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay),
+                new RemoteTransition(runner.toRemoteTransition(),
+                        mLauncher.getIApplicationThread()));
         return new ActivityOptionsWrapper(options, onEndCallback);
     }
 
@@ -364,13 +371,10 @@
         // before our internal listeners.
         mLauncher.getStateManager().setCurrentAnimation(anim);
 
-        final int rotationChange = getRotationChange(appTargets);
         // Note: the targetBounds are relative to the launcher
         int startDelay = getSingleFrameMs(mLauncher);
-        Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange);
-        Animator windowAnimator = getOpeningWindowAnimators(v, appTargets, wallpaperTargets,
-                nonAppTargets, windowTargetBounds, areAllTargetsTranslucent(appTargets),
-                rotationChange);
+        Animator windowAnimator = getOpeningWindowAnimators(
+                v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing);
         windowAnimator.setStartDelay(startDelay);
         anim.play(windowAnimator);
         if (launcherClosing) {
@@ -384,17 +388,6 @@
                     launcherContentAnimator.second.run();
                 }
             });
-        } else {
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationStart(Animator animation) {
-                    mLauncher.addOnResumeCallback(() ->
-                            ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
-                                    MULTI_PROPERTY_VALUE,
-                                    mLauncher.getStateManager().getState().getDepth(
-                                            mLauncher)).start());
-                }
-            });
         }
     }
 
@@ -403,23 +396,11 @@
             @NonNull LauncherAppWidgetHostView v,
             @NonNull RemoteAnimationTarget[] appTargets,
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
-            @NonNull RemoteAnimationTarget[] nonAppTargets) {
+            @NonNull RemoteAnimationTarget[] nonAppTargets,
+            boolean launcherClosing) {
         mLauncher.getStateManager().setCurrentAnimation(anim);
-
-        Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
-        anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets,
-                windowTargetBounds, areAllTargetsTranslucent(appTargets)));
-
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mLauncher.addOnResumeCallback(() ->
-                        ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
-                                MULTI_PROPERTY_VALUE,
-                                mLauncher.getStateManager().getState().getDepth(
-                                        mLauncher)).start());
-            }
-        });
+        anim.play(getOpeningWindowAnimatorsForWidget(
+                v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing));
     }
 
     /**
@@ -657,7 +638,11 @@
             RemoteAnimationTarget[] appTargets,
             RemoteAnimationTarget[] wallpaperTargets,
             RemoteAnimationTarget[] nonAppTargets,
-            Rect windowTargetBounds, boolean appTargetsAreTranslucent, int rotationChange) {
+            boolean launcherClosing) {
+        int rotationChange = getRotationChange(appTargets);
+        Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange);
+        boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets);
+
         RectF launcherIconBounds = new RectF();
         FloatingIconView floatingView = FloatingIconView.getFloatingIconView(mLauncher, v,
                 !appTargetsAreTranslucent, launcherIconBounds, true /* isOpening */);
@@ -853,7 +838,6 @@
                                 .setShadowRadius(mShadowRadius.value);
                     } else if (target.mode == MODE_CLOSING) {
                         if (target.localBounds != null) {
-                            final Rect localBounds = target.localBounds;
                             tmpPos.set(target.localBounds.left, target.localBounds.top);
                         } else {
                             tmpPos.set(target.position.x, target.position.y);
@@ -898,7 +882,7 @@
 
         // If app targets are translucent, do not animate the background as it causes a visible
         // flicker when it resets itself at the end of its animation.
-        if (appTargetsAreTranslucent) {
+        if (appTargetsAreTranslucent || !launcherClosing) {
             animatorSet.play(appAnimator);
         } else {
             animatorSet.playTogether(appAnimator, getBackgroundAnimator());
@@ -909,8 +893,10 @@
     private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v,
             RemoteAnimationTarget[] appTargets,
             RemoteAnimationTarget[] wallpaperTargets,
-            RemoteAnimationTarget[] nonAppTargets, Rect windowTargetBounds,
-            boolean appTargetsAreTranslucent) {
+            RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) {
+        Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
+        boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets);
+
         final RectF widgetBackgroundBounds = new RectF();
         final Rect appWindowCrop = new Rect();
         final Matrix matrix = new Matrix();
@@ -1037,7 +1023,7 @@
 
         // If app targets are translucent, do not animate the background as it causes a visible
         // flicker when it resets itself at the end of its animation.
-        if (appTargetsAreTranslucent) {
+        if (appTargetsAreTranslucent || !launcherClosing) {
             animatorSet.play(appAnimator);
         } else {
             animatorSet.playTogether(appAnimator, getBackgroundAnimator());
@@ -1095,28 +1081,26 @@
         if (hasControlRemoteAppTransitionPermission()) {
             mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
 
-            RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
+            RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
             definition.addRemoteAnimation(WindowManager.TRANSIT_OLD_WALLPAPER_OPEN,
                     WindowConfiguration.ACTIVITY_TYPE_STANDARD,
-                    new RemoteAnimationAdapterCompat(
+                    new RemoteAnimationAdapter(
                             new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner,
                                     false /* startAtFrontOfQueue */),
-                            CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */,
-                            mLauncher.getIApplicationThread()));
+                            CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
 
             if (KEYGUARD_ANIMATION.get()) {
                 mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */);
                 definition.addRemoteAnimation(
                         WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
-                        new RemoteAnimationAdapterCompat(
+                        new RemoteAnimationAdapter(
                                 new LauncherAnimationRunner(
                                         mHandler, mKeyguardGoingAwayRunner,
                                         true /* startAtFrontOfQueue */),
-                                CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */,
-                                mLauncher.getIApplicationThread()));
+                                CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
             }
 
-            mLauncher.registerRemoteAnimations(definition.getWrapped());
+            mLauncher.registerRemoteAnimations(definition);
         }
     }
 
@@ -1129,11 +1113,25 @@
         }
         if (hasControlRemoteAppTransitionPermission()) {
             mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */);
-            mLauncherOpenTransition = RemoteAnimationAdapterCompat.buildRemoteTransition(
+            mLauncherOpenTransition = new RemoteTransition(
                     new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner,
-                            false /* startAtFrontOfQueue */), mLauncher.getIApplicationThread());
-            mLauncherOpenTransition.addHomeOpenCheck(mLauncher.getComponentName());
-            SystemUiProxy.INSTANCE.get(mLauncher).registerRemoteTransition(mLauncherOpenTransition);
+                            false /* startAtFrontOfQueue */).toRemoteTransition(),
+                    mLauncher.getIApplicationThread());
+
+            TransitionFilter homeCheck = new TransitionFilter();
+            // No need to handle the transition that also dismisses keyguard.
+            homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+            homeCheck.mRequirements =
+                    new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
+                            new TransitionFilter.Requirement()};
+            homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
+            homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName();
+            homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+            homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
+            homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
+            homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+            SystemUiProxy.INSTANCE.get(mLauncher)
+                    .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
         }
         if (mBackAnimationController != null) {
             mBackAnimationController.registerBackCallbacks(mHandler);
@@ -1717,7 +1715,7 @@
             final boolean skipFirstFrame;
             if (launchingFromWidget) {
                 composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
-                        wallpaperTargets, nonAppTargets);
+                        wallpaperTargets, nonAppTargets, launcherClosing);
                 addCujInstrumentation(
                         anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_WIDGET);
                 skipFirstFrame = true;
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 7a483a8..1beabf1 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -18,22 +18,22 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 
 import android.app.prediction.AppTarget;
-import android.content.ComponentName;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /** Task to update model as a result of predicted widgets update */
@@ -59,50 +59,43 @@
         Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
                 widget -> new ComponentKey(widget.providerName, widget.user)).collect(
                 Collectors.toSet());
+        Predicate<WidgetItem> notOnWorkspace = w -> !widgetsInWorkspace.contains(w);
         Map<PackageUserKey, List<WidgetItem>> allWidgets =
                 dataModel.widgetsModel.getAllWidgetsWithoutShortcuts();
 
-        FixedContainerItems fixedContainerItems =
-                new FixedContainerItems(mPredictorState.containerId);
+        List<WidgetItem> servicePredictedItems = new ArrayList<>();
+        List<WidgetItem> localFilteredWidgets = new ArrayList<>();
 
-        if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) {
-            for (AppTarget app : mTargets) {
-                PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(),
-                        app.getUser());
-                if (allWidgets.containsKey(packageUserKey)) {
-                    List<WidgetItem> notAddedWidgets = allWidgets.get(packageUserKey).stream()
-                            .filter(item ->
-                                    !widgetsInWorkspace.contains(
-                                            new ComponentKey(item.componentName, item.user)))
-                            .collect(Collectors.toList());
-                    if (notAddedWidgets.size() > 0) {
-                        // Even an apps have more than one widgets, we only include one widget.
-                        fixedContainerItems.items.add(
-                                new PendingAddWidgetInfo(
-                                        notAddedWidgets.get(0).widgetInfo,
-                                        CONTAINER_WIDGETS_PREDICTION));
-                    }
-                }
+        for (AppTarget app : mTargets) {
+            PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), app.getUser());
+            List<WidgetItem> widgets = allWidgets.get(packageUserKey);
+            if (widgets == null || widgets.isEmpty()) {
+                continue;
             }
-        } else {
-            Map<ComponentKey, WidgetItem> widgetItems =
-                    allWidgets.values().stream().flatMap(List::stream).distinct()
-                            .collect(Collectors.toMap(widget -> (ComponentKey) widget,
-                                    widget -> widget));
-            for (AppTarget app : mTargets) {
-                if (TextUtils.isEmpty(app.getClassName())) {
+            String className = app.getClassName();
+            if (!TextUtils.isEmpty(className)) {
+                WidgetItem item = widgets.stream()
+                        .filter(w -> className.equals(w.componentName.getClassName()))
+                        .filter(notOnWorkspace)
+                        .findFirst()
+                        .orElse(null);
+                if (item != null) {
+                    servicePredictedItems.add(item);
                     continue;
                 }
-                ComponentKey targetWidget = new ComponentKey(
-                        new ComponentName(app.getPackageName(), app.getClassName()), app.getUser());
-                if (widgetItems.containsKey(targetWidget)) {
-                    fixedContainerItems.items.add(
-                            new PendingAddWidgetInfo(widgetItems.get(
-                                    targetWidget).widgetInfo,
-                                    CONTAINER_WIDGETS_PREDICTION));
-                }
             }
+            // No widget was added by the service, try local filtering
+            widgets.stream().filter(notOnWorkspace).findFirst()
+                    .ifPresent(localFilteredWidgets::add);
         }
+        if (servicePredictedItems.isEmpty()) {
+            servicePredictedItems.addAll(localFilteredWidgets);
+        }
+        FixedContainerItems fixedContainerItems =
+                new FixedContainerItems(mPredictorState.containerId);
+        servicePredictedItems.forEach(w -> fixedContainerItems.items.add(
+                new PendingAddWidgetInfo(w.widgetInfo, CONTAINER_WIDGETS_PREDICTION)));
+
         dataModel.extraItems.put(mPredictorState.containerId, fixedContainerItems);
         bindExtraContainerItems(fixedContainerItems);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index f1e6747..08ed60d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -21,6 +21,7 @@
 
 import android.animation.Animator;
 
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.fallback.RecentsState;
@@ -70,12 +71,14 @@
      * Currently this animation just force stashes the taskbar in Overview.
      */
     public Animator createAnimToRecentsState(RecentsState toState, long duration) {
-        boolean forceStashed = toState.hasOverviewActions();
+        boolean useStashedLauncherState = toState.hasOverviewActions();
+        boolean stashedLauncherState =
+                useStashedLauncherState && !FeatureFlags.ENABLE_TASKBAR_IN_OVERVIEW.get();
         TaskbarStashController controller = mControllers.taskbarStashController;
         // Set both FLAG_IN_STASHED_LAUNCHER_STATE and FLAG_IN_APP to ensure the state is respected.
         // For all other states, just use the current stashed-in-app setting (e.g. if long clicked).
-        controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, forceStashed);
-        controller.updateStateForFlag(FLAG_IN_APP, !forceStashed);
+        controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, stashedLauncherState);
+        controller.updateStateForFlag(FLAG_IN_APP, !useStashedLauncherState);
         return controller.applyStateWithoutStart(duration);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/FloatingTaskIntentResolver.java b/quickstep/src/com/android/launcher3/taskbar/FloatingTaskIntentResolver.java
deleted file mode 100644
index 5f4d239..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/FloatingTaskIntentResolver.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.launcher3.taskbar;
-
-import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
-
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.launcher3.R;
-
-// TODO: This would be replaced by the thing that has the role and provides the intent.
-/**
- * Helper to determine what intent should be used to display in a floating window, if one
- * exists.
- */
-public class FloatingTaskIntentResolver {
-    private static final String TAG = FloatingTaskIntentResolver.class.getSimpleName();
-
-    @Nullable
-    /** Gets an intent for a floating task, if one exists. */
-    public static Intent getIntent(Context context) {
-        PackageManager pm = context.getPackageManager();
-        String pkg = context.getString(R.string.floating_task_package);
-        String action = context.getString(R.string.floating_task_action);
-        if (TextUtils.isEmpty(pkg) || TextUtils.isEmpty(action)) {
-            Log.d(TAG, "intent could not be found, pkg= " + pkg + " action= " + action);
-            return null;
-        }
-        Intent intent = createIntent(pm, null, pkg, action);
-        if (intent != null) {
-            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            return intent;
-        }
-        Log.d(TAG, "No valid intent found!");
-        return null;
-    }
-
-    @Nullable
-    private static Intent createIntent(PackageManager pm, @Nullable String activityName,
-            String packageName, String action) {
-        if (TextUtils.isEmpty(activityName)) {
-            activityName = queryActivityForAction(pm, packageName, action);
-        }
-        if (TextUtils.isEmpty(activityName)) {
-            Log.d(TAG, "Activity name is empty even after action search: " + action);
-            return null;
-        }
-        ComponentName component = new ComponentName(packageName, activityName);
-        Intent intent = new Intent(action).setComponent(component).setPackage(packageName);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        Log.d(TAG, "createIntent returning: " + intent);
-        return intent;
-    }
-
-    @Nullable
-    private static String queryActivityForAction(PackageManager pm, String packageName,
-            String action) {
-        Intent intent = new Intent(action).setPackage(packageName);
-        ResolveInfo resolveInfo = pm.resolveActivity(intent, MATCH_DEFAULT_ONLY);
-        if (resolveInfo == null || resolveInfo.activityInfo == null) {
-            Log.d(TAG, "queryActivityForAction: + " + resolveInfo);
-            return null;
-        }
-        ActivityInfo info = resolveInfo.activityInfo;
-        if (!info.exported) {
-            Log.d(TAG, "queryActivityForAction: + " + info + " not exported");
-            return null;
-        }
-        if (!info.enabled) {
-            Log.d(TAG, "queryActivityForAction: + " + info + " not enabled");
-            return null;
-        }
-        return resolveInfo.activityInfo.name;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index c9e42b7..174f97f 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;
 
@@ -193,16 +192,15 @@
      */
     public Animator createAnimToLauncher(@NonNull LauncherState toState,
             @NonNull RecentsAnimationCallbacks callbacks, long duration) {
-        return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
-    }
+        AnimatorSet set = new AnimatorSet();
+        Animator taskbarState = mTaskbarLauncherStateController
+                .createAnimToLauncher(toState, callbacks, duration);
+        long halfDuration = Math.round(duration * 0.5f);
+        Animator translation =
+                mControllers.taskbarTranslationController.createAnimToLauncher(halfDuration);
 
-    /**
-     * @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);
+        set.playTogether(taskbarState, translation);
+        return set;
     }
 
     public boolean isDraggingItem() {
@@ -356,6 +354,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/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 12dbcb3..45d5739 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -30,6 +30,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -66,6 +67,7 @@
 
     // Initialized in init.
     private TaskbarControllers mControllers;
+    private int mTaskbarSize;
 
     // The bounds we want to clip to in the settled state when showing the stashed handle.
     private final Rect mStashedHandleBounds = new Rect();
@@ -96,15 +98,18 @@
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         Resources resources = mActivity.getResources();
         if (isPhoneGestureNavMode(mActivity.getDeviceProfile())) {
-            mStashedHandleView.getLayoutParams().height =
-                    resources.getDimensionPixelSize(R.dimen.taskbar_size);
+            mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_size);
             mStashedHandleWidth =
                     resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
         } else {
-            mStashedHandleView.getLayoutParams().height = deviceProfile.taskbarSize;
+            mTaskbarSize = deviceProfile.taskbarSize;
             mStashedHandleWidth = resources
                     .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
         }
+        int taskbarBottomMargin = DisplayController.isTransientTaskbar(mActivity)
+                ? resources.getDimensionPixelSize(R.dimen.transient_taskbar_margin)
+                : 0;
+        mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin;
 
         mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue(
                 isPhoneGestureNavMode(deviceProfile) ? 1 : 0);
@@ -181,9 +186,17 @@
      * morphs into the size of where the taskbar icons will be.
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed) {
+        Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
+
+        if (DisplayController.isTransientTaskbar(mActivity)) {
+            // Account for the full visual height of the transient taskbar.
+            int heightDiff = (mTaskbarSize - visualBounds.height()) / 2;
+            visualBounds.top -= heightDiff;
+            visualBounds.bottom += heightDiff;
+        }
+
         final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
-                mStashedHandleRadius, mStashedHandleRadius,
-                mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds);
+                mStashedHandleRadius, mStashedHandleRadius, visualBounds, mStashedHandleBounds);
 
         boolean isReversed = !isStashed;
         boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
@@ -220,6 +233,13 @@
     }
 
     /**
+     * Sets the translation of the stashed handle during the swipe up gesture.
+     */
+    protected void setTranslationYForSwipe(float transY) {
+        mStashedHandleView.setTranslationY(transY);
+    }
+
+    /**
      * Should be called when the home button is disabled, so we can hide this handle as well.
      */
     public void setIsHomeButtonDisabled(boolean homeDisabled) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 593605f..3c9e96f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -24,6 +24,8 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 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;
 import static com.android.launcher3.taskbar.TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW;
 import static com.android.launcher3.testing.shared.ResourceUtils.getBoolByName;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
@@ -75,6 +77,8 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag;
+import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.launcher3.testing.TestLogging;
@@ -220,6 +224,7 @@
                 new TaskbarAllAppsController(),
                 new TaskbarInsetsController(this),
                 new VoiceInteractionWindowController(this),
+                new TaskbarTranslationController(this),
                 isDesktopMode
                         ? new DesktopTaskbarRecentAppsController(this)
                         : TaskbarRecentAppsController.DEFAULT);
@@ -297,13 +302,19 @@
         // Taskbar is on the logical bottom of the screen
         boolean isVerticalBarLayout = TaskbarManager.isPhoneMode(deviceProfile) &&
                 deviceProfile.isLandscape;
+
+        int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_SLIPPERY
+                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+        if (DisplayController.isTransientTaskbar(this)) {
+            windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+        }
         WindowManager.LayoutParams windowLayoutParams = new WindowManager.LayoutParams(
                 isVerticalBarLayout ? mLastRequestedNonFullscreenHeight : MATCH_PARENT,
                 isVerticalBarLayout ? MATCH_PARENT : mLastRequestedNonFullscreenHeight,
                 type,
-                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                        | WindowManager.LayoutParams.FLAG_SLIPPERY
-                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+                windowFlags,
                 PixelFormat.TRANSLUCENT);
         windowLayoutParams.setTitle(WINDOW_TITLE);
         windowLayoutParams.packageName = getPackageName();
@@ -467,7 +478,7 @@
 
     @Override
     public void onDragEnd() {
-        maybeSetTaskbarWindowNotFullscreen();
+        onDragEndOrViewRemoved();
     }
 
     @Override
@@ -572,24 +583,33 @@
     }
 
     /**
+     * Called to update a {@link AutohideSuspendFlag} with a new value.
+     */
+    public void setAutohideSuspendFlag(@AutohideSuspendFlag int flag, boolean newValue) {
+        mControllers.taskbarAutohideSuspendController.updateFlag(flag, newValue);
+    }
+
+    /**
      * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
      */
     public void setTaskbarWindowFullscreen(boolean fullscreen) {
-        mControllers.taskbarAutohideSuspendController.updateFlag(
-                TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, fullscreen);
+        setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, fullscreen);
         mIsFullscreen = fullscreen;
         setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : mLastRequestedNonFullscreenHeight);
     }
 
     /**
-     * Reverts Taskbar window to its original size, if all floating views are closed and there is
-     * no system drag operation in progress.
+     * Called when drag ends or when a view is removed from the DragLayer.
      */
-    void maybeSetTaskbarWindowNotFullscreen() {
-        if (AbstractFloatingView.getAnyView(this, TYPE_ALL) == null
-                && !mControllers.taskbarDragController.isSystemDragInProgress()) {
+    void onDragEndOrViewRemoved() {
+        boolean isDragInProgress = mControllers.taskbarDragController.isSystemDragInProgress();
+
+        if (!isDragInProgress && !AbstractFloatingView.hasOpenView(this, TYPE_ALL)) {
+            // Reverts Taskbar window to its original size
             setTaskbarWindowFullscreen(false);
         }
+
+        setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, isDragInProgress);
     }
 
     public boolean isTaskbarWindowFullscreen() {
@@ -703,6 +723,7 @@
             Task task = (Task) tag;
             ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
                     ActivityOptions.makeBasic());
+            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
         } else if (tag instanceof FolderInfo) {
             FolderIcon folderIcon = (FolderIcon) view;
             Folder folder = folderIcon.getFolder();
@@ -762,6 +783,7 @@
                     }
 
                     mControllers.uiController.onTaskbarIconLaunched(info);
+                    mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
                 } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
                     Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
                             .show();
@@ -771,6 +793,7 @@
         } else if (tag instanceof AppInfo) {
             startItemInfoActivity((AppInfo) tag);
             mControllers.uiController.onTaskbarIconLaunched((AppInfo) tag);
+            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
         } else {
             Log.e(TAG, "Unknown type clicked: " + tag);
         }
@@ -806,6 +829,34 @@
     }
 
     /**
+     * Called when we want to unstash taskbar when user performs swipes up gesture.
+     */
+    public void onSwipeToUnstashTaskbar() {
+        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
+    }
+
+    /**
+     * Called to start the taskbar translation spring to its settled translation (0).
+     */
+    public void startTranslationSpring() {
+        mControllers.taskbarTranslationController.startSpring();
+    }
+
+    /**
+     * Returns a callback to help monitor the swipe gesture.
+     */
+    public TransitionCallback getTranslationCallbacks() {
+        return mControllers.taskbarTranslationController.getTransitionCallback();
+    }
+
+    /**
+     * Called when a transient Autohide flag suspend status changes.
+     */
+    public void onTransientAutohideSuspendFlagChanged(boolean isSuspended) {
+        mControllers.taskbarStashController.updateTaskbarTimeout(isSuspended);
+    }
+
+    /**
      * Called when we detect a motion down or up/cancel in the nav region while stashed.
      * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
      */
@@ -928,4 +979,9 @@
         mControllers.dumpLogs(prefix + "\t", pw);
         mDeviceProfile.dump(this, prefix, pw);
     }
+
+    @VisibleForTesting
+    public int getTaskbarAllAppsTopPadding() {
+        return mControllers.taskbarAllAppsController.getTaskbarAllAppsTopPadding();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 3cf9c99..4350e9c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -33,21 +33,28 @@
 public class TaskbarAutohideSuspendController implements
         TaskbarControllers.LoggableTaskbarController {
 
+    // Taskbar window is fullscreen.
     public static final int FLAG_AUTOHIDE_SUSPEND_FULLSCREEN = 1 << 0;
+    // User is dragging item.
     public static final int FLAG_AUTOHIDE_SUSPEND_DRAGGING = 1 << 1;
+    // User has touched down but has not lifted finger.
+    public static final int FLAG_AUTOHIDE_SUSPEND_TOUCHING = 1 << 2;
 
     @IntDef(flag = true, value = {
             FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
             FLAG_AUTOHIDE_SUSPEND_DRAGGING,
+            FLAG_AUTOHIDE_SUSPEND_TOUCHING,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutohideSuspendFlag {}
 
+    private final TaskbarActivityContext mActivity;
     private final SystemUiProxy mSystemUiProxy;
 
     private @AutohideSuspendFlag int mAutohideSuspendFlags = 0;
 
     public TaskbarAutohideSuspendController(TaskbarActivityContext activity) {
+        mActivity = activity;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity);
     }
 
@@ -59,12 +66,27 @@
      * Adds or removes the given flag, then notifies system UI proxy whether to suspend auto-hide.
      */
     public void updateFlag(@AutohideSuspendFlag int flag, boolean enabled) {
+        int flagsBefore = mAutohideSuspendFlags;
         if (enabled) {
             mAutohideSuspendFlags |= flag;
         } else {
             mAutohideSuspendFlags &= ~flag;
         }
-        mSystemUiProxy.notifyTaskbarAutohideSuspend(mAutohideSuspendFlags != 0);
+        if (flagsBefore == mAutohideSuspendFlags) {
+            // Nothing has changed, no need to notify.
+            return;
+        }
+
+        boolean isSuspended = isSuspended();
+        mSystemUiProxy.notifyTaskbarAutohideSuspend(isSuspended);
+        mActivity.onTransientAutohideSuspendFlagChanged(isSuspended);
+    }
+
+    /**
+     * Returns true iff taskbar autohide is currently suspended.
+     */
+    public boolean isSuspended() {
+        return mAutohideSuspendFlags != 0;
     }
 
     @Override
@@ -79,6 +101,7 @@
         appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
                 "FLAG_AUTOHIDE_SUSPEND_FULLSCREEN");
         appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_DRAGGING, "FLAG_AUTOHIDE_SUSPEND_DRAGGING");
+        appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TOUCHING, "FLAG_AUTOHIDE_SUSPEND_TOUCHING");
         return str.toString();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index abd467d..ff7e8e9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -34,6 +34,7 @@
 
     val paint: Paint = Paint()
     var backgroundHeight = context.deviceProfile.taskbarSize.toFloat()
+    var translationYForSwipe = 0f
 
     private var maxBackgroundHeight = context.deviceProfile.taskbarSize.toFloat()
     private val transientBackgroundBounds = context.transientTaskbarBounds
@@ -44,8 +45,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 +68,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()
@@ -94,11 +115,13 @@
             canvas.translate(canvas.width - rightCornerRadius, -rightCornerRadius)
             canvas.drawPath(invertedRightCornerPath, paint)
         } else {
+            // Approximates the stash/unstash animation to transform the background.
             val scaleFactor = backgroundHeight / maxBackgroundHeight
             val width = transientBackgroundBounds.width()
             val widthScale = mapToRange(scaleFactor, 0f, 1f, 0.4f, 1f, Interpolators.LINEAR)
             val newWidth = widthScale * width
             val delta = width - newWidth
+            canvas.translate(0f, bottomMargin * ((1f - scaleFactor) / 2f))
 
             // Draw shadow.
             val shadowAlpha = mapToRange(paint.alpha.toFloat(), 0f, 255f, 0f, 25f,
@@ -112,13 +135,17 @@
 
             canvas.drawRoundRect(
                 transientBackgroundBounds.left + (delta / 2f),
-                0f,
+                translationYForSwipe,
                 transientBackgroundBounds.right - (delta / 2f),
-                backgroundHeight,
+                backgroundHeight + translationYForSwipe,
                 radius, radius, paint
             )
         }
 
         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..0328df9 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;
@@ -55,9 +56,11 @@
     public final TaskbarInsetsController taskbarInsetsController;
     public final VoiceInteractionWindowController voiceInteractionWindowController;
     public final TaskbarRecentAppsController taskbarRecentAppsController;
+    public final TaskbarTranslationController taskbarTranslationController;
     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 +70,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,
@@ -87,6 +93,7 @@
             TaskbarAllAppsController taskbarAllAppsController,
             TaskbarInsetsController taskbarInsetsController,
             VoiceInteractionWindowController voiceInteractionWindowController,
+            TaskbarTranslationController taskbarTranslationController,
             TaskbarRecentAppsController taskbarRecentAppsController) {
         this.taskbarActivityContext = taskbarActivityContext;
         this.taskbarDragController = taskbarDragController;
@@ -108,6 +115,7 @@
         this.taskbarAllAppsController = taskbarAllAppsController;
         this.taskbarInsetsController = taskbarInsetsController;
         this.voiceInteractionWindowController = voiceInteractionWindowController;
+        this.taskbarTranslationController = taskbarTranslationController;
         this.taskbarRecentAppsController = taskbarRecentAppsController;
     }
 
@@ -139,6 +147,7 @@
         taskbarInsetsController.init(this);
         voiceInteractionWindowController.init(this);
         taskbarRecentAppsController.init(this);
+        taskbarTranslationController.init(this);
 
         mControllersToLog = new LoggableTaskbarController[] {
                 taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -146,8 +155,13 @@
                 taskbarUnfoldAnimationController, taskbarKeyguardController,
                 stashedHandleViewController, taskbarStashController, taskbarEduController,
                 taskbarAutohideSuspendController, taskbarPopupController, taskbarInsetsController,
+                voiceInteractionWindowController, taskbarTranslationController
+        };
+        mBackgroundRendererControllers = new BackgroundRendererController[] {
+                taskbarDragLayerController, taskbarScrimViewController,
                 voiceInteractionWindowController
         };
+        mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS);
 
         mAreAllControllersInitialized = true;
         for (Runnable postInitCallback : mPostInitCallbacks) {
@@ -164,6 +178,7 @@
 
     public void onConfigurationChanged(@Config int configChanges) {
         navbarButtonsViewController.onConfigurationChanged(configChanges);
+        taskbarDragLayerController.onConfigurationChanged();
     }
 
     /**
@@ -191,6 +206,7 @@
         taskbarRecentAppsController.onDestroy();
 
         mControllersToLog = null;
+        mBackgroundRendererControllers = null;
     }
 
     /**
@@ -224,6 +240,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 +266,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/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 9a1e064..d7bb16e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -18,10 +18,12 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.NonNull;
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Intent;
@@ -49,7 +51,6 @@
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
@@ -70,6 +71,7 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.quickstep.util.LogUtils;
+import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.systemui.shared.recents.model.Task;
 
 import java.io.PrintWriter;
@@ -83,7 +85,8 @@
 public class TaskbarDragController extends DragController<BaseTaskbarContext> implements
         TaskbarControllers.LoggableTaskbarController {
 
-    private static boolean DEBUG_DRAG_SHADOW_SURFACE = false;
+    private static final boolean DEBUG_DRAG_SHADOW_SURFACE = false;
+    private static final int ANIM_DURATION_RETURN_ICON_TO_TASKBAR = 300;
 
     private final int mDragIconSize;
     private final int[] mTempXY = new int[2];
@@ -99,6 +102,8 @@
 
     // Animation for the drag shadow back into position after an unsuccessful drag
     private ValueAnimator mReturnAnimator;
+    private boolean mDisallowGlobalDrag;
+    private boolean mDisallowLongClick;
 
     public TaskbarDragController(BaseTaskbarContext activity) {
         super(activity);
@@ -110,6 +115,14 @@
         mControllers = controllers;
     }
 
+    public void setDisallowGlobalDrag(boolean disallowGlobalDrag) {
+        mDisallowGlobalDrag = disallowGlobalDrag;
+    }
+
+    public void setDisallowLongClick(boolean disallowLongClick) {
+        mDisallowLongClick = disallowLongClick;
+    }
+
     /**
      * Attempts to start a system drag and drop operation for the given View, using its tag to
      * generate the ClipDescription and Intent.
@@ -131,7 +144,7 @@
             View view,
             @Nullable DragPreviewProvider dragPreviewProvider,
             @Nullable Point iconShift) {
-        if (!(view instanceof BubbleTextView)) {
+        if (!(view instanceof BubbleTextView) || mDisallowLongClick) {
             return false;
         }
         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onTaskbarItemLongClick");
@@ -293,6 +306,7 @@
     }
 
     private void startSystemDrag(BubbleTextView btv) {
+        if (mDisallowGlobalDrag) return;
         View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(btv) {
 
             @Override
@@ -422,6 +436,45 @@
     }
 
     @Override
+    protected void endDrag() {
+        if (mDisallowGlobalDrag) {
+            // We need to explicitly set deferDragViewCleanupPostAnimation to true here so the
+            // super call doesn't remove it from the drag layer before the animation completes.
+            // This variable gets set in to false in super.dispatchDropComplete() because it
+            // (rightfully so, perhaps) thinks this drag operation has failed, and does its own
+            // internal cleanup.
+            // Another way to approach this would be to make all of overview a drop target and
+            // accept the drop as successful and then run the setupReturnDragAnimator to simulate
+            // drop failure to the user
+            mDragObject.deferDragViewCleanupPostAnimation = true;
+
+            float fromX = mDragObject.x - mDragObject.xOffset;
+            float fromY = mDragObject.y - mDragObject.yOffset;
+            DragView dragView = mDragObject.dragView;
+            setupReturnDragAnimator(fromX, fromY, (View) mDragObject.originalView,
+                    (x, y, scale, alpha) -> {
+                        dragView.setTranslationX(x);
+                        dragView.setTranslationY(y);
+                        dragView.setScaleX(scale);
+                        dragView.setScaleY(scale);
+                        dragView.setAlpha(alpha);
+                    });
+            mReturnAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    callOnDragEnd();
+                    dragView.remove();
+                    dragView.clearAnimation();
+                    mReturnAnimator = null;
+
+                }
+            });
+            mReturnAnimator.start();
+        }
+        super.endDrag();
+    }
+
+    @Override
     protected void callOnDragEnd() {
         super.callOnDragEnd();
         maybeOnDragEnd();
@@ -432,56 +485,20 @@
         SurfaceControl dragSurface = dragEvent.getDragSurface();
 
         // For top level icons, the target is the icon itself
-        View target = btv;
-        Object tag = btv.getTag();
-        if (tag instanceof ItemInfo) {
-            ItemInfo item = (ItemInfo) tag;
-            TaskbarViewController taskbarViewController = mControllers.taskbarViewController;
-            if (item.container == CONTAINER_ALL_APPS || item.container == CONTAINER_PREDICTION) {
-                // Since all apps closes when the drag starts, target the all apps button instead.
-                target = taskbarViewController.getAllAppsButtonView();
-            } else if (item.container >= 0) {
-                // Since folders close when the drag starts, target the folder icon instead.
-                Predicate<ItemInfo> matcher = ItemInfoMatcher.forFolderMatch(
-                        ItemInfoMatcher.ofItemIds(IntSet.wrap(item.id)));
-                target = taskbarViewController.getFirstIconMatch(matcher);
-            } else if (item.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
-                // Find first icon with same package/user as the deep shortcut.
-                Predicate<ItemInfo> packageUserMatcher = ItemInfoMatcher.ofPackages(
-                        Collections.singleton(item.getTargetPackage()), item.user);
-                target = taskbarViewController.getFirstIconMatch(packageUserMatcher);
-            }
-        }
-
-        // Finish any pending return animation before starting a new drag
-        if (mReturnAnimator != null) {
-            mReturnAnimator.end();
-        }
+        View target = findTaskbarTargetForIconView(btv);
 
         float fromX = dragEvent.getX() - dragEvent.getOffsetX();
         float fromY = dragEvent.getY() - dragEvent.getOffsetY();
-        int[] toPosition = target.getLocationOnScreen();
-        float toScale = (float) target.getWidth() / mDragIconSize;
-        float toAlpha = (target == btv) ? 1f : 0f;
         final ViewRootImpl viewRoot = target.getViewRootImpl();
         SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-        mReturnAnimator = ValueAnimator.ofFloat(0f, 1f);
-        mReturnAnimator.setDuration(300);
-        mReturnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        mReturnAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                float t = animation.getAnimatedFraction();
-                float accelT = Interpolators.ACCEL_2.getInterpolation(t);
-                float scale = 1f - t * (1f - toScale);
-                float alpha = 1f - accelT * (1f - toAlpha);
-                tx.setPosition(dragSurface, Utilities.mapRange(t, fromX, toPosition[0]),
-                        Utilities.mapRange(t, fromY, toPosition[1]));
-                tx.setScale(dragSurface, scale, scale);
-                tx.setAlpha(dragSurface, alpha);
-                tx.apply();
-            }
-        });
+        setupReturnDragAnimator(fromX, fromY, btv,
+                (x, y, scale, alpha) -> {
+                    tx.setPosition(dragSurface, x, y);
+                    tx.setScale(dragSurface, scale, scale);
+                    tx.setAlpha(dragSurface, alpha);
+                    tx.apply();
+                });
+
         mReturnAnimator.addListener(new AnimatorListenerAdapter() {
             private boolean mCanceled = false;
 
@@ -517,6 +534,63 @@
         mReturnAnimator.start();
     }
 
+    private View findTaskbarTargetForIconView(@NonNull View iconView) {
+        Object tag = iconView.getTag();
+        if (tag instanceof ItemInfo) {
+            ItemInfo item = (ItemInfo) tag;
+            TaskbarViewController taskbarViewController = mControllers.taskbarViewController;
+            if (item.container == CONTAINER_ALL_APPS || item.container == CONTAINER_PREDICTION) {
+                // Since all apps closes when the drag starts, target the all apps button instead.
+                return taskbarViewController.getAllAppsButtonView();
+            } else if (item.container >= 0) {
+                // Since folders close when the drag starts, target the folder icon instead.
+                Predicate<ItemInfo> matcher = ItemInfoMatcher.forFolderMatch(
+                        ItemInfoMatcher.ofItemIds(IntSet.wrap(item.id)));
+                return taskbarViewController.getFirstIconMatch(matcher);
+            } else if (item.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
+                // Find first icon with same package/user as the deep shortcut.
+                Predicate<ItemInfo> packageUserMatcher = ItemInfoMatcher.ofPackages(
+                        Collections.singleton(item.getTargetPackage()), item.user);
+                return taskbarViewController.getFirstIconMatch(packageUserMatcher);
+            }
+        }
+        return iconView;
+    }
+
+    private void setupReturnDragAnimator(float fromX, float fromY, View originalView,
+            TaskbarReturnPropertiesListener animListener) {
+        // Finish any pending return animation before starting a new return
+        if (mReturnAnimator != null) {
+            mReturnAnimator.end();
+        }
+
+        // For top level icons, the target is the icon itself
+        View target = findTaskbarTargetForIconView(originalView);
+
+        int[] toPosition = target.getLocationOnScreen();
+        float toScale = (float) target.getWidth() / mDragIconSize;
+        float toAlpha = (target == originalView) ? 1f : 0f;
+        MultiValueUpdateListener listener = new MultiValueUpdateListener() {
+            final FloatProp mDx = new FloatProp(fromX, toPosition[0], 0,
+                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.FAST_OUT_SLOW_IN);
+            final FloatProp mDy = new FloatProp(fromY, toPosition[1], 0,
+                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR,
+                    FAST_OUT_SLOW_IN);
+            final FloatProp mScale = new FloatProp(1f, toScale, 0,
+                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, FAST_OUT_SLOW_IN);
+            final FloatProp mAlpha = new FloatProp(1f, toAlpha, 0,
+                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.ACCEL_2);
+            @Override
+            public void onUpdate(float percent, boolean initOnly) {
+                animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value);
+            }
+        };
+        mReturnAnimator = ValueAnimator.ofFloat(0f, 1f);
+        mReturnAnimator.setDuration(ANIM_DURATION_RETURN_ICON_TO_TASKBAR);
+        mReturnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mReturnAnimator.addUpdateListener(listener);
+    }
+
     @Override
     protected float getX(MotionEvent ev) {
         // We will resize to fill the screen while dragging, so use screen coordinates. This ensures
@@ -540,7 +614,7 @@
 
     @Override
     protected void exitDrag() {
-        if (mDragObject != null) {
+        if (mDragObject != null && !mDisallowGlobalDrag) {
             mActivity.getDragLayer().removeView(mDragObject.dragView);
         }
     }
@@ -556,6 +630,10 @@
         return null;
     }
 
+    interface TaskbarReturnPropertiesListener {
+        void updateDragShadow(float x, float y, float scale, float alpha);
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarDragController:");
@@ -566,5 +644,7 @@
         pw.println(prefix + "\tmRegistrationY=" + mRegistrationY);
         pw.println(prefix + "\tmIsSystemDragInProgress=" + mIsSystemDragInProgress);
         pw.println(prefix + "\tisInternalDragInProgess=" + super.isDragging());
+        pw.println(prefix + "\tmDisallowGlobalDrag=" + mDisallowGlobalDrag);
+        pw.println(prefix + "\tmDisallowLongClick=" + mDisallowLongClick);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 7e75779..d0059f7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -116,6 +116,22 @@
     }
 
     @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mControllerCallbacks != null) {
+            mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mControllerCallbacks != null) {
+            mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         if (mControllerCallbacks != null) {
@@ -150,6 +166,23 @@
         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();
+    }
+
+    /*
+     * Sets the translation of the background during the swipe up gesture.
+     */
+    protected void setBackgroundTranslationYForSwipe(float translationY) {
+        mBackgroundRenderer.setTranslationYForSwipe(translationY);
+        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 353f1e0..e54fc00 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -18,11 +18,14 @@
 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;
 import com.android.quickstep.AnimatedFloat;
 
@@ -31,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);
@@ -62,6 +67,7 @@
         mTaskbarDragLayer = taskbarDragLayer;
         final Resources resources = mTaskbarDragLayer.getResources();
         mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+        updateGestureHeight();
     }
 
     public void init(TaskbarControllers controllers) {
@@ -121,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
@@ -131,12 +150,24 @@
         updateNavBarDarkIntensityMultiplier();
     }
 
+    /**
+     * Sets the translation of the background during the swipe up gesture.
+     */
+    public void setTranslationYForSwipe(float transY) {
+        mTaskbarDragLayer.setBackgroundTranslationYForSwipe(transY);
+    }
+
     private void updateBackgroundOffset() {
         mTaskbarDragLayer.setTaskbarBackgroundOffset(mBgOffset.value);
 
         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);
@@ -157,6 +188,8 @@
      */
     public class TaskbarDragLayerCallbacks {
 
+        private final int[] mTempOutLocation = new int[2];
+
         /**
          * Called to update the touchable insets.
          * @see ViewTreeObserver.InternalInsetsInfo#setTouchableInsets(int)
@@ -166,10 +199,41 @@
         }
 
         /**
+         * Listens to TaskbarDragLayer touch events and responds accordingly.
+         */
+        public void tryStashBasedOnMotionEvent(MotionEvent ev) {
+            if (!DisplayController.isTransientTaskbar(mActivity)) {
+                return;
+            }
+            if (mControllers.taskbarStashController.isStashed()) {
+                return;
+            }
+
+            boolean stashTaskbar = false;
+
+            MotionEvent screenCoordinates = MotionEvent.obtain(ev);
+            if (ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
+                stashTaskbar = true;
+            } else if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                mTaskbarDragLayer.getLocationOnScreen(mTempOutLocation);
+                screenCoordinates.offsetLocation(mTempOutLocation[0], mTempOutLocation[1]);
+
+                if (!mControllers.taskbarViewController.isEventOverAnyItem(screenCoordinates)
+                        && screenCoordinates.getY() < mGestureHeightYThreshold) {
+                    stashTaskbar = true;
+                }
+            }
+
+            if (stashTaskbar) {
+                mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
+            }
+        }
+
+        /**
          * Called when a child is removed from TaskbarDragLayer.
          */
         public void onDragLayerViewRemoved() {
-            mActivity.maybeSetTaskbarWindowNotFullscreen();
+            mActivity.onDragEndOrViewRemoved();
         }
 
         /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java
index 16cc0ac..365ec75 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java
@@ -219,7 +219,7 @@
         }
 
         int getIconLayoutBoundsWidth() {
-            return mControllers.taskbarViewController.getIconLayoutBounds().width();
+            return mControllers.taskbarViewController.getIconLayoutWidth();
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 63f1486..fc26f5f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -38,6 +38,7 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.RecentsAnimationCallbacks;
@@ -72,6 +73,7 @@
 
     private TaskbarControllers mControllers;
     private AnimatedFloat mTaskbarBackgroundAlpha;
+    private AnimatedFloat mTaskbarCornerRoundness;
     private MultiProperty mIconAlphaForHome;
     private QuickstepLauncher mLauncher;
 
@@ -117,6 +119,10 @@
                     mLauncherState = finalState;
                     updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false);
                     applyState();
+                    mControllers.taskbarDragController.setDisallowGlobalDrag(
+                            (finalState instanceof OverviewState));
+                    mControllers.taskbarDragController.setDisallowLongClick(
+                            finalState == LauncherState.OVERVIEW_SPLIT_SELECT);
                 }
             };
 
@@ -128,6 +134,7 @@
 
         mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
                 .getTaskbarBackgroundAlpha();
+        mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
         mIconAlphaForHome = mControllers.taskbarViewController
                 .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);
 
@@ -245,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)
@@ -321,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.
@@ -338,6 +348,7 @@
             }
             animatorSet.play(iconAlignAnim);
         }
+
         animatorSet.setInterpolator(EMPHASIZED);
 
         if (start) {
@@ -351,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);
@@ -389,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 64eb99e..72ae1d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -39,11 +39,13 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.launcher3.Alarm;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.SystemUiProxy;
@@ -70,6 +72,7 @@
     public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 7; // All apps is visible.
     public static final int FLAG_IN_SETUP = 1 << 8; // In the Setup Wizard
     public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 9; // phone screen gesture nav, stashed
+    public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 10; // Autohide (transient taskbar).
 
     // If any of these flags are enabled, isInApp should return true.
     private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -78,7 +81,7 @@
     private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL
             | FLAG_STASHED_IN_APP_PINNED | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP
             | FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS
-            | FLAG_STASHED_SMALL_SCREEN;
+            | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO;
 
     private static final int FLAGS_STASHED_IN_APP_IGNORING_IME =
             FLAGS_STASHED_IN_APP & ~FLAG_STASHED_IN_APP_IME;
@@ -132,6 +135,9 @@
      */
     private static final boolean DEFAULT_STASHED_PREF = false;
 
+    // Auto stashes when user has not interacted with the Taskbar after X ms.
+    private static final long NO_TOUCH_TIMEOUT_TO_STASH_MS = 5000;
+
     private final TaskbarActivityContext mActivity;
     private final SharedPreferences mPrefs;
     private final int mStashedHeight;
@@ -162,6 +168,8 @@
 
     private boolean mEnableManualStashingDuringTests = false;
 
+    private final Alarm mTimeoutAlarm = new Alarm();
+
     // Evaluate whether the handle should be stashed
     private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
             flags -> {
@@ -182,8 +190,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;
@@ -210,13 +223,16 @@
                 StashedHandleViewController.ALPHA_INDEX_STASHED);
         mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale();
 
+        boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
         // We use supportsVisualStashing() here instead of supportsManualStashing() because we want
         // it to work properly for tests that recreate taskbar. This check is here just to ensure
         // that taskbar unstashes when going to 3 button mode (supportsVisualStashing() false).
         boolean isManuallyStashedInApp = supportsVisualStashing()
-                && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF);
+                && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF)
+                && !isTransientTaskbar;
         boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
         updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp);
+        updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isTransientTaskbar);
         updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup);
         updateStateForFlag(FLAG_IN_SETUP, isInSetup);
         updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, isPhoneMode()
@@ -243,7 +259,8 @@
     protected boolean supportsManualStashing() {
         return supportsVisualStashing()
                 && isInApp()
-                && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || mEnableManualStashingDuringTests);
+                && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || mEnableManualStashingDuringTests)
+                && !DisplayController.isTransientTaskbar(mActivity);
     }
 
     /**
@@ -332,7 +349,8 @@
      * @see WindowInsets.Type#systemBars()
      */
     public int getContentHeightToReportToApps() {
-        if (isPhoneMode() && !mActivity.isThreeButtonNav()) {
+        if ((isPhoneMode() && !mActivity.isThreeButtonNav())
+                || DisplayController.isTransientTaskbar(mActivity)) {
             return getStashedHeight();
         }
 
@@ -377,6 +395,20 @@
     }
 
     /**
+     * Stash or unstashes the transient taskbar.
+     */
+    public void updateAndAnimateTransientTaskbar(boolean stash) {
+        if (!DisplayController.isTransientTaskbar(mActivity)) {
+            return;
+        }
+
+        if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) {
+            updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
+            applyState();
+        }
+    }
+
+    /**
      * Should be called when long pressing the nav region when taskbar is present.
      * @return Whether taskbar was stashed and now is unstashed.
      */
@@ -549,11 +581,17 @@
             public void onAnimationStart(Animator animation) {
                 mIsStashed = isStashed;
                 onIsStashedChanged(mIsStashed);
+
+                cancelTimeoutIfExists();
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
                 mAnimator = null;
+
+                if (!mIsStashed) {
+                    tryStartTaskbarTimeout();
+                }
             }
         });
     }
@@ -779,6 +817,54 @@
         mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed);
     }
 
+    /**
+     * Cancels a timeout if any exists.
+     */
+    public void cancelTimeoutIfExists() {
+        if (mTimeoutAlarm.alarmPending()) {
+            mTimeoutAlarm.cancelAlarm();
+        }
+    }
+
+    /**
+     * Updates the status of the taskbar timeout.
+     * @param isAutohideSuspended If true, cancels any existing timeout
+     *                            If false, attempts to re/start the timeout
+     */
+    public void updateTaskbarTimeout(boolean isAutohideSuspended) {
+        if (!DisplayController.isTransientTaskbar(mActivity)) {
+            return;
+        }
+        if (isAutohideSuspended) {
+            cancelTimeoutIfExists();
+        } else {
+            tryStartTaskbarTimeout();
+        }
+    }
+
+    /**
+     * Attempts to start timer to auto hide the taskbar based on time.
+     */
+    public void tryStartTaskbarTimeout() {
+        if (!DisplayController.isTransientTaskbar(mActivity)) {
+            return;
+        }
+        if (mIsStashed) {
+            return;
+        }
+        cancelTimeoutIfExists();
+
+        mTimeoutAlarm.setOnAlarmListener(this::onTaskbarTimeout);
+        mTimeoutAlarm.setAlarm(NO_TOUCH_TIMEOUT_TO_STASH_MS);
+    }
+
+    private void onTaskbarTimeout(Alarm alarm) {
+        if (mControllers.taskbarAutohideSuspendController.isSuspended()) {
+            return;
+        }
+        updateAndAnimateTransientTaskbar(true);
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarStashController:");
@@ -794,17 +880,18 @@
     }
 
     private static String getStateString(int flags) {
-        StringJoiner str = new StringJoiner("|");
-        appendFlag(str, flags, FLAGS_IN_APP, "FLAG_IN_APP");
-        appendFlag(str, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL");
-        appendFlag(str, flags, FLAG_STASHED_IN_APP_PINNED, "FLAG_STASHED_IN_APP_PINNED");
-        appendFlag(str, flags, FLAG_STASHED_IN_APP_EMPTY, "FLAG_STASHED_IN_APP_EMPTY");
-        appendFlag(str, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP");
-        appendFlag(str, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME");
-        appendFlag(str, flags, FLAG_IN_STASHED_LAUNCHER_STATE, "FLAG_IN_STASHED_LAUNCHER_STATE");
-        appendFlag(str, flags, FLAG_STASHED_IN_TASKBAR_ALL_APPS, "FLAG_STASHED_IN_APP_ALL_APPS");
-        appendFlag(str, flags, FLAG_IN_SETUP, "FLAG_IN_SETUP");
-        return str.toString();
+        StringJoiner sj = new StringJoiner("|");
+        appendFlag(sj, flags, FLAGS_IN_APP, "FLAG_IN_APP");
+        appendFlag(sj, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL");
+        appendFlag(sj, flags, FLAG_STASHED_IN_APP_PINNED, "FLAG_STASHED_IN_APP_PINNED");
+        appendFlag(sj, flags, FLAG_STASHED_IN_APP_EMPTY, "FLAG_STASHED_IN_APP_EMPTY");
+        appendFlag(sj, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP");
+        appendFlag(sj, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME");
+        appendFlag(sj, flags, FLAG_IN_STASHED_LAUNCHER_STATE, "FLAG_IN_STASHED_LAUNCHER_STATE");
+        appendFlag(sj, flags, FLAG_STASHED_IN_TASKBAR_ALL_APPS, "FLAG_STASHED_IN_TASKBAR_ALL_APPS");
+        appendFlag(sj, flags, FLAG_IN_SETUP, "FLAG_IN_SETUP");
+        appendFlag(sj, flags, FLAG_STASHED_IN_APP_AUTO, "FLAG_STASHED_IN_APP_AUTO");
+        return sj.toString();
     }
 
     private class StatePropertyHolder {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
new file mode 100644
index 0000000..1a7ec13
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -0,0 +1,197 @@
+/*
+ * 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.launcher3.taskbar;
+
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.quickstep.AnimatedFloat.VALUE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.AnimatedFloat;
+
+import java.io.PrintWriter;
+
+/**
+ * Class responsible for translating the transient taskbar UI during a swipe gesture.
+ *
+ * The translation is controlled, in priority order:
+ * - animation to home
+ * - a spring animation
+ * - controlled by user
+ *
+ * The spring animation will play start once the user lets go or when user pauses to go to overview.
+ * When the user goes home, the stash animation will play.
+ */
+public class TaskbarTranslationController implements TaskbarControllers.LoggableTaskbarController {
+
+    private final TaskbarActivityContext mContext;
+    private TaskbarControllers mControllers;
+    private final AnimatedFloat mTranslationYForSwipe = new AnimatedFloat(
+            this::updateTranslationYForSwipe);
+
+    private boolean mHasSprungOnceThisGesture;
+    private @Nullable ValueAnimator mSpringBounce;
+    private boolean mGestureEnded;
+    private boolean mAnimationToHomeRunning;
+
+    private final boolean mIsTransientTaskbar;
+
+    private final TransitionCallback mCallback;
+
+    public TaskbarTranslationController(TaskbarActivityContext context) {
+        mContext = context;
+        mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
+        mCallback = new TransitionCallback();
+    }
+
+    /**
+     * Initialization method.
+     */
+    public void init(TaskbarControllers controllers) {
+        mControllers = controllers;
+    }
+
+    /**
+     * Called to cancel any existing animations.
+     */
+    public void cancelAnimationIfExists() {
+        if (mSpringBounce != null) {
+            mSpringBounce.cancel();
+            mSpringBounce = null;
+        }
+        reset();
+    }
+
+    private void updateTranslationYForSwipe() {
+        if (!mIsTransientTaskbar) {
+            return;
+        }
+
+        float transY = mTranslationYForSwipe.value;
+        mControllers.stashedHandleViewController.setTranslationYForSwipe(transY);
+        mControllers.taskbarViewController.setTranslationYForSwipe(transY);
+        mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
+    }
+
+    /**
+     * Starts a spring aniamtion to set the views back to the resting state.
+     */
+    public void startSpring() {
+        if (mHasSprungOnceThisGesture || mAnimationToHomeRunning) {
+            return;
+        }
+        mSpringBounce = new SpringAnimationBuilder(mContext)
+                .setStartValue(mTranslationYForSwipe.value)
+                .setEndValue(0)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                .setStiffness(SpringForce.STIFFNESS_LOW)
+                .build(mTranslationYForSwipe, VALUE);
+        mSpringBounce.addListener(forEndCallback(() -> {
+            if (mGestureEnded) {
+                reset();
+            }
+        }));
+        mSpringBounce.start();
+        mHasSprungOnceThisGesture = true;
+    }
+
+    private void reset() {
+        mGestureEnded = false;
+        mHasSprungOnceThisGesture = false;
+    }
+
+    /**
+     * Returns a callback to help monitor the swipe gesture.
+     */
+    public TransitionCallback getTransitionCallback() {
+        return mCallback;
+    }
+
+    /**
+     * Returns an animation to reset the taskbar translation for animation back to launcher.
+     */
+    public ObjectAnimator createAnimToLauncher(long duration) {
+        ObjectAnimator animator = ObjectAnimator.ofFloat(mTranslationYForSwipe, VALUE, 0);
+        animator.setInterpolator(Interpolators.LINEAR);
+        animator.setDuration(duration);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                cancelAnimationIfExists();
+                mAnimationToHomeRunning = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimationToHomeRunning = false;
+                reset();
+            }
+        });
+        return animator;
+    }
+
+    /**
+     * Helper class to communicate to/from  the input consumer.
+     */
+    public class TransitionCallback {
+
+        /**
+         * Called when there is movement to move the taskbar.
+         */
+        public void onActionMove(float dY) {
+            if (mAnimationToHomeRunning
+                    || (mHasSprungOnceThisGesture && !mGestureEnded)) {
+                return;
+            }
+
+            mTranslationYForSwipe.updateValue(dY);
+        }
+
+        /**
+         * Called when swipe gesture has ended.
+         */
+        public void onActionEnd() {
+            if (mHasSprungOnceThisGesture) {
+                reset();
+            } else {
+                mGestureEnded = true;
+                startSpring();
+            }
+        }
+    }
+
+    @Override
+    public void dumpLogs(String prefix, PrintWriter pw) {
+        pw.println(prefix + "TaskbarTranslationController:");
+
+        pw.println(prefix + "\tmTranslationYForSwipe=" + mTranslationYForSwipe.value);
+        pw.println(prefix + "\tmHasSprungOnceThisGesture=" + mHasSprungOnceThisGesture);
+        pw.println(prefix + "\tmAnimationToHomeRunning=" + mAnimationToHomeRunning);
+        pw.println(prefix + "\tmGestureEnded=" + mGestureEnded);
+        pw.println(prefix + "\tmSpringBounce is running=" + (mSpringBounce != null
+                && mSpringBounce.isRunning()));
+    }
+}
+
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 49dba95..1014cb6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import android.view.MotionEvent;
 import android.view.View;
 
 import androidx.annotation.CallSuper;
@@ -97,6 +98,36 @@
         }
     }
 
+    /**
+     * Returns true iff taskbar is stashed.
+     */
+    public boolean isTaskbarStashed() {
+        return mControllers.taskbarStashController.isStashed();
+    }
+
+    /**
+     * Called at the end of the swipe gesture on Transient taskbar.
+     */
+    public void startTranslationSpring() {
+        mControllers.taskbarActivityContext.startTranslationSpring();
+    }
+
+    /*
+     * @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);
+    }
+
+    /**
+     * 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/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c754a59..fe38bb1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -16,14 +16,11 @@
 package com.android.launcher3.taskbar;
 
 import android.content.Context;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Rect;
-import android.os.SystemProperties;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -85,12 +82,6 @@
 
     private View mQsb;
 
-    // Only non-null when device supports having a floating task.
-    private @Nullable View mFloatingTaskButton;
-    private @Nullable Intent mFloatingTaskIntent;
-    private static final boolean FLOATING_TASKS_ENABLED =
-            SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
-
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -139,17 +130,6 @@
         // TODO: Disable touch events on QSB otherwise it can crash.
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
 
-        if (FLOATING_TASKS_ENABLED) {
-            mFloatingTaskIntent = FloatingTaskIntentResolver.getIntent(context);
-            if (mFloatingTaskIntent != null) {
-                mFloatingTaskButton = LayoutInflater.from(context)
-                        .inflate(R.layout.taskbar_floating_task_button, this, false);
-                mFloatingTaskButton.setPadding(mItemPadding, mItemPadding, mItemPadding,
-                        mItemPadding);
-            } else {
-                Log.d(TAG, "Floating tasks is enabled but no intent was found!");
-            }
-        }
     }
 
     private int getColorWithGivenLuminance(int color, float luminance) {
@@ -177,10 +157,6 @@
         if (mAllAppsButton != null) {
             mAllAppsButton.setOnClickListener(mControllerCallbacks.getAllAppsButtonClickListener());
         }
-        if (mFloatingTaskButton != null) {
-            mFloatingTaskButton.setOnClickListener(
-                    mControllerCallbacks.getFloatingTaskButtonListener(mFloatingTaskIntent));
-        }
     }
 
     private void removeAndRecycle(View view) {
@@ -205,9 +181,6 @@
         }
         removeView(mQsb);
 
-        if (mFloatingTaskButton != null) {
-            removeView(mFloatingTaskButton);
-        }
 
         for (int i = 0; i < hotseatItemInfos.length; i++) {
             ItemInfo hotseatItemInfo = hotseatItemInfos[i];
@@ -290,11 +263,6 @@
             mQsb.setVisibility(View.INVISIBLE);
         }
 
-        if (mFloatingTaskButton != null) {
-            int index = Utilities.isRtl(getResources()) ? 0 : getChildCount();
-            addView(mFloatingTaskButton, index);
-        }
-
         mThemeIconsBackground = calculateThemeIconsBackground();
         setThemedIconsBackgroundColor(mThemeIconsBackground);
     }
@@ -325,12 +293,8 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         int count = getChildCount();
-        int countExcludingQsb = count;
         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
-        if (deviceProfile.isQsbInline) {
-            countExcludingQsb--;
-        }
-        int spaceNeeded = countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
+        int spaceNeeded = getIconLayoutWidth();
         int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
         boolean layoutRtl = isLayoutRtl();
         int iconEnd = right - (right - left - spaceNeeded) / 2;
@@ -381,12 +345,22 @@
     }
 
     @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        mControllerCallbacks.onInterceptTouchEvent(ev);
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (!mTouchEnabled) {
             return true;
         }
-        if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) {
-            // Don't allow long pressing between icons, or above/below them.
+        if (mIconLayoutBounds.left <= event.getX()
+                && event.getX() <= mIconLayoutBounds.right
+                && !DisplayController.isTransientTaskbar(mActivityContext)) {
+            // Don't allow long pressing between icons, or above/below them
+            // unless its transient taskbar.
+            mControllerCallbacks.clearTouchInProgress();
             return true;
         }
         if (mControllerCallbacks.onTouchEvent(event)) {
@@ -403,6 +377,7 @@
 
     public void setTouchesEnabled(boolean touchEnabled) {
         this.mTouchEnabled = touchEnabled;
+        mControllerCallbacks.clearTouchInProgress();
     }
 
     /**
@@ -421,6 +396,18 @@
     }
 
     /**
+     * Returns the space used by the icons
+     */
+    public int getIconLayoutWidth() {
+        int countExcludingQsb = getChildCount();
+        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+        if (deviceProfile.isQsbInline) {
+            countExcludingQsb--;
+        }
+        return countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
+    }
+
+    /**
      * Returns the app icons currently shown in the taskbar.
      */
     public View[] getIconViews() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index f0277a4..80a31b4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -22,16 +22,18 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
 import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 import static com.android.quickstep.AnimatedFloat.VALUE;
 
 import android.annotation.NonNull;
-import android.content.Intent;
 import android.graphics.Rect;
 import android.util.FloatProperty;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.Nullable;
 import androidx.core.graphics.ColorUtils;
 import androidx.core.view.OneShotPreDrawListener;
 
@@ -48,6 +50,7 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.ThemedIconDrawable;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.HorizontalInsettableView;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -55,7 +58,6 @@
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.SystemUiProxy;
 
 import java.io.PrintWriter;
 import java.util.function.Predicate;
@@ -89,6 +91,7 @@
             this::updateTranslationY);
     private AnimatedFloat mTaskbarNavButtonTranslationY;
     private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay;
+    private float mTaskbarIconTranslationYForSwipe;
 
     private final int mTaskbarBottomMargin;
 
@@ -97,6 +100,9 @@
 
     private final TaskbarModelCallbacks mModelCallbacks;
 
+    // Captures swipe down action to close transient taskbar.
+    protected @Nullable SingleAxisSwipeDetector mSwipeDownDetector;
+
     // Initialized in init.
     private TaskbarControllers mControllers;
 
@@ -119,6 +125,31 @@
         mTaskbarBottomMargin = DisplayController.isTransientTaskbar(activity)
                 ? activity.getResources().getDimensionPixelSize(R.dimen.transient_taskbar_margin)
                 : 0;
+
+        if (DisplayController.isTransientTaskbar(mActivity)) {
+            mSwipeDownDetector = new SingleAxisSwipeDetector(activity,
+                    new SingleAxisSwipeDetector.Listener() {
+                        private float mLastDisplacement;
+
+                        @Override
+                        public boolean onDrag(float displacement) {
+                            mLastDisplacement = displacement;
+                            return false;
+                        }
+
+                        @Override
+                        public void onDragEnd(float velocity) {
+                            if (mLastDisplacement > 0) {
+                                mControllers.taskbarStashController
+                                        .updateAndAnimateTransientTaskbar(true);
+                            }
+                        }
+
+                        @Override
+                        public void onDragStart(boolean start, float startDisplacement) {}
+                    }, VERTICAL);
+            mSwipeDownDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
+        }
     }
 
     public void init(TaskbarControllers controllers) {
@@ -201,6 +232,10 @@
         return mTaskbarView.getIconLayoutBounds();
     }
 
+    public int getIconLayoutWidth() {
+        return mTaskbarView.getIconLayoutWidth();
+    }
+
     public View[] getIconViews() {
         return mTaskbarView.getIconViews();
     }
@@ -226,9 +261,18 @@
         mTaskbarView.setScaleY(scale);
     }
 
+    /**
+     * Sets the translation of the TaskbarView during the swipe up gesture.
+     */
+    public void setTranslationYForSwipe(float transY) {
+        mTaskbarIconTranslationYForSwipe = transY;
+        updateTranslationY();
+    }
+
     private void updateTranslationY() {
         mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value
-                + mTaskbarIconTranslationYForStash.value);
+                + mTaskbarIconTranslationYForStash.value
+                + mTaskbarIconTranslationYForSwipe);
     }
 
     private void updateIconsBackground() {
@@ -246,10 +290,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) {
@@ -261,8 +304,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();
@@ -288,7 +330,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;
@@ -436,6 +478,8 @@
         private float mDownX, mDownY;
         private boolean mCanceledStashHint;
 
+        private boolean mTouchInProgress;
+
         public View.OnClickListener getIconOnClickListener() {
             return mActivity.getItemOnClickListener();
         }
@@ -447,13 +491,6 @@
             };
         }
 
-        public View.OnClickListener getFloatingTaskButtonListener(@NonNull Intent intent) {
-            return v -> {
-                SystemUiProxy proxy = SystemUiProxy.INSTANCE.get(v.getContext());
-                proxy.showFloatingTask(intent);
-            };
-        }
-
         public View.OnLongClickListener getIconOnLongClickListener() {
             return mControllers.taskbarDragController::startDragOnLongClick;
         }
@@ -464,37 +501,75 @@
         }
 
         /**
+         * Simply listens to all intercept touch events passed to TaskbarView.
+         */
+        public void onInterceptTouchEvent(MotionEvent ev) {
+            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                mTouchInProgress = true;
+            }
+
+            if (mTouchInProgress && mSwipeDownDetector != null) {
+                mSwipeDownDetector.onTouchEvent(ev);
+            }
+
+            if (ev.getAction() == MotionEvent.ACTION_UP
+                    || ev.getAction() == MotionEvent.ACTION_CANCEL) {
+                clearTouchInProgress();
+            }
+        }
+
+        /**
          * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
          * consume the touch so TaskbarView treats it as an ACTION_CANCEL.
          */
         public boolean onTouchEvent(MotionEvent motionEvent) {
+            boolean shouldConsumeTouch = false;
+            boolean clearTouchInProgress = false;
+
             final float x = motionEvent.getRawX();
             final float y = motionEvent.getRawY();
             switch (motionEvent.getAction()) {
                 case MotionEvent.ACTION_DOWN:
+                    mTouchInProgress = true;
                     mDownX = x;
                     mDownY = y;
                     mControllers.taskbarStashController.startStashHint(/* animateForward = */ true);
                     mCanceledStashHint = false;
                     break;
                 case MotionEvent.ACTION_MOVE:
-                    if (!mCanceledStashHint
+                    if (mTouchInProgress
+                            && !mCanceledStashHint
                             && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
                         mControllers.taskbarStashController.startStashHint(
                                 /* animateForward= */ false);
                         mCanceledStashHint = true;
-                        return true;
+                        shouldConsumeTouch = true;
                     }
                     break;
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL:
-                    if (!mCanceledStashHint) {
+                    if (mTouchInProgress && !mCanceledStashHint) {
                         mControllers.taskbarStashController.startStashHint(
                                 /* animateForward= */ false);
                     }
+                    clearTouchInProgress = true;
                     break;
             }
-            return false;
+
+            if (mTouchInProgress && mSwipeDownDetector != null) {
+                mSwipeDownDetector.onTouchEvent(motionEvent);
+            }
+            if (clearTouchInProgress) {
+                clearTouchInProgress();
+            }
+            return shouldConsumeTouch;
+        }
+
+        /**
+         * Ensures that we do not pass any more touch events to the SwipeDetector.
+         */
+        public void clearTouchInProgress() {
+            mTouchInProgress = false;
         }
     }
 
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/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index ea37944..85c6318 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar.allapps;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.appprediction.PredictionRowView;
@@ -123,4 +124,11 @@
                 .findFixedRowByType(PredictionRowView.class)
                 .setPredictedApps(mPredictedApps);
     }
+
+
+    @VisibleForTesting
+    public int getTaskbarAllAppsTopPadding() {
+        // Allow null-pointer since this should only be null if the apps view is not showing.
+        return mAppsView.getActiveRecyclerView().getClipBounds().top;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index eaf577b..9813c8a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -70,6 +70,7 @@
 import android.view.WindowManagerGlobal;
 import android.window.SplashScreen;
 
+import androidx.annotation.BinderThread;
 import androidx.annotation.Nullable;
 
 import com.android.app.viewcapture.ViewCapture;
@@ -135,6 +136,7 @@
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
 import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SplitWithKeyboardShortcutController;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
@@ -179,6 +181,8 @@
     private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
     private @Nullable RotationChangeProvider mRotationChangeProvider;
     private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
+
+    private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
     /**
      * If Launcher restarted while in the middle of an Overview split select, it needs this data to
      * recover. In all other cases this will remain null.
@@ -194,11 +198,13 @@
         super.setupViews();
 
         mActionsView = findViewById(R.id.overview_actions_view);
-        RecentsView overviewPanel = (RecentsView) getOverviewPanel();
+        RecentsView overviewPanel = getOverviewPanel();
         SplitSelectStateController controller =
                 new SplitSelectStateController(this, mHandler, getStateManager(),
                         getDepthController(), getStatsLogManager());
         overviewPanel.init(mActionsView, controller);
+        mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
+                controller);
         mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
         mActionsView.updateVerticalMargin(DisplayController.getNavigationMode(this));
 
@@ -213,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
@@ -321,6 +329,17 @@
         super.showAllAppsFromIntent(alreadyOnHome);
     }
 
+    protected void onItemClicked(View view) {
+        if (!mSplitWithKeyboardShortcutController.handleSecondAppSelectionForSplit(view)) {
+            QuickstepLauncher.super.getItemOnClickListener().onClick(view);
+        }
+    }
+
+    @Override
+    public View.OnClickListener getItemOnClickListener() {
+        return this::onItemClicked;
+    }
+
     @Override
     public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
         Stream<SystemShortcut.Factory> base = Stream.of(WellbeingModel.SHORTCUT_FACTORY);
@@ -402,6 +421,7 @@
 
         super.onDestroy();
         mHotseatPredictionController.destroy();
+        mSplitWithKeyboardShortcutController.onDestroy();
         if (mViewCapture != null) mViewCapture.close();
     }
 
@@ -576,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);
@@ -832,6 +841,12 @@
         return activityOptions;
     }
 
+    @Override
+    @BinderThread
+    public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+        mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop);
+    }
+
     /**
      * Adds a new launch cookie for the activity launch if supported.
      *
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index bd270f9..b09e531 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -48,7 +48,7 @@
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -64,6 +64,7 @@
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -98,9 +99,11 @@
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.launcher3.tracing.SwipeHandlerProto;
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface.AnimationFactory;
@@ -311,6 +314,10 @@
     // Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold
     private final float mQuickSwitchScaleScrollThreshold;
 
+    private final int mTaskbarAppWindowThreshold;
+    private final int mTaskbarCatchUpThreshold;
+    private boolean mTaskbarAlreadyOpen;
+
     public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
@@ -331,11 +338,17 @@
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
-        mQuickSwitchScaleScrollThreshold = context.getResources().getDimension(
-                R.dimen.quick_switch_scaling_scroll_threshold);
 
-        mSplashMainWindowShiftLength = -context.getResources().getDimensionPixelSize(
-                R.dimen.starting_surface_exit_animation_window_shift_length);
+        Resources res = context.getResources();
+        mTaskbarAppWindowThreshold = res
+                .getDimensionPixelSize(R.dimen.taskbar_app_window_threshold);
+        mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
+
+        mQuickSwitchScaleScrollThreshold = res
+                .getDimension(R.dimen.quick_switch_scaling_scroll_threshold);
+
+        mSplashMainWindowShiftLength = -res
+                .getDimensionPixelSize(R.dimen.starting_surface_exit_animation_window_shift_length);
 
         initAfterSubclassConstructor();
         initStateCallbacks();
@@ -650,6 +663,8 @@
                         true/* moveFocusedTask */,
                         new ActiveGestureLog.CompoundString(
                                 "motion pause detected (animate=true)"));
+                Optional.ofNullable(mActivityInterface.getTaskbarController())
+                        .ifPresent(TaskbarUIController::startTranslationSpring);
                 performHapticFeedback();
             }
 
@@ -824,7 +839,7 @@
             return;
         }
         mLauncherTransitionController.setProgress(
-                Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
+                Math.max(getTaskbarProgress(), getScaleProgressDueToScroll()), mDragLengthFactor);
     }
 
     /**
@@ -953,7 +968,6 @@
                 new ActiveGestureLog.CompoundString("on gesture started (animate=false)"));
         mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
-        SystemUiProxy.INSTANCE.get(mContext).notifySwipeUpGestureStarted();
     }
 
     /**
@@ -1176,7 +1190,7 @@
     }
 
     private boolean hasReachedOverviewThreshold() {
-        return mCurrentShift.value > MIN_PROGRESS_FOR_OVERVIEW;
+        return getTaskbarProgress() > MIN_PROGRESS_FOR_OVERVIEW;
     }
 
     @UiThread
@@ -1699,10 +1713,6 @@
     private void resumeLastTask() {
         if (mRecentsAnimationController != null) {
             mRecentsAnimationController.finish(false /* toRecents */, null);
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "finishRecentsAnimation",
-                    /* extras= */ false,
-                    /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
         }
         doLogGesture(LAST_TASK, null);
         reset();
@@ -1911,10 +1921,6 @@
                 mRecentsAnimationController.detachNavigationBarFromApp(true);
             }
         }
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "finishRecentsAnimation",
-                /* extras= */ true,
-                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
     }
 
     private void finishCurrentTransitionToHome() {
@@ -1926,10 +1932,6 @@
             finishRecentsControllerToHome(
                     () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
         }
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "finishRecentsAnimation",
-                /* extras= */ true,
-                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
         doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView());
     }
 
@@ -2040,6 +2042,9 @@
                 mGestureState.updateLastStartedTaskId(taskId);
                 boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
                         .contains(taskId);
+                if (!hasTaskPreviouslyAppeared) {
+                    ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
+                }
                 nextTask.launchTask(success -> {
                     resultCallback.accept(success);
                     if (success) {
@@ -2204,7 +2209,7 @@
             AnimatorControllerWithResistance playbackController =
                     remoteHandle.getPlaybackController();
             if (playbackController != null) {
-                playbackController.setProgress(Math.max(mCurrentShift.value,
+                playbackController.setProgress(Math.max(getTaskbarProgress(),
                         getScaleProgressDueToScroll()), mDragLengthFactor);
             }
 
@@ -2248,6 +2253,41 @@
         return scaleProgress;
     }
 
+    /**
+     * Updates the current status of taskbar during this swipe.
+     */
+    public void setTaskbarAlreadyOpen(boolean taskbarAlreadyOpen) {
+        mTaskbarAlreadyOpen = taskbarAlreadyOpen;
+    }
+
+    /**
+     * Overrides the current shift progress to keep the app window at the bottom of the screen
+     * while the transient taskbar is being swiped in.
+     *
+     * There is also a catch up period so that the window can start moving 1:1 with the swipe.
+     */
+    private float getTaskbarProgress() {
+        if (!DisplayController.isTransientTaskbar(mContext)) {
+            return mCurrentShift.value;
+        }
+
+        if (mTaskbarAlreadyOpen) {
+            return mCurrentShift.value;
+        }
+
+        if (mCurrentDisplacement < mTaskbarAppWindowThreshold) {
+            return 0;
+        }
+
+        // "Catch up" with `mCurrentShift.value`.
+        if (mCurrentDisplacement < mTaskbarCatchUpThreshold) {
+            return Utilities.mapToRange(mCurrentDisplacement, mTaskbarAppWindowThreshold,
+                    mTaskbarCatchUpThreshold, 0, mCurrentShift.value, ACCEL_DEACCEL);
+        }
+
+        return mCurrentShift.value;
+    }
+
     private void setDividerShown(boolean shown, boolean immediate) {
         if (mDividerAnimator != null) {
             mDividerAnimator.cancel();
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/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 3c4ee75..31b78b3 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -21,7 +21,6 @@
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_HOME;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_LAST_TASK;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_NEW_TASK;
 
 import android.annotation.Nullable;
@@ -341,8 +340,6 @@
                 ActiveGestureLog.INSTANCE.trackEvent(SET_END_TARGET_NEW_TASK);
                 break;
             case LAST_TASK:
-                ActiveGestureLog.INSTANCE.trackEvent(SET_END_TARGET_LAST_TASK);
-                break;
             case RECENTS:
             default:
                 // No-Op
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index b7cdecd..9621ce6 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -20,6 +20,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 public class QuickstepTestInformationHandler extends TestInformationHandler {
 
@@ -112,6 +113,13 @@
                         resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size));
                 return response;
             }
+
+            case TestProtocol.REQUEST_TASKBAR_ALL_APPS_TOP_PADDING: {
+                return getTISBinderUIProperty(Bundle::putInt, tisBinder ->
+                        tisBinder.getTaskbarManager()
+                                .getCurrentActivityContext()
+                                .getTaskbarAllAppsTopPadding());
+            }
         }
 
         return super.call(method, arg, extras);
@@ -159,4 +167,16 @@
             throw new RuntimeException(e);
         }
     }
+
+    private <T> Bundle getTISBinderUIProperty(
+            BundleSetter<T> bundleSetter, Function<TouchInteractionService.TISBinder, T> provider) {
+        Bundle response = new Bundle();
+
+        runOnTISBinder(tisBinder -> bundleSetter.set(
+                response,
+                TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                provider.apply(tisBinder)));
+
+        return response;
+    }
 }
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/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 97ce30f..dc405ff 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -37,9 +37,11 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.view.Display;
+import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl.Transaction;
 import android.view.View;
+import android.window.RemoteTransition;
 import android.window.SplashScreen;
 
 import androidx.annotation.Nullable;
@@ -79,7 +81,6 @@
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -259,13 +260,11 @@
 
         final LauncherAnimationRunner wrapper = new LauncherAnimationRunner(
                 mUiHandler, mActivityLaunchAnimationRunner, true /* startAtFrontOfQueue */);
-        RemoteAnimationAdapterCompat adapterCompat = new RemoteAnimationAdapterCompat(
-                wrapper, RECENTS_LAUNCH_DURATION,
-                RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
-                        - STATUS_BAR_TRANSITION_PRE_DELAY, getIApplicationThread());
         final ActivityOptions options = ActivityOptions.makeRemoteAnimation(
-                adapterCompat.getWrapped(),
-                adapterCompat.getRemoteTransition().getTransition());
+                new RemoteAnimationAdapter(wrapper, RECENTS_LAUNCH_DURATION,
+                        RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
+                                - STATUS_BAR_TRANSITION_PRE_DELAY),
+                new RemoteTransition(wrapper.toRemoteTransition(), getIApplicationThread()));
         final ActivityOptionsWrapper activityOptions = new ActivityOptionsWrapper(options,
                 onEndCallback);
         activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
@@ -401,12 +400,9 @@
     private void startHomeInternal() {
         LauncherAnimationRunner runner = new LauncherAnimationRunner(
                 getMainThreadHandler(), mAnimationToHomeFactory, true);
-        RemoteAnimationAdapterCompat adapterCompat =
-                new RemoteAnimationAdapterCompat(runner, HOME_APPEAR_DURATION, 0,
-                        getIApplicationThread());
         ActivityOptions options = ActivityOptions.makeRemoteAnimation(
-                adapterCompat.getWrapped(),
-                adapterCompat.getRemoteTransition().getTransition());
+                new RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+                new RemoteTransition(runner.toRemoteTransition(), getIApplicationThread()));
         startHomeIntentSafely(this, options.toBundle());
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 2451ad8..b82ff03 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -130,7 +130,7 @@
     public final void onAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
             ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "onRecentsAnimationCancelled",
+                    /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
                     /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationCanceled(thumbnailDatas);
@@ -142,7 +142,7 @@
     @Override
     public void onTasksAppeared(RemoteAnimationTarget[] apps) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            ActiveGestureLog.INSTANCE.addLog("onTasksAppeared",
+            ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared",
                     ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED);
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onTasksAppeared(apps);
@@ -164,6 +164,8 @@
 
     private final void onAnimationFinished(RecentsAnimationController controller) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+            ActiveGestureLog.INSTANCE.addLog(
+                    /* event= */ "RecentsAnimationCallbacks.onAnimationFinished");
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationFinished(controller);
             }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 6088a36..4adfae5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
 
 import android.content.Context;
 import android.os.RemoteException;
@@ -155,6 +156,10 @@
             mPendingFinishCallbacks.add(callback);
             return;
         }
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "finishRecentsAnimation",
+                /* extras= */ toRecents,
+                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
 
         // Finish not yet requested
         mFinishRequested = true;
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index f591a1c..ddb06ce 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -65,6 +65,7 @@
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
     // visible.
     protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+    protected float mCurrentDisplacement;
 
     // The distance needed to drag to reach the task size in recents.
     protected int mTransitionDragLength;
@@ -116,6 +117,8 @@
     public void updateDisplacement(float displacement) {
         // We are moving in the negative x/y direction
         displacement = -displacement;
+        mCurrentDisplacement = displacement;
+
         float shift;
         if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
             shift = mDragLengthFactor;
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 7705a25..acf597b 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -44,6 +44,8 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.IOnBackInvokedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
@@ -53,13 +55,11 @@
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.SmartspaceState;
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.desktopmode.IDesktopMode;
-import com.android.wm.shell.floating.IFloatingTasks;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
 import com.android.wm.shell.pip.IPipAnimationListener;
@@ -74,6 +74,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 
 /**
  * Holds the reference to SystemUI.
@@ -90,7 +91,6 @@
     private IPip mPip;
     private ISysuiUnlockAnimationController mSysuiUnlockAnimationController;
     private ISplitScreen mSplitScreen;
-    private IFloatingTasks mFloatingTasks;
     private IOneHanded mOneHanded;
     private IShellTransitions mShellTransitions;
     private IStartingWindow mStartingWindow;
@@ -110,7 +110,8 @@
     private IStartingWindowListener mStartingWindowListener;
     private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
     private IRecentTasksListener mRecentTasksListener;
-    private final ArrayList<RemoteTransitionCompat> mRemoteTransitions = new ArrayList<>();
+    private final LinkedHashMap<RemoteTransition, TransitionFilter> mRemoteTransitions =
+            new LinkedHashMap<>();
     private IOnBackInvokedCallback mBackToLauncherCallback;
 
     // Used to dedupe calls to SystemUI
@@ -168,7 +169,7 @@
     }
 
     public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
-            IFloatingTasks floatingTasks, IOneHanded oneHanded, IShellTransitions shellTransitions,
+            IOneHanded oneHanded, IShellTransitions shellTransitions,
             IStartingWindow startingWindow, IRecentTasks recentTasks,
             ISysuiUnlockAnimationController sysuiUnlockAnimationController,
             IBackAnimation backAnimation, IDesktopMode desktopMode) {
@@ -176,7 +177,6 @@
         mSystemUiProxy = proxy;
         mPip = pip;
         mSplitScreen = splitScreen;
-        mFloatingTasks = floatingTasks;
         mOneHanded = oneHanded;
         mShellTransitions = shellTransitions;
         mStartingWindow = startingWindow;
@@ -198,9 +198,7 @@
         if (mSysuiUnlockAnimationController != null && mLauncherUnlockAnimationController != null) {
             setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
         }
-        for (int i = mRemoteTransitions.size() - 1; i >= 0; --i) {
-            registerRemoteTransition(mRemoteTransitions.get(i));
-        }
+        new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition);
         if (mRecentTasksListener != null && mRecentTasks != null) {
             registerRecentTasksListener(mRecentTasksListener);
         }
@@ -210,7 +208,7 @@
     }
 
     public void clearProxy() {
-        setProxy(null, null, null, null, null, null, null, null, null, null, null);
+        setProxy(null, null, null, null, null, null, null, null, null, null);
     }
 
     // TODO(141886704): Find a way to remove this
@@ -347,17 +345,6 @@
     }
 
     @Override
-    public void notifySwipeUpGestureStarted() {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.notifySwipeUpGestureStarted();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call notifySwipeUpGestureStarted", e);
-            }
-        }
-    }
-
-    @Override
     public void notifyPrioritizedRotation(int rotation) {
         if (mSystemUiProxy != null) {
             try {
@@ -550,11 +537,11 @@
     /** Start multiple tasks in split-screen simultaneously. */
     public void startTasks(int taskId1, Bundle options1, int taskId2, Bundle options2,
             @SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio,
-            RemoteTransitionCompat remoteTransition, InstanceId instanceId) {
+            RemoteTransition remoteTransition, InstanceId instanceId) {
         if (mSystemUiProxy != null) {
             try {
                 mSplitScreen.startTasks(taskId1, options1, taskId2, options2, splitPosition,
-                        splitRatio, remoteTransition.getTransition(), instanceId);
+                        splitRatio, remoteTransition, instanceId);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startTasks");
             }
@@ -564,12 +551,12 @@
     public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
             Bundle options1, int taskId, Bundle options2,
             @SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio,
-            RemoteTransitionCompat remoteTransition, InstanceId instanceId) {
+            RemoteTransition remoteTransition, InstanceId instanceId) {
         if (mSystemUiProxy != null) {
             try {
                 mSplitScreen.startIntentAndTask(pendingIntent, fillInIntent, options1,
                         taskId, options2, splitPosition, splitRatio,
-                        remoteTransition.getTransition(), instanceId);
+                        remoteTransition, instanceId);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startIntentAndTask");
             }
@@ -578,11 +565,11 @@
 
     public void startShortcutAndTask(ShortcutInfo shortcutInfo, Bundle options1, int taskId,
             Bundle options2, @SplitConfigurationOptions.StagePosition int splitPosition,
-            float splitRatio, RemoteTransitionCompat remoteTransition, InstanceId instanceId) {
+            float splitRatio, RemoteTransition remoteTransition, InstanceId instanceId) {
         if (mSystemUiProxy != null) {
             try {
                 mSplitScreen.startShortcutAndTask(shortcutInfo, options1, taskId, options2,
-                        splitPosition, splitRatio, remoteTransition.getTransition(), instanceId);
+                        splitPosition, splitRatio, remoteTransition, instanceId);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startShortcutAndTask");
             }
@@ -696,20 +683,6 @@
     }
 
     //
-    // Floating tasks
-    //
-
-    public void showFloatingTask(Intent intent) {
-        if (mFloatingTasks != null) {
-            try {
-                mFloatingTasks.showTask(intent);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Launcher: Failed call showFloatingTask", e);
-            }
-        }
-    }
-
-    //
     // One handed
     //
 
@@ -737,24 +710,24 @@
     // Remote transitions
     //
 
-    public void registerRemoteTransition(RemoteTransitionCompat remoteTransition) {
+    public void registerRemoteTransition(
+            RemoteTransition remoteTransition, TransitionFilter filter) {
         if (mShellTransitions != null) {
             try {
-                mShellTransitions.registerRemote(remoteTransition.getFilter(),
-                        remoteTransition.getTransition());
+                mShellTransitions.registerRemote(filter, remoteTransition);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
         }
-        if (!mRemoteTransitions.contains(remoteTransition)) {
-            mRemoteTransitions.add(remoteTransition);
+        if (!mRemoteTransitions.containsKey(remoteTransition)) {
+            mRemoteTransitions.put(remoteTransition, filter);
         }
     }
 
-    public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
+    public void unregisterRemoteTransition(RemoteTransition remoteTransition) {
         if (mShellTransitions != null) {
             try {
-                mShellTransitions.unregisterRemote(remoteTransition.getTransition());
+                mShellTransitions.unregisterRemote(remoteTransition);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 30d445f..c45b2f0 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -21,6 +21,8 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
+import static com.android.systemui.shared.system.RemoteTransitionCompat.newRemoteTransition;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -29,6 +31,7 @@
 import android.os.SystemProperties;
 import android.util.Log;
 import android.view.RemoteAnimationTarget;
+import android.window.RemoteTransition;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -36,10 +39,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
@@ -100,6 +103,9 @@
     @UiThread
     public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState,
             Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "startRecentsAnimation",
+                /* gestureEvent= */ START_RECENTS_ANIMATION);
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
@@ -223,11 +229,10 @@
         mCallbacks.addListener(listener);
 
         if (ENABLE_SHELL_TRANSITIONS) {
-            RemoteTransitionCompat transition = new RemoteTransitionCompat(mCallbacks,
+            RemoteTransition transition = newRemoteTransition(mCallbacks,
                     mController != null ? mController.getController() : null,
                     mCtx.getIApplicationThread());
-            final ActivityOptions options = ActivityOptions.makeRemoteTransition(
-                    transition.getTransition());
+            final ActivityOptions options = ActivityOptions.makeRemoteTransition(transition);
             // Allowing to pause Home if Home is top activity and Recents is not Home. So when user
             // start home when recents animation is playing, the home activity can be resumed again
             // to let the transition controller collect Home activity.
@@ -250,6 +255,7 @@
      * Continues the existing running recents animation for a new gesture.
      */
     public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation");
         mCallbacks.removeListener(mLastGestureState);
         mLastGestureState = gestureState;
         mCallbacks.addListener(gestureState);
@@ -288,6 +294,8 @@
      */
     public void finishRunningRecentsAnimation(boolean toHome) {
         if (mController != null) {
+            ActiveGestureLog.INSTANCE.addLog(
+                    /* event= */ "finishRunningRecentsAnimation", toHome);
             mCallbacks.notifyAnimationCanceled();
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
                     ? mController::finishAnimationToHome
@@ -320,6 +328,7 @@
      * Cleans up the recents animation entirely.
      */
     private void cleanUpRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
         if (mLiveTileCleanUpHandler != null) {
             mLiveTileCleanUpHandler.run();
             mLiveTileCleanUpHandler = null;
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 9d5e7c3..e8722e2 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -43,7 +43,6 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
-import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Matrix;
@@ -399,9 +398,8 @@
      */
     public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
             @NonNull StateManager stateManager, @Nullable DepthController depthController,
-            int initialTaskId, @Nullable PendingIntent initialTaskPendingIntent, int secondTaskId,
-            @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t,
-            @NonNull Runnable finishCallback) {
+            int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
+            SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
         if (launchingTaskView != null) {
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.addListener(new AnimatorListenerAdapter() {
@@ -491,8 +489,7 @@
      * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
      * case where launcher handles animating starting split tasks from app icon) */
     public static void composeRecentsSplitLaunchAnimatorLegacy(
-            @Nullable GroupedTaskView launchingTaskView, int initialTaskId,
-            @Nullable PendingIntent initialTaskPendingIntent, int secondTaskId,
+            @Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId,
             @NonNull RemoteAnimationTarget[] appTargets,
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
             @NonNull RemoteAnimationTarget[] nonAppTargets,
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index e9f9d80..450774b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep;
 
+import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
@@ -22,6 +23,7 @@
 import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -32,7 +34,6 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
@@ -47,7 +48,6 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
-import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Icon;
 import android.os.Build;
@@ -55,11 +55,11 @@
 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;
 import android.view.MotionEvent;
+import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.BinderThread;
@@ -86,7 +86,6 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
@@ -103,7 +102,6 @@
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
 import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.ProxyScreenStatusProvider;
-import com.android.quickstep.util.SplitScreenBounds;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -114,7 +112,6 @@
 import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.desktopmode.IDesktopMode;
-import com.android.wm.shell.floating.IFloatingTasks;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
 import com.android.wm.shell.recents.IRecentTasks;
@@ -140,21 +137,7 @@
 
     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 KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount";
-    private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE";
     private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
-    private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
-
-    /**
-     * System Action ID to show all apps.
-     * TODO: Use AccessibilityService's corresponding global action constant in S
-     */
-    private static final int SYSTEM_ACTION_ID_ALL_APPS = 14;
-
-    private int mBackGestureNotificationCounter = -1;
 
     private final TISBinder mTISBinder = new TISBinder();
 
@@ -172,8 +155,6 @@
             IPip pip = IPip.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_PIP));
             ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
                     KEY_EXTRA_SHELL_SPLIT_SCREEN));
-            IFloatingTasks floatingTasks = IFloatingTasks.Stub.asInterface(bundle.getBinder(
-                    KEY_EXTRA_SHELL_FLOATING_TASKS));
             IOneHanded onehanded = IOneHanded.Stub.asInterface(
                     bundle.getBinder(KEY_EXTRA_SHELL_ONE_HANDED));
             IShellTransitions shellTransitions = IShellTransitions.Stub.asInterface(
@@ -191,7 +172,7 @@
                     bundle.getBinder(KEY_EXTRA_SHELL_DESKTOP_MODE));
             MAIN_EXECUTOR.execute(() -> {
                 SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
-                        splitscreen, floatingTasks, onehanded, shellTransitions, startingWindow,
+                        splitscreen, onehanded, shellTransitions, startingWindow,
                         recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode);
                 TouchInteractionService.this.initInputMonitor("TISBinder#onInitialize()");
                 preloadOverview(true /* fromInit */);
@@ -232,12 +213,6 @@
 
         @BinderThread
         @Override
-        public void onTip(int actionType, int viewType) {
-            // Please delete this method from the interface
-        }
-
-        @BinderThread
-        @Override
         public void onAssistantAvailable(boolean available) {
             MAIN_EXECUTOR.execute(() -> {
                 mDeviceState.setAssistantAvailable(available);
@@ -254,10 +229,9 @@
             });
         }
 
-        @BinderThread
-        public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
-                boolean gestureSwipeLeft) {
-            // Remove this method from the interface
+        @Override
+        public void onNavigationBarSurface(SurfaceControl surface) {
+            // TODO: implement
         }
 
         @BinderThread
@@ -274,12 +248,6 @@
             MAIN_EXECUTOR.execute(() -> mDeviceState.setDeferredGestureRegion(region));
         }
 
-        @Override
-        public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
-            WindowBounds wb = new WindowBounds(bounds, insets);
-            MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
-        }
-
         @BinderThread
         @Override
         public void onScreenTurnedOn() {
@@ -298,6 +266,16 @@
             MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOff);
         }
 
+        @BinderThread
+        @Override
+        public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+            StatefulActivity activity =
+                    mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
+            if (activity != null) {
+                activity.enterStageSplitFromRunningApp(leftOrTop);
+            }
+        }
+
         /**
          * Preloads the Overview activity.
          *
@@ -483,8 +461,6 @@
 
         // Temporarily disable model preload
         // new ModelPreload().start(this);
-        mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
-                .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
         resetHomeBounceSeenOnQuickstepEnabledFirstTime();
 
         mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
@@ -522,11 +498,11 @@
                     Icon.createWithResource(this, R.drawable.ic_apps),
                     getString(R.string.all_apps_label),
                     getString(R.string.all_apps_label),
-                    PendingIntent.getActivity(this, SYSTEM_ACTION_ID_ALL_APPS, intent,
+                    PendingIntent.getActivity(this, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, intent,
                             PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
-            am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
+            am.registerSystemAction(allAppsAction, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
         } else {
-            am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+            am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
         }
 
         StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface()
@@ -604,7 +580,7 @@
         ProtoTracer.INSTANCE.get(this).remove(this);
 
         getSystemService(AccessibilityManager.class)
-                .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+                .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
 
         mTaskbarManager.destroy();
         sConnected = false;
@@ -829,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()) {
@@ -927,6 +885,9 @@
                 .append(consumer.getName())
                 .append(". reason(s):")
                 .append(reasonString));
+        if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
+            ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
+        }
     }
 
     private void handleOrientationSetup(InputConsumer baseInputConsumer) {
@@ -1251,12 +1212,22 @@
     }
 
     private void onCommand(PrintWriter pw, LinkedList<String> args) {
-        switch (args.pollFirst()) {
+        String cmd = args.pollFirst();
+        if (cmd == null) {
+            pw.println("Command missing");
+            printAvailableCommands(pw);
+            return;
+        }
+        switch (cmd) {
             case "clear-touch-log":
                 ActiveGestureLog.INSTANCE.clear();
                 break;
             case "print-gesture-log":
                 ActiveGestureLog.INSTANCE.dump("", pw);
+                break;
+            default:
+                pw.println("Command does not exist: " + cmd);
+                printAvailableCommands(pw);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 60d5ba4..b3d3c3d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -30,13 +30,13 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
 import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.os.Build;
 import android.util.Log;
@@ -48,13 +48,16 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.tracing.InputConsumerProto;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.AbsSwipeUpHandler;
 import com.android.quickstep.AbsSwipeUpHandler.Factory;
+import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationCallbacks;
@@ -63,7 +66,6 @@
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
@@ -97,6 +99,7 @@
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
     private final InputMonitorCompat mInputMonitorCompat;
     private final InputEventReceiver mInputEventReceiver;
+    private final BaseActivityInterface mActivityInterface;
 
     private final AbsSwipeUpHandler.Factory mHandlerFactory;
 
@@ -131,6 +134,10 @@
     // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
     private float mStartDisplacement;
 
+    private final boolean mIsTransientTaskbar;
+    private final boolean mTaskbarAlreadyOpen;
+    private final int mTaskbarHomeOverviewThreshold;
+
     public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
@@ -142,6 +149,11 @@
         mTaskAnimationManager = taskAnimationManager;
         mGestureState = gestureState;
         mHandlerFactory = handlerFactory;
+        mActivityInterface = mGestureState.getActivityInterface();
+
+        Resources res = base.getResources();
+        mTaskbarHomeOverviewThreshold = res
+                .getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold);
 
         mMotionPauseDetector = new MotionPauseDetector(base, false,
                 mNavBarPosition.isLeftEdge() || mNavBarPosition.isRightEdge()
@@ -153,6 +165,10 @@
         mInputMonitorCompat = inputMonitorCompat;
         mInputEventReceiver = inputEventReceiver;
 
+        TaskbarUIController controller = mActivityInterface.getTaskbarController();
+        mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
+        mIsTransientTaskbar = DisplayController.isTransientTaskbar(base);
+
         boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
 
@@ -279,6 +295,7 @@
                 float upDist = -displacement;
                 boolean passedSlop = squaredHypot(displacementX, displacementY)
                         >= mSquaredTouchSlop;
+
                 if (!mPassedSlopOnThisGesture && passedSlop) {
                     mPassedSlopOnThisGesture = true;
                 }
@@ -323,7 +340,13 @@
                     }
 
                     if (mDeviceState.isFullyGesturalNavMode()) {
-                        mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
+                        float minDisplacement = mMotionPauseMinDisplacement;
+
+                        if (mIsTransientTaskbar && !mTaskbarAlreadyOpen) {
+                            minDisplacement += mTaskbarHomeOverviewThreshold;
+                        }
+
+                        mMotionPauseDetector.setDisallowPause(upDist < minDisplacement
                                 || isLikelyToStartNewTask);
                         mMotionPauseDetector.addPosition(ev);
                         mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
@@ -357,13 +380,11 @@
 
         // Notify the handler that the gesture has actually started
         mInteractionHandler.onGestureStarted(isLikelyToStartNewTask);
+
+        mInteractionHandler.setTaskbarAlreadyOpen(mTaskbarAlreadyOpen);
     }
 
     private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "startRecentsAnimation",
-                /* gestureEvent= */ START_RECENTS_ANIMATION);
-
         mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
index b9b5e7c..2462394 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
@@ -28,8 +28,6 @@
 import android.graphics.Point;
 import android.view.MotionEvent;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -43,7 +41,6 @@
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
@@ -102,8 +99,7 @@
         mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize;
 
         // Init states
-        mStateCallback = new MultiStateCallback(
-                STATE_NAMES, ProgressDelegateInputConsumer::getTrackedEventForState);
+        mStateCallback = new MultiStateCallback(STATE_NAMES);
         mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
                 this::endRemoteAnimation);
         mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_FLING_FINISHED,
@@ -113,14 +109,6 @@
         mSwipeDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
     }
 
-    @Nullable
-    private static ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateFlag) {
-        if (stateFlag == STATE_HANDLER_INVALIDATED) {
-            return ActiveGestureErrorDetector.GestureEvent.STATE_HANDLER_INVALIDATED;
-        }
-        return null;
-    }
-
     @Override
     public int getType() {
         return TYPE_PROGRESS_DELEGATE;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
index 3785de4..b7f2022 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -15,16 +15,26 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
+import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
@@ -37,20 +47,36 @@
     private final GestureDetector mLongPressDetector;
     private final float mSquaredTouchSlop;
 
-
-    private float mDownX, mDownY;
+    private float mLongPressDownX, mLongPressDownY;
     private boolean mCanceledUnstashHint;
     private final float mUnstashArea;
     private final float mScreenWidth;
 
+    private final int mTaskbarThresholdY;
+    private boolean mHasPassedTaskbarThreshold;
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private int mActivePointerId = INVALID_POINTER_ID;
+
+    private final boolean mIsTransientTaskbar;
+
+    private final @Nullable TransitionCallback mTransitionCallback;
+
     public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
         mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
-        mUnstashArea = context.getResources()
-                .getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
+
+        Resources res = context.getResources();
+        mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
+        int taskbarThreshold = res.getDimensionPixelSize(R.dimen.taskbar_nav_threshold);
+        int screenHeight = taskbarActivityContext.getDeviceProfile().heightPx;
+        mTaskbarThresholdY = screenHeight - taskbarThreshold;
+
+        mIsTransientTaskbar = DisplayController.isTransientTaskbar(context);
 
         mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
             @Override
@@ -58,6 +84,10 @@
                 onLongPressDetected(motionEvent);
             }
         });
+
+        mTransitionCallback = mIsTransientTaskbar
+                ? taskbarActivityContext.getTranslationCallbacks()
+                : null;
     }
 
     @Override
@@ -76,28 +106,83 @@
                 final float y = ev.getRawY();
                 switch (ev.getAction()) {
                     case MotionEvent.ACTION_DOWN:
+                        mActivePointerId = ev.getPointerId(0);
+                        mDownPos.set(ev.getX(), ev.getY());
+                        mLastPos.set(mDownPos);
+
+                        mHasPassedTaskbarThreshold = false;
+                        mTaskbarActivityContext.setAutohideSuspendFlag(
+                                FLAG_AUTOHIDE_SUSPEND_TOUCHING, true);
                         if (isInArea(x)) {
-                            mDownX = x;
-                            mDownY = y;
-                            mTaskbarActivityContext.startTaskbarUnstashHint(
-                                    /* animateForward = */ true);
-                            mCanceledUnstashHint = false;
+                            if (!mIsTransientTaskbar) {
+                                mLongPressDownX = x;
+                                mLongPressDownY = y;
+                                mTaskbarActivityContext.startTaskbarUnstashHint(
+                                        /* animateForward = */ true);
+                                mCanceledUnstashHint = false;
+                            }
+                        }
+                        break;
+                    case MotionEvent.ACTION_POINTER_UP:
+                        int ptrIdx = ev.getActionIndex();
+                        int ptrId = ev.getPointerId(ptrIdx);
+                        if (ptrId == mActivePointerId) {
+                            final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                            mDownPos.set(
+                                    ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                                    ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                            mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                            mActivePointerId = ev.getPointerId(newPointerIdx);
                         }
                         break;
                     case MotionEvent.ACTION_MOVE:
-                        if (!mCanceledUnstashHint
-                                && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
+                        if (!mIsTransientTaskbar
+                                && !mCanceledUnstashHint
+                                && squaredHypot(mLongPressDownX - x, mLongPressDownY - y)
+                                > mSquaredTouchSlop) {
                             mTaskbarActivityContext.startTaskbarUnstashHint(
                                     /* animateForward = */ false);
                             mCanceledUnstashHint = true;
                         }
+
+                        int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                        if (pointerIndex == INVALID_POINTER_ID) {
+                            break;
+                        }
+                        mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                        if (mIsTransientTaskbar) {
+                            float dY = mLastPos.y - mDownPos.y;
+                            boolean passedTaskbarThreshold = dY < 0
+                                    && mLastPos.y < mTaskbarThresholdY;
+
+                            if (!mHasPassedTaskbarThreshold
+                                    && passedTaskbarThreshold) {
+                                mHasPassedTaskbarThreshold = true;
+
+                                mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                            }
+
+                            if (dY < 0) {
+                                dY = -OverScroll.dampedScroll(-dY, mTaskbarThresholdY);
+                                if (mTransitionCallback != null) {
+                                    mTransitionCallback.onActionMove(dY);
+                                }
+                            }
+                        }
                         break;
                     case MotionEvent.ACTION_UP:
                     case MotionEvent.ACTION_CANCEL:
-                        if (!mCanceledUnstashHint) {
+                        if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
                             mTaskbarActivityContext.startTaskbarUnstashHint(
                                     /* animateForward = */ false);
                         }
+                        mTaskbarActivityContext.setAutohideSuspendFlag(
+                                FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
+                        if (mTransitionCallback != null) {
+                            mTransitionCallback.onActionEnd();
+                        }
+                        mHasPassedTaskbarThreshold = false;
                         break;
                 }
             }
@@ -111,7 +196,9 @@
     }
 
     private void onLongPressDetected(MotionEvent motionEvent) {
-        if (mTaskbarActivityContext != null && isInArea(motionEvent.getRawX())) {
+        if (mTaskbarActivityContext != null
+                && isInArea(motionEvent.getRawX())
+                && !mIsTransientTaskbar) {
             boolean taskBarPressed = mTaskbarActivityContext.onLongPressToUnstashTaskbar();
             if (taskBarPressed) {
                 setActive(motionEvent);
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/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 791f93b..dac5a31 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -473,6 +473,7 @@
         private int mPackageId = 0;
         private long mLatencyInMillis;
         private int mQueryLength = -1;
+        private int mSubEventType = 0;
 
         StatsCompatLatencyLogger(Context context, ActivityContext activityContext) {
             mContext = context;
@@ -510,6 +511,12 @@
         }
 
         @Override
+        public StatsLatencyLogger withSubEventType(int type) {
+            this.mSubEventType = type;
+            return this;
+        }
+
+        @Override
         public void log(EventEnum event) {
             if (IS_VERBOSE) {
                 String name = (event instanceof Enum) ? ((Enum) event).name() :
@@ -526,7 +533,8 @@
                     mPackageId, // package_id
                     mLatencyInMillis, // latency_in_millis
                     mType.getId(), //type
-                    mQueryLength // query_length
+                    mQueryLength, // query_length
+                    mSubEventType // sub_event_type
             );
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 53e0c2b..60065fb 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -33,10 +33,11 @@
      * Enums associated to gesture navigation events.
      */
     public enum GestureEvent {
-        MOTION_DOWN, MOTION_UP, SET_END_TARGET, SET_END_TARGET_HOME, SET_END_TARGET_LAST_TASK,
-        SET_END_TARGET_NEW_TASK, ON_SETTLED_ON_END_TARGET, START_RECENTS_ANIMATION,
-        FINISH_RECENTS_ANIMATION, CANCEL_RECENTS_ANIMATION, SET_ON_PAGE_TRANSITION_END_CALLBACK,
-        CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT, SCROLLER_ANIMATION_ABORTED, TASK_APPEARED,
+        MOTION_DOWN, MOTION_UP, SET_END_TARGET, SET_END_TARGET_HOME, SET_END_TARGET_NEW_TASK,
+        ON_SETTLED_ON_END_TARGET, START_RECENTS_ANIMATION, FINISH_RECENTS_ANIMATION,
+        CANCEL_RECENTS_ANIMATION, SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION,
+        CLEANUP_SCREENSHOT, SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
+        FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER,
 
         /**
          * These GestureEvents are specifically associated to state flags that get set in
@@ -91,35 +92,40 @@
                     case MOTION_UP:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.MOTION_DOWN),
-                                /* errorMessage= */ prefix + "\t\tMotion up detected before/without"
+                                prefix,
+                                /* errorMessage= */ "Motion up detected before/without"
                                         + " motion down.",
                                 writer);
                         break;
                     case ON_SETTLED_ON_END_TARGET:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.SET_END_TARGET),
-                                /* errorMessage= */ prefix + "\t\tonSettledOnEndTarget called "
+                                prefix,
+                                /* errorMessage= */ "onSettledOnEndTarget called "
                                         + "before/without setEndTarget.",
                                 writer);
                         break;
                     case FINISH_RECENTS_ANIMATION:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
-                                /* errorMessage= */ prefix + "\t\tfinishRecentsAnimation called "
+                                prefix,
+                                /* errorMessage= */ "finishRecentsAnimation called "
                                         + "before/without startRecentsAnimation.",
                                 writer);
                         break;
                     case CANCEL_RECENTS_ANIMATION:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
-                                /* errorMessage= */ prefix + "\t\tcancelRecentsAnimation called "
+                                prefix,
+                                /* errorMessage= */ "cancelRecentsAnimation called "
                                         + "before/without startRecentsAnimation.",
                                 writer);
                         break;
                     case CLEANUP_SCREENSHOT:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED),
-                                /* errorMessage= */ prefix + "\t\trecents activity screenshot was "
+                                prefix,
+                                /* errorMessage= */ "recents activity screenshot was "
                                         + "cleaned up before/without STATE_SCREENSHOT_CAPTURED "
                                         + "being set.",
                                 writer);
@@ -129,48 +135,66 @@
                                 encounteredEvents.contains(GestureEvent.SET_END_TARGET_HOME)
                                         && !encounteredEvents.contains(
                                                 GestureEvent.ON_SETTLED_ON_END_TARGET),
-                                /* errorMessage= */ prefix + "\t\trecents view scroller animation "
+                                prefix,
+                                /* errorMessage= */ "recents view scroller animation "
                                         + "aborted after setting end target HOME, but before"
                                         + " settling on end target.",
                                 writer);
                         break;
                     case TASK_APPEARED:
                         errorDetected |= printErrorIfTrue(
-                                !encounteredEvents.contains(GestureEvent.SET_END_TARGET_LAST_TASK)
-                                        && !encounteredEvents.contains(
-                                        GestureEvent.SET_END_TARGET_NEW_TASK),
-                                /* errorMessage= */ prefix + "\t\tonTasksAppeared called "
-                                        + "before/without setting end target to last or new task",
+                                !encounteredEvents.contains(GestureEvent.SET_END_TARGET_NEW_TASK),
+                                prefix,
+                                /* errorMessage= */ "onTasksAppeared called "
+                                        + "before/without setting end target to new task",
+                                writer);
+                        errorDetected |= printErrorIfTrue(
+                                !encounteredEvents.contains(GestureEvent.EXPECTING_TASK_APPEARED),
+                                prefix,
+                                /* errorMessage= */ "onTasksAppeared was not expected to be called",
+                                writer);
+                        break;
+                    case EXPECTING_TASK_APPEARED:
+                        errorDetected |= printErrorIfTrue(
+                                !encounteredEvents.contains(GestureEvent.SET_END_TARGET_NEW_TASK),
+                                prefix,
+                                /* errorMessage= */ "expecting onTasksAppeared to be called "
+                                        + "before/without setting end target to new task",
                                 writer);
                         break;
                     case STATE_GESTURE_COMPLETED:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.MOTION_UP),
-                                /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_COMPLETED set "
+                                prefix,
+                                /* errorMessage= */ "STATE_GESTURE_COMPLETED set "
                                         + "before/without motion up.",
                                 writer);
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED),
-                                /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_COMPLETED set "
+                                prefix,
+                                /* errorMessage= */ "STATE_GESTURE_COMPLETED set "
                                         + "before/without STATE_GESTURE_STARTED.",
                                 writer);
                         break;
                     case STATE_GESTURE_CANCELLED:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.MOTION_UP),
-                                /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_CANCELLED set "
+                                prefix,
+                                /* errorMessage= */ "STATE_GESTURE_CANCELLED set "
                                         + "before/without motion up.",
                                 writer);
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED),
-                                /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_CANCELLED set "
+                                prefix,
+                                /* errorMessage= */ "STATE_GESTURE_CANCELLED set "
                                         + "before/without STATE_GESTURE_STARTED.",
                                 writer);
                         break;
                     case STATE_SCREENSHOT_CAPTURED:
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(GestureEvent.STATE_CAPTURE_SCREENSHOT),
-                                /* errorMessage= */ prefix + "\t\tSTATE_SCREENSHOT_CAPTURED set "
+                                prefix,
+                                /* errorMessage= */ "STATE_SCREENSHOT_CAPTURED set "
                                         + "before/without STATE_CAPTURE_SCREENSHOT.",
                                 writer);
                         break;
@@ -178,7 +202,8 @@
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(
                                         GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK),
-                                /* errorMessage= */ prefix + "\t\tSTATE_RECENTS_SCROLLING_FINISHED "
+                                prefix,
+                                /* errorMessage= */ "STATE_RECENTS_SCROLLING_FINISHED "
                                         + "set before/without calling "
                                         + "setOnPageTransitionEndCallback.",
                                 writer);
@@ -187,16 +212,19 @@
                         errorDetected |= printErrorIfTrue(
                                 !encounteredEvents.contains(
                                         GestureEvent.START_RECENTS_ANIMATION),
-                                /* errorMessage= */ prefix + "\t\tSTATE_RECENTS_ANIMATION_CANCELED "
+                                prefix,
+                                /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED "
                                         + "set before/without startRecentsAnimation.",
                                 writer);
                         break;
                     case MOTION_DOWN:
                     case SET_END_TARGET:
                     case SET_END_TARGET_HOME:
+                    case SET_END_TARGET_NEW_TASK:
                     case START_RECENTS_ANIMATION:
                     case SET_ON_PAGE_TRANSITION_END_CALLBACK:
                     case CANCEL_CURRENT_ANIMATION:
+                    case FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER:
                     case STATE_GESTURE_STARTED:
                     case STATE_END_TARGET_ANIMATION_FINISHED:
                     case STATE_CAPTURE_SCREENSHOT:
@@ -210,31 +238,36 @@
             // Check that all required events were found.
             errorDetected |= printErrorIfTrue(
                     !encounteredEvents.contains(GestureEvent.MOTION_DOWN),
-                    /* errorMessage= */ prefix + "\t\tMotion down never detected.",
+                    prefix,
+                    /* errorMessage= */ "Motion down never detected.",
                     writer);
             errorDetected |= printErrorIfTrue(
                     !encounteredEvents.contains(GestureEvent.MOTION_UP),
-                    /* errorMessage= */ prefix + "\t\tMotion up never detected.",
+                    prefix,
+                    /* errorMessage= */ "Motion up never detected.",
                     writer);
 
             errorDetected |= printErrorIfTrue(
                     /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
                             && !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET),
-                    /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but "
+                    prefix,
+                    /* errorMessage= */ "setEndTarget was called, but "
                             + "onSettledOnEndTarget wasn't.",
                     writer);
             errorDetected |= printErrorIfTrue(
                     /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
                             && !encounteredEvents.contains(
                                     GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED),
-                    /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but "
+                    prefix,
+                    /* errorMessage= */ "setEndTarget was called, but "
                             + "STATE_END_TARGET_ANIMATION_FINISHED was never set.",
                     writer);
             errorDetected |= printErrorIfTrue(
                     /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
                             && !encounteredEvents.contains(
                                     GestureEvent.STATE_RECENTS_SCROLLING_FINISHED),
-                    /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but "
+                    prefix,
+                    /* errorMessage= */ "setEndTarget was called, but "
                             + "STATE_RECENTS_SCROLLING_FINISHED was never set.",
                     writer);
             errorDetected |= printErrorIfTrue(
@@ -243,7 +276,8 @@
                             && encounteredEvents.contains(
                                     GestureEvent.STATE_RECENTS_SCROLLING_FINISHED)
                             && !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET),
-                    /* errorMessage= */ prefix + "\t\tSTATE_END_TARGET_ANIMATION_FINISHED and "
+                    prefix,
+                    /* errorMessage= */ "STATE_END_TARGET_ANIMATION_FINISHED and "
                             + "STATE_RECENTS_SCROLLING_FINISHED were set, but onSettledOnEndTarget "
                             + "wasn't called.",
                     writer);
@@ -253,7 +287,8 @@
                             GestureEvent.START_RECENTS_ANIMATION)
                             && !encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION)
                             && !encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION),
-                    /* errorMessage= */ prefix + "\t\tstartRecentsAnimation was called, but "
+                    prefix,
+                    /* errorMessage= */ "startRecentsAnimation was called, but "
                             + "finishRecentsAnimation and cancelRecentsAnimation weren't.",
                     writer);
 
@@ -261,7 +296,8 @@
                     /* condition= */ encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED)
                             && !encounteredEvents.contains(GestureEvent.STATE_GESTURE_COMPLETED)
                             && !encounteredEvents.contains(GestureEvent.STATE_GESTURE_CANCELLED),
-                    /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_STARTED was set, but "
+                    prefix,
+                    /* errorMessage= */ "STATE_GESTURE_STARTED was set, but "
                             + "STATE_GESTURE_COMPLETED and STATE_GESTURE_CANCELLED weren't.",
                     writer);
 
@@ -269,7 +305,8 @@
                     /* condition= */ encounteredEvents.contains(
                             GestureEvent.STATE_CAPTURE_SCREENSHOT)
                             && !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED),
-                    /* errorMessage= */ prefix + "\t\tSTATE_CAPTURE_SCREENSHOT was set, but "
+                    prefix,
+                    /* errorMessage= */ "STATE_CAPTURE_SCREENSHOT was set, but "
                             + "STATE_SCREENSHOT_CAPTURED wasn't.",
                     writer);
 
@@ -278,15 +315,18 @@
                             GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK)
                             && !encounteredEvents.contains(
                                     GestureEvent.STATE_RECENTS_SCROLLING_FINISHED),
-                    /* errorMessage= */ prefix + "\t\tsetOnPageTransitionEndCallback called, but "
+                    prefix,
+                    /* errorMessage= */ "setOnPageTransitionEndCallback called, but "
                             + "STATE_RECENTS_SCROLLING_FINISHED wasn't set.",
                     writer);
 
             errorDetected |= printErrorIfTrue(
-                    /* condition= */ !encounteredEvents.contains(
-                            GestureEvent.CANCEL_CURRENT_ANIMATION)
+                    /* condition= */ encounteredEvents.contains(
+                            GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER)
+                            && !encounteredEvents.contains(GestureEvent.CANCEL_CURRENT_ANIMATION)
                             && !encounteredEvents.contains(GestureEvent.STATE_HANDLER_INVALIDATED),
-                    /* errorMessage= */ prefix + "\t\tAbsSwipeUpHandler.cancelCurrentAnimation "
+                    prefix,
+                    /* errorMessage= */ "AbsSwipeUpHandler.cancelCurrentAnimation "
                             + "wasn't called and STATE_HANDLER_INVALIDATED wasn't set.",
                     writer);
 
@@ -294,23 +334,17 @@
                     /* condition= */ encounteredEvents.contains(
                             GestureEvent.STATE_RECENTS_ANIMATION_CANCELED)
                             && !encounteredEvents.contains(GestureEvent.CLEANUP_SCREENSHOT),
-                    /* errorMessage= */ prefix + "\t\tSTATE_RECENTS_ANIMATION_CANCELED was set but "
+                    prefix,
+                    /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED was set but "
                             + "the task screenshot wasn't cleaned up.",
                     writer);
 
             errorDetected |= printErrorIfTrue(
                     /* condition= */ encounteredEvents.contains(
-                            GestureEvent.SET_END_TARGET_LAST_TASK)
+                            GestureEvent.EXPECTING_TASK_APPEARED)
                             && !encounteredEvents.contains(GestureEvent.TASK_APPEARED),
-                    /* errorMessage= */ prefix + "\t\tend target set to last task, but "
-                            + "onTaskAppeared wasn't called.",
-                    writer);
-            errorDetected |= printErrorIfTrue(
-                    /* condition= */ encounteredEvents.contains(
-                            GestureEvent.SET_END_TARGET_NEW_TASK)
-                            && !encounteredEvents.contains(GestureEvent.TASK_APPEARED),
-                    /* errorMessage= */ prefix + "\t\tend target set to new task, but "
-                            + "onTaskAppeared wasn't called.",
+                    prefix,
+                    /* errorMessage= */ "onTaskAppeared was expected to be called but wasn't.",
                     writer);
 
             if (!errorDetected) {
@@ -320,11 +354,11 @@
     }
 
     private static boolean printErrorIfTrue(
-            boolean condition, String errorMessage, PrintWriter writer) {
+            boolean condition, String prefix, String errorMessage, PrintWriter writer) {
         if (!condition) {
             return false;
         }
-        writer.println(errorMessage);
+        writer.println(prefix + "\t\t- " + errorMessage);
         return true;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
deleted file mode 100644
index 483a1c6..0000000
--- a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2020 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.util;
-
-import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_180;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.WindowBounds;
-
-import java.util.ArrayList;
-
-/**
- * Utility class to hold the information abound a window bounds for split screen
- */
-@TargetApi(Build.VERSION_CODES.R)
-public class SplitScreenBounds {
-
-    public static final SplitScreenBounds INSTANCE = new SplitScreenBounds();
-    private final ArrayList<OnChangeListener> mListeners = new ArrayList<>();
-
-    @Nullable
-    private WindowBounds mBounds;
-
-    private SplitScreenBounds() { }
-
-    @UiThread
-    public void setSecondaryWindowBounds(@NonNull WindowBounds bounds) {
-        if (!bounds.equals(mBounds)) {
-            mBounds = bounds;
-            for (OnChangeListener listener : mListeners) {
-                listener.onSecondaryWindowBoundsChanged();
-            }
-        }
-    }
-
-    public @NonNull WindowBounds getSecondaryWindowBounds(Context context) {
-        if (mBounds == null) {
-            mBounds = createDefaultWindowBounds(context);
-        }
-        return mBounds;
-    }
-
-    /**
-     * Creates window bounds as 50% of device size
-     */
-    private static WindowBounds createDefaultWindowBounds(Context context) {
-        WindowMetrics wm = context.getSystemService(WindowManager.class).getMaximumWindowMetrics();
-        WindowBounds bounds = WindowBounds.fromWindowMetrics(wm);
-
-        int rotation = DisplayController.INSTANCE.get(context).getInfo().rotation;
-        int halfDividerSize = context.getResources()
-                .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
-
-        if (rotation == ROTATION_0 || rotation == ROTATION_180) {
-            bounds.bounds.top = bounds.insets.top + bounds.availableSize.y / 2 + halfDividerSize;
-            bounds.insets.top = 0;
-        } else {
-            bounds.bounds.left = bounds.insets.left + bounds.availableSize.x / 2 + halfDividerSize;
-            bounds.insets.left = 0;
-        }
-        return new WindowBounds(bounds.bounds, bounds.insets);
-    }
-
-    public void addOnChangeListener(OnChangeListener listener) {
-        mListeners.add(listener);
-    }
-
-    public void removeOnChangeListener(OnChangeListener listener) {
-        mListeners.remove(listener);
-    }
-
-    /**
-     * Interface to receive window bounds changes
-     */
-    public interface OnChangeListener {
-
-        /**
-         * Called when window bounds for secondary window changes
-         */
-        void onSecondaryWindowBoundsChanged();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 3119a77..08f9fa6 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -22,8 +22,10 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
+import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition;
 
 import android.annotation.NonNull;
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityThread;
 import android.app.PendingIntent;
@@ -33,6 +35,7 @@
 import android.content.pm.ShortcutInfo;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
@@ -40,6 +43,9 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 
 import androidx.annotation.Nullable;
@@ -57,13 +63,11 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
-import com.android.systemui.shared.system.RemoteTransitionRunner;
 
 import java.util.function.Consumer;
 
@@ -84,6 +88,8 @@
     private ItemInfo mItemInfo;
     private Intent mInitialTaskIntent;
     private int mInitialTaskId = INVALID_TASK_ID;
+    private String mInitialTaskPackageName;
+    private Intent mSecondTaskIntent;
     private int mSecondTaskId = INVALID_TASK_ID;
     private String mSecondTaskPackageName;
     private boolean mRecentsAnimationRunning;
@@ -95,6 +101,8 @@
     /** Represents where split is intended to be invoked from. */
     private StatsLogManager.EventEnum mSplitEvent;
 
+    private FloatingTaskView mFirstFloatingTaskView;
+
     public SplitSelectStateController(Context context, Handler handler, StateManager stateManager,
             DepthController depthController, StatsLogManager statsLogManager) {
         mContext = context;
@@ -106,19 +114,36 @@
     }
 
     /**
-     * To be called after first task selected
+     * To be called after first task selected in Overview.
      */
-    public void setInitialTaskSelect(int taskId, @StagePosition int stagePosition,
+    public void setInitialTaskSelect(Task task, @StagePosition int stagePosition,
             StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) {
-        mInitialTaskId = taskId;
+        mInitialTaskId = task.key.id;
+        mInitialTaskPackageName = task.getTopComponent().getPackageName();
         setInitialData(stagePosition, splitEvent, itemInfo);
     }
 
+    /**
+     * To be called after first task selected from home or all apps.
+     */
     public void setInitialTaskSelect(Intent intent, @StagePosition int stagePosition,
             @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent) {
         mInitialTaskIntent = intent;
         mUser = itemInfo.user;
         mItemInfo = itemInfo;
+        mInitialTaskPackageName = intent.getComponent().getPackageName();
+        setInitialData(stagePosition, splitEvent, itemInfo);
+    }
+
+    /**
+     * To be called after first task selected from using a split shortcut from the fullscreen
+     * running app.
+     */
+    public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info,
+            @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
+            StatsLogManager.EventEnum splitEvent) {
+        mInitialTaskId = info.taskId;
+        mInitialTaskPackageName = info.topActivity.getPackageName();
         setInitialData(stagePosition, splitEvent, itemInfo);
     }
 
@@ -134,27 +159,11 @@
      * to be launched. Call after launcher side animations are complete.
      */
     public void launchSplitTasks(Consumer<Boolean> callback) {
-        final Intent fillInIntent;
-        if (mInitialTaskIntent != null) {
-            fillInIntent = new Intent();
-            if (TextUtils.equals(mInitialTaskIntent.getComponent().getPackageName(),
-                    mSecondTaskPackageName)) {
-                fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
-            }
-        } else {
-            fillInIntent = null;
-        }
-
-        final PendingIntent pendingIntent = mInitialTaskIntent == null ? null : (mUser != null
-                ? PendingIntent.getActivityAsUser(mContext, 0, mInitialTaskIntent,
-                FLAG_MUTABLE, null /* options */, mUser)
-                : PendingIntent.getActivity(mContext, 0, mInitialTaskIntent, FLAG_MUTABLE));
-
         Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
                 LogUtils.getShellShareableInstanceId();
-        launchTasks(mInitialTaskId, pendingIntent, fillInIntent, mSecondTaskId, mStagePosition,
-                callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
-                instanceIds.first);
+        launchTasks(mInitialTaskId, mInitialTaskIntent, mInitialTaskPackageName, mSecondTaskId,
+                mSecondTaskIntent, mSecondTaskPackageName, mStagePosition, callback,
+                false /* freezeTaskList */, DEFAULT_SPLIT_RATIO, instanceIds.first);
 
         mStatsLogManager.logger()
                 .withItemInfo(mItemInfo)
@@ -162,23 +171,25 @@
                 .log(mSplitEvent);
     }
 
-
     /**
      * To be called as soon as user selects the second task (even if animations aren't complete)
      * @param task The second task that will be launched.
      */
     public void setSecondTask(Task task) {
         mSecondTaskId = task.key.id;
-        if (mInitialTaskIntent != null) {
-            mSecondTaskPackageName = task.getTopComponent().getPackageName();
-        }
+        mSecondTaskPackageName = task.getTopComponent().getPackageName();
+    }
+
+    public void setSecondTask(Intent intent) {
+        mSecondTaskIntent = intent;
+        mSecondTaskPackageName = intent.getComponent().getPackageName();
     }
 
     /**
      * To be called when we want to launch split pairs from an existing GroupedTaskView.
      */
-    public void launchTasks(GroupedTaskView groupedTaskView,
-            Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTasks(GroupedTaskView groupedTaskView, Consumer<Boolean> callback,
+            boolean freezeTaskList) {
         mLaunchingTaskView = groupedTaskView;
         TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers =
                 groupedTaskView.getTaskIdAttributeContainers();
@@ -194,22 +205,23 @@
      */
     public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition,
             Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
-        launchTasks(taskId1, null /* taskPendingIntent */, null /* fillInIntent */, taskId2,
-                stagePosition, callback, freezeTaskList, splitRatio, null);
+        launchTasks(taskId1, null /* intent1 */, null /* packageName1 */, taskId2,
+                null /* intent2 */, null /* packageName2 */, stagePosition, callback,
+                freezeTaskList, splitRatio, null);
     }
 
     /**
      * To be called when we want to launch split pairs from Overview. Split can be initiated from
      * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a
      * fill in intent with a taskId2 are set.
-     * @param taskPendingIntent is null when split is initiated from Overview
+     * @param intent1 is null when split is initiated from Overview
      * @param stagePosition representing location of task1
      * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that create
      *                   a split instance, null for cases that bring existing instaces to the
      *                   foreground (quickswitch, launching previous pairs from overview)
      */
-    public void launchTasks(int taskId1, @Nullable PendingIntent taskPendingIntent,
-            @Nullable Intent fillInIntent, int taskId2, @StagePosition int stagePosition,
+    public void launchTasks(int taskId1, @Nullable Intent intent1, String packageName1, int taskId2,
+            @Nullable Intent intent2, String packageName2, @StagePosition int stagePosition,
             Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
             @Nullable InstanceId shellInstanceId) {
         TestLogging.recordEvent(
@@ -220,57 +232,105 @@
         }
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteSplitLaunchTransitionRunner animationRunner =
-                    new RemoteSplitLaunchTransitionRunner(taskId1, taskPendingIntent, taskId2,
-                            callback);
-            final RemoteTransitionCompat remoteTransition = new RemoteTransitionCompat(
-                    animationRunner, MAIN_EXECUTOR,
+                    new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback);
+            final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
                     ActivityThread.currentActivityThread().getApplicationThread());
-            if (taskPendingIntent == null) {
+            if (intent1 == null && intent2 == null) {
                 mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
                         null /* options2 */, stagePosition, splitRatio, remoteTransition,
                         shellInstanceId);
+            } else if (intent2 == null) {
+                launchIntentOrShortcut(intent1, packageName2, options1, taskId2, stagePosition,
+                        splitRatio, remoteTransition, shellInstanceId);
+            } else if (intent1 == null) {
+                launchIntentOrShortcut(intent2, packageName1, options1, taskId1,
+                        getOppositeStagePosition(stagePosition), splitRatio, remoteTransition,
+                        shellInstanceId);
             } else {
-                final ShortcutInfo shortcutInfo = getShortcutInfo(mInitialTaskIntent,
-                        taskPendingIntent.getCreatorUserHandle());
-                if (shortcutInfo != null) {
-                    mSystemUiProxy.startShortcutAndTask(shortcutInfo,
-                            options1.toBundle(), taskId2, null /* options2 */, stagePosition,
-                            splitRatio, remoteTransition, shellInstanceId);
-                } else {
-                    mSystemUiProxy.startIntentAndTask(taskPendingIntent,
-                            fillInIntent, options1.toBundle(), taskId2, null /* options2 */,
-                            stagePosition, splitRatio, remoteTransition, shellInstanceId);
-                }
+                // TODO: the case when both split apps are started from an intent.
             }
         } else {
             final RemoteSplitLaunchAnimationRunner animationRunner =
-                    new RemoteSplitLaunchAnimationRunner(taskId1, taskPendingIntent, taskId2,
-                            callback);
+                    new RemoteSplitLaunchAnimationRunner(taskId1, taskId2, callback);
             final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
-                    RemoteAnimationAdapterCompat.wrapRemoteAnimationRunner(animationRunner),
-                    300, 150,
+                    animationRunner, 300, 150,
                     ActivityThread.currentActivityThread().getApplicationThread());
 
-            if (taskPendingIntent == null) {
+            if (intent1 == null && intent2 == null) {
                 mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
                         taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
                         shellInstanceId);
+            } else if (intent2 == null) {
+                launchIntentOrShortcutLegacy(intent1, packageName2, options1, taskId2,
+                        stagePosition, splitRatio, adapter, shellInstanceId);
+            } else if (intent1 == null) {
+                launchIntentOrShortcutLegacy(intent2, packageName1, options1, taskId1,
+                        getOppositeStagePosition(stagePosition), splitRatio, adapter,
+                        shellInstanceId);
             } else {
-                final ShortcutInfo shortcutInfo = getShortcutInfo(mInitialTaskIntent,
-                        taskPendingIntent.getCreatorUserHandle());
-                if (shortcutInfo != null) {
-                    mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
-                            options1.toBundle(), taskId2, null /* options2 */, stagePosition,
-                            splitRatio, adapter, shellInstanceId);
-                } else {
-                    mSystemUiProxy.startIntentAndTaskWithLegacyTransition(taskPendingIntent,
-                            fillInIntent, options1.toBundle(), taskId2, null /* options2 */,
-                            stagePosition, splitRatio, adapter, shellInstanceId);
-                }
+                // TODO: the case when both split apps are started from an intent.
             }
         }
     }
 
+    private void launchIntentOrShortcut(Intent intent, String otherTaskPackageName,
+            ActivityOptions options1, int taskId, @StagePosition int stagePosition,
+            float splitRatio, RemoteTransition remoteTransition,
+            @Nullable InstanceId shellInstanceId) {
+        PendingIntent pendingIntent = getPendingIntent(intent);
+        final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
+                pendingIntent.getCreatorUserHandle());
+        if (shortcutInfo != null) {
+            mSystemUiProxy.startShortcutAndTask(shortcutInfo,
+                    options1.toBundle(), taskId, null /* options2 */, stagePosition,
+                    splitRatio, remoteTransition, shellInstanceId);
+        } else {
+            mSystemUiProxy.startIntentAndTask(pendingIntent,
+                    getFillInIntent(intent, otherTaskPackageName), options1.toBundle(), taskId,
+                    null /* options2 */, stagePosition, splitRatio, remoteTransition,
+                    shellInstanceId);
+        }
+    }
+
+    private void launchIntentOrShortcutLegacy(Intent intent, String otherTaskPackageName,
+            ActivityOptions options1, int taskId, @StagePosition int stagePosition,
+            float splitRatio, RemoteAnimationAdapter adapter,
+            @Nullable InstanceId shellInstanceId) {
+        PendingIntent pendingIntent = getPendingIntent(intent);
+        final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
+                pendingIntent.getCreatorUserHandle());
+        if (shortcutInfo != null) {
+            mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
+                    options1.toBundle(), taskId, null /* options2 */, stagePosition,
+                    splitRatio, adapter, shellInstanceId);
+        } else {
+            mSystemUiProxy.startIntentAndTaskWithLegacyTransition(pendingIntent,
+                    getFillInIntent(intent, otherTaskPackageName), options1.toBundle(), taskId,
+                    null /* options2 */, stagePosition, splitRatio, adapter,
+                    shellInstanceId);
+        }
+    }
+
+    private PendingIntent getPendingIntent(Intent intent) {
+        return intent == null ? null : (mUser != null
+                ? PendingIntent.getActivityAsUser(mContext, 0, intent,
+                FLAG_MUTABLE, null /* options */, mUser)
+                : PendingIntent.getActivity(mContext, 0, intent, FLAG_MUTABLE));
+    }
+
+    private Intent getFillInIntent(Intent intent, String otherTaskPackageName) {
+        if (intent == null) {
+            return null;
+        }
+
+        Intent fillInIntent = new Intent();
+        if (TextUtils.equals(intent.getComponent().getPackageName(), otherTaskPackageName)) {
+            fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+        return fillInIntent;
+    }
+
+
     public @StagePosition int getActiveSplitStagePosition() {
         return mStagePosition;
     }
@@ -280,7 +340,7 @@
     }
 
     public void setRecentsAnimationRunning(boolean running) {
-        this.mRecentsAnimationRunning = running;
+        mRecentsAnimationRunning = running;
     }
 
     @Nullable
@@ -308,52 +368,63 @@
     /**
      * Requires Shell Transitions
      */
-    private class RemoteSplitLaunchTransitionRunner implements RemoteTransitionRunner {
+    private class RemoteSplitLaunchTransitionRunner extends IRemoteTransition.Stub {
 
         private final int mInitialTaskId;
-        private final PendingIntent mInitialTaskPendingIntent;
         private final int mSecondTaskId;
         private final Consumer<Boolean> mSuccessCallback;
 
-        RemoteSplitLaunchTransitionRunner(int initialTaskId, PendingIntent initialTaskPendingIntent,
-                int secondTaskId, Consumer<Boolean> callback) {
+        RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
+                Consumer<Boolean> callback) {
             mInitialTaskId = initialTaskId;
-            mInitialTaskPendingIntent = initialTaskPendingIntent;
             mSecondTaskId = secondTaskId;
             mSuccessCallback = callback;
         }
 
         @Override
-        public void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
-            TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
-                    mDepthController, mInitialTaskId, mInitialTaskPendingIntent, mSecondTaskId,
-                    info, t, () -> {
-                    finishCallback.run();
-                    if (mSuccessCallback != null) {
-                        mSuccessCallback.accept(true);
-                    }
-                });
-            // After successful launch, call resetState
-            resetState();
+        public void startAnimation(IBinder transition, TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishedCallback) {
+            final Runnable finishAdapter = () ->  {
+                try {
+                    finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to call transition finished callback", e);
+                }
+            };
+
+            MAIN_EXECUTOR.execute(() -> {
+                TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
+                        mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> {
+                            finishAdapter.run();
+                            if (mSuccessCallback != null) {
+                                mSuccessCallback.accept(true);
+                            }
+                        });
+                // After successful launch, call resetState
+                resetState();
+            });
         }
+
+        @Override
+        public void mergeAnimation(IBinder transition, TransitionInfo info,
+                SurfaceControl.Transaction t, IBinder mergeTarget,
+                IRemoteTransitionFinishedCallback finishedCallback) { }
     }
 
     /**
      * LEGACY
      * Remote animation runner for animation to launch an app.
      */
-    private class RemoteSplitLaunchAnimationRunner implements RemoteAnimationRunnerCompat {
+    private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat {
 
         private final int mInitialTaskId;
-        private final PendingIntent mInitialTaskPendingIntent;
         private final int mSecondTaskId;
         private final Consumer<Boolean> mSuccessCallback;
 
-        RemoteSplitLaunchAnimationRunner(int initialTaskId, PendingIntent initialTaskPendingIntent,
-                int secondTaskId, Consumer<Boolean> successCallback) {
+        RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
+                Consumer<Boolean> successCallback) {
             mInitialTaskId = initialTaskId;
-            mInitialTaskPendingIntent = initialTaskPendingIntent;
             mSecondTaskId = secondTaskId;
             mSuccessCallback = successCallback;
         }
@@ -364,9 +435,8 @@
                 Runnable finishedCallback) {
             postAsyncCallback(mHandler,
                     () -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
-                            mLaunchingTaskView, mInitialTaskId, mInitialTaskPendingIntent,
-                            mSecondTaskId, apps, wallpapers, nonApps, mStateManager,
-                            mDepthController, () -> {
+                            mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
+                            nonApps, mStateManager, mDepthController, () -> {
                                 finishedCallback.run();
                                 if (mSuccessCallback != null) {
                                     mSuccessCallback.accept(true);
@@ -376,7 +446,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
             postAsyncCallback(mHandler, () -> {
                 if (mSuccessCallback != null) {
                     // Launching legacy tasks while recents animation is running will always cause
@@ -394,7 +464,10 @@
     public void resetState() {
         mInitialTaskId = INVALID_TASK_ID;
         mInitialTaskIntent = null;
+        mInitialTaskPackageName = null;
         mSecondTaskId = INVALID_TASK_ID;
+        mSecondTaskIntent = null;
+        mSecondTaskPackageName = null;
         mStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
@@ -407,7 +480,7 @@
      *         chosen
      */
     public boolean isSplitSelectActive() {
-        return isInitialTaskIntentSet() && mSecondTaskId == INVALID_TASK_ID;
+        return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
     }
 
     /**
@@ -415,7 +488,7 @@
      *          be launched
      */
     public boolean isBothSplitAppsConfirmed() {
-        return isInitialTaskIntentSet() && mSecondTaskId != INVALID_TASK_ID;
+        return isInitialTaskIntentSet() && isSecondTaskIntentSet();
     }
 
     private boolean isInitialTaskIntentSet() {
@@ -425,4 +498,16 @@
     public int getInitialTaskId() {
         return mInitialTaskId;
     }
+
+    private boolean isSecondTaskIntentSet() {
+        return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null);
+    }
+
+    public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
+        mFirstFloatingTaskView = floatingTaskView;
+    }
+
+    public FloatingTaskView getFirstFloatingTaskView() {
+        return mFirstFloatingTaskView;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
new file mode 100644
index 0000000..3587bd1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -0,0 +1,193 @@
+/*
+ * 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.util;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.view.View;
+
+import androidx.annotation.BinderThread;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.views.FloatingTaskView;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+/** Transitions app from fullscreen to stage split when triggered from keyboard shortcuts. */
+public class SplitWithKeyboardShortcutController {
+
+    private final QuickstepLauncher mLauncher;
+    private final SplitSelectStateController mController;
+    private final OverviewComponentObserver mOverviewComponentObserver;
+
+    private final int mSplitPlaceholderSize;
+    private final int mSplitPlaceholderInset;
+
+    public SplitWithKeyboardShortcutController(QuickstepLauncher launcher,
+            SplitSelectStateController controller) {
+        mLauncher = launcher;
+        mController = controller;
+        RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
+                launcher.getApplicationContext());
+        mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(),
+                deviceState);
+
+        mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
+                R.dimen.split_placeholder_size);
+        mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
+                R.dimen.split_placeholder_inset);
+    }
+
+    @BinderThread
+    public void enterStageSplit(boolean leftOrTop) {
+        if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()) {
+            return;
+        }
+        RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
+                SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
+                false /* allowMinimizeSplitScreen */);
+        SplitWithKeyboardShortcutRecentsAnimationListener listener =
+                new SplitWithKeyboardShortcutRecentsAnimationListener(leftOrTop);
+
+        MAIN_EXECUTOR.execute(() -> {
+            callbacks.addListener(listener);
+            UI_HELPER_EXECUTOR.execute(
+                    // Transition from fullscreen app to enter stage split in launcher with
+                    // recents animation.
+                    () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+                            mOverviewComponentObserver.getOverviewIntent(),
+                            SystemClock.uptimeMillis(), callbacks, null, null));
+        });
+    }
+
+    /**
+     * Handles second app selection from stage split. If the item can't be opened in split or
+     * it's not in stage split state, we pass it onto Launcher's default item click handler.
+     */
+    public boolean handleSecondAppSelectionForSplit(View view) {
+        if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
+                || !mController.isSplitSelectActive()) {
+            return false;
+        }
+        Object tag = view.getTag();
+        Intent intent;
+        if (tag instanceof WorkspaceItemInfo) {
+            final WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) tag;
+            intent = workspaceItemInfo.intent;
+        } else if (tag instanceof com.android.launcher3.model.data.AppInfo) {
+            final com.android.launcher3.model.data.AppInfo appInfo =
+                    (com.android.launcher3.model.data.AppInfo) tag;
+            intent = appInfo.intent;
+        } else {
+            return false;
+        }
+        mController.setSecondTask(intent);
+        mController.launchSplitTasks(aBoolean -> mLauncher.getDragLayer().removeView(
+                mController.getFirstFloatingTaskView()));
+        return true;
+    }
+
+    public void onDestroy() {
+        mOverviewComponentObserver.onDestroy();
+    }
+
+    private class SplitWithKeyboardShortcutRecentsAnimationListener implements
+            RecentsAnimationCallbacks.RecentsAnimationListener {
+
+        private final boolean mLeftOrTop;
+        private final Rect mTempRect = new Rect();
+
+        private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) {
+            mLeftOrTop = leftOrTop;
+        }
+
+        @Override
+        public void onRecentsAnimationStart(RecentsAnimationController controller,
+                RecentsAnimationTargets targets) {
+            ActivityManager.RunningTaskInfo runningTaskInfo =
+                    ActivityManagerWrapper.getInstance().getRunningTask();
+            mController.setInitialTaskSelect(runningTaskInfo,
+                    mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT,
+                    null /* itemInfo */,
+                    mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP
+                            : LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM);
+
+            RecentsView recentsView = mLauncher.getOverviewPanel();
+            recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
+                    mSplitPlaceholderSize, mSplitPlaceholderInset, mLauncher.getDeviceProfile(),
+                    mController.getActiveSplitStagePosition(), mTempRect);
+
+            PendingAnimation anim = new PendingAnimation(
+                    SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
+            RectF startingTaskRect = new RectF();
+            final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
+                    mLauncher, mLauncher.getDragLayer(),
+                    controller.screenshotTask(runningTaskInfo.taskId).thumbnail,
+                    null /* icon */, startingTaskRect);
+            RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
+                    .getIconCache()
+                    .updateIconInBackground(
+                            Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
+                                    false /* isLocked */),
+                            (task) -> {
+                                if (task.thumbnail != null) {
+                                    floatingTaskView.setIcon(task.thumbnail.thumbnail);
+                                }
+                            });
+            floatingTaskView.setAlpha(1);
+            floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
+                    false /* fadeWithThumbnail */, true /* isStagedTask */);
+            mController.setFirstFloatingTaskView(floatingTaskView);
+
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    controller.finish(true /* toRecents */, null /* onFinishComplete */,
+                            false /* sendUserLeaveHint */);
+                }
+            });
+            anim.buildAnim().start();
+        }
+    };
+}
diff --git a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
index 580cc99..3756b4a 100644
--- a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
@@ -26,7 +26,7 @@
     public int getPlaceholderIconFadeInStart() { return 167; }
     public int getPlaceholderIconFadeInEnd() { return 250; }
     public int getStagedRectSlideStart() { return 0; }
-    public int getStagedRectSlideEnd() { return 383; }
+    public int getStagedRectSlideEnd() { return 500; }
 
     public int getDuration() { return TABLET_CONFIRM_DURATION; }
 }
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index 50be5ea..d098ffc 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -99,6 +99,10 @@
         return false;
     }
 
+    public float getScrollAlpha() {
+        return mScrollAlpha;
+    }
+
     public void setContentAlpha(float alpha) {
         if (mContentAlpha != alpha) {
             mContentAlpha = alpha;
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index dc1ae52..1d421b2 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -11,6 +11,7 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -74,6 +75,7 @@
         }
     };
 
+    private int mSplitHolderSize;
     private FloatingTaskThumbnailView mThumbnailView;
     private SplitPlaceholderView mSplitPlaceholderView;
     private RectF mStartingPosition;
@@ -97,6 +99,9 @@
         mActivity = BaseActivity.fromContext(context);
         mIsRtl = Utilities.isRtl(getResources());
         mFullscreenParams = new FullscreenDrawParams(context);
+
+        mSplitHolderSize = context.getResources().getDimensionPixelSize(
+                R.dimen.split_placeholder_icon_size);
     }
 
     @Override
@@ -126,8 +131,7 @@
         RecentsView recentsView = launcher.getOverviewPanel();
         mOrientationHandler = recentsView.getPagedOrientationHandler();
         mStagePosition = recentsView.getSplitSelectController().getActiveSplitStagePosition();
-        mSplitPlaceholderView.setIcon(icon,
-                mContext.getResources().getDimensionPixelSize(R.dimen.split_placeholder_icon_size));
+        mSplitPlaceholderView.setIcon(icon, mSplitHolderSize);
         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
     }
 
@@ -193,6 +197,10 @@
         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
     }
 
+    public void setIcon(Bitmap icon) {
+        mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize);
+    }
+
     protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
         mStartingPosition.set(pos);
         lp.ignoreInsets = true;
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index a16ff8f..eeabdc8 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -82,6 +82,8 @@
     private static final int INDEX_FULLSCREEN_ALPHA = 2;
     private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
     private static final int INDEX_SHARE_TARGET_ALPHA = 4;
+    private static final int INDEX_SCROLL_ALPHA = 5;
+    private static final int NUM_ALPHAS = 6;
 
     public @interface SplitButtonHiddenFlags { }
     public static final int FLAG_IS_NOT_TABLET = 1 << 0;
@@ -126,7 +128,7 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mMultiValueAlpha = new MultiValueAlpha(findViewById(R.id.action_buttons), 5);
+        mMultiValueAlpha = new MultiValueAlpha(findViewById(R.id.action_buttons), NUM_ALPHAS);
         mMultiValueAlpha.setUpdateVisibility(true);
 
         findViewById(R.id.action_screenshot).setOnClickListener(this);
@@ -247,6 +249,10 @@
         return mMultiValueAlpha.get(INDEX_SHARE_TARGET_ALPHA);
     }
 
+    public MultiProperty getIndexScrollAlpha() {
+        return mMultiValueAlpha.get(INDEX_SCROLL_ALPHA);
+    }
+
     /**
      * Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar.
      */
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ce96b71..47374e0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -122,6 +122,7 @@
 import androidx.annotation.UiThread;
 import androidx.core.graphics.ColorUtils;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
 import com.android.launcher3.DeviceProfile;
@@ -178,7 +179,6 @@
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitAnimationTimings;
-import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.SurfaceTransaction;
 import com.android.quickstep.util.SurfaceTransactionApplier;
@@ -210,7 +210,7 @@
 public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
-        TaskVisualsChangeListener, SplitScreenBounds.OnChangeListener {
+        TaskVisualsChangeListener {
 
     private static final String TAG = "RecentsView";
     private static final boolean DEBUG = false;
@@ -948,7 +948,6 @@
         SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
                 mIPipAnimationListener);
         mOrientationState.initListeners();
-        SplitScreenBounds.INSTANCE.addOnChangeListener(this);
         mTaskOverlayFactory.initListeners();
     }
 
@@ -965,7 +964,6 @@
         executeSideTaskLaunchCallback();
         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
-        SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
         mIPipAnimationListener.setActivityAndRecentsView(null, null);
         mOrientationState.destroyListeners();
         mTaskOverlayFactory.removeListeners();
@@ -1913,6 +1911,9 @@
         }
         int scroll = mOrientationHandler.getPrimaryScroll(this);
         mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
+
+        // Clear all button alpha was set by the previous line.
+        mActionsView.getIndexScrollAlpha().setValue(1 - mClearAllButton.getScrollAlpha());
     }
 
     @Override
@@ -2241,8 +2242,14 @@
         for (int i = 0; i < getTaskViewCount(); i++) {
             requireTaskViewAt(i).setOrientationState(mOrientationState);
         }
+        boolean shouldRotateMenuForFakeRotation =
+                !mOrientationState.isRecentsActivityRotationAllowed();
+        if (!shouldRotateMenuForFakeRotation) {
+            return;
+        }
         TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
         if (tv != null) {
+            // Rotation is supported on phone (details at b/254198019#comment4)
             tv.onRotationChanged();
         }
     }
@@ -2897,6 +2904,19 @@
 
         InteractionJankMonitorWrapper.begin(this,
                 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                if (mSplitHiddenTaskView == getRunningTaskView()) {
+                    finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
+                            null /* onFinishComplete */);
+                } else {
+                    switchToScreenshot(
+                            () -> finishRecentsAnimation(true /* toRecents */,
+                                    false /* shouldPip */, null /* onFinishComplete */));
+                }
+            }
+        });
         anim.addEndListener(success -> {
             if (success) {
                 InteractionJankMonitorWrapper.end(
@@ -4184,11 +4204,9 @@
     public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition,
             StatsLogManager.EventEnum splitEvent) {
         mSplitHiddenTaskView = taskView;
-        mSplitSelectStateController.setInitialTaskSelect(taskView.getTask().key.id,
+        mSplitSelectStateController.setInitialTaskSelect(taskView.getTask(),
                 stagePosition, splitEvent, taskView.getItemInfo());
         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
-        finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
-                null /* onFinishComplete */);
     }
 
     /**
@@ -5195,12 +5213,6 @@
         return null;
     }
 
-    @Override
-    public void onSecondaryWindowBoundsChanged() {
-        // Invalidate the task view size
-        setInsets(mInsets);
-    }
-
     /** Enables or disables modal state for RecentsView */
     public abstract void setModalStateEnabled(boolean isModalState, boolean animate);
 
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index b903691..83341cb 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -41,7 +41,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
@@ -136,21 +135,21 @@
     public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
             throws Exception {
         // WHEN newPredicationTask is executed with app predication of 5 apps.
-        AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className",
+        AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
                 mUserHandle);
-        AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className",
+        AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "provider1",
                 mUserHandle);
         AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
                 mUserHandle);
-        AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className",
+        AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
                 mUserHandle);
-        AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className",
+        AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
                 mUserHandle);
         mModelHelper.executeTaskForTest(
                 newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
                 .forEach(Runnable::run);
 
-        // THEN only 3 widgets are returned because
+        // THEN only 2 widgets are returned because
         // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
         //    excluded from the result.
         // 2. app3 doesn't have a widget.
@@ -159,45 +158,39 @@
                 .stream()
                 .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
                 .collect(Collectors.toList());
-        assertThat(recommendedWidgets).hasSize(3);
+        assertThat(recommendedWidgets).hasSize(2);
         assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
-        assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2);
-        assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+        assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
     }
 
     @Test
-    public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder()
+    public void widgetsRecommendationRan_shouldReturnPackageWidgetsWhenEmpty()
             throws Exception {
-        if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) {
-            return;
-        }
 
-        // WHEN newPredicationTask is executed with 5 predicated widgets.
-        AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
-                mUserHandle);
-        AppTarget widget2 = new AppTarget(new AppTargetId("app1"), "app1", "provider2",
+        // Not installed widget
+        AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider3",
                 mUserHandle);
         // Not installed app
         AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1",
                 mUserHandle);
-        // Not installed widget
-        AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider3",
+        // Workspace added widgets
+        AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
                 mUserHandle);
         AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
                 mUserHandle);
         mModelHelper.executeTaskForTest(
-                newWidgetsPredicationTask(List.of(widget5, widget3, widget2, widget4, widget1)))
+                newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1)))
                 .forEach(Runnable::run);
 
-        // THEN only 3 widgets are returned because the launcher only filters out non-exist widgets.
+        // THEN only 2 widgets are returned because the launcher only filters out non-exist widgets.
         List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
                 .stream()
                 .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
                 .collect(Collectors.toList());
-        assertThat(recommendedWidgets).hasSize(3);
-        assertWidgetInfo(recommendedWidgets.get(0).info, mApp5Provider1);
-        assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider2);
-        assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+        assertThat(recommendedWidgets).hasSize(2);
+        // Another widget from the same package
+        assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2);
+        assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
     }
 
     private void assertWidgetInfo(
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 39f93d9..5518dc8 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -30,11 +30,11 @@
     <LinearLayout
         android:id="@+id/folder_footer"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/folder_label_height"
+        android:layout_height="@dimen/folder_footer_height_default"
         android:clipChildren="false"
         android:orientation="horizontal"
-        android:paddingLeft="12dp"
-        android:paddingRight="12dp">
+        android:paddingLeft="@dimen/folder_footer_horiz_padding"
+        android:paddingRight="@dimen/folder_footer_horiz_padding">
 
         <com.android.launcher3.folder.FolderNameEditText
             android:id="@+id/folder_name"
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 9da3e87..b2a3a0d 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -30,6 +30,7 @@
         android:layout_height="wrap_content"
         android:layout_below="@id/collapse_handle"
         android:paddingBottom="16dp"
+        android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
         android:orientation="vertical">
 
         <TextView
@@ -40,7 +41,6 @@
             android:textSize="24sp"
             android:layout_marginTop="24dp"
             android:textColor="?android:attr/textColorSecondary"
-            android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
             android:text="@string/widget_button_text"/>
 
         <FrameLayout
@@ -49,7 +49,6 @@
             android:layout_height="wrap_content"
             android:elevation="0.1dp"
             android:background="?android:attr/colorBackground"
-            android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
             android:paddingBottom="8dp"
             android:clipToPadding="false"
             launcher:layout_sticky="true" >
@@ -63,7 +62,6 @@
             android:layout_marginTop="8dp"
             android:background="@drawable/widgets_recommendation_background"
             android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
-            android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
             android:visibility="gone" />
     </com.android.launcher3.views.StickyHeaderLayout>
 
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-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
index 7b2ed8b..09b2d6f 100644
--- a/res/values-sw720dp/dimens.xml
+++ b/res/values-sw720dp/dimens.xml
@@ -43,4 +43,7 @@
 
 <!-- Bottom sheet-->
     <dimen name="bottom_sheet_extra_top_padding">300dp</dimen>
+
+    <!--  Folder spaces  -->
+    <dimen name="folder_footer_horiz_padding">24dp</dimen>
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 283c793..f270b10 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -141,9 +141,12 @@
         <attr name="numColumns" format="integer" />
         <!--  numSearchContainerColumns defaults to numColumns, if not specified -->
         <attr name="numSearchContainerColumns" format="integer" />
+
         <!-- numFolderRows & numFolderColumns defaults to numRows & numColumns, if not specified -->
         <attr name="numFolderRows" format="integer" />
         <attr name="numFolderColumns" format="integer" />
+        <attr name="folderStyle" format="reference" />
+
         <!-- numAllAppsColumns defaults to numColumns, if not specified -->
         <attr name="numAllAppsColumns" format="integer" />
         <!-- Number of columns to use when extending the all-apps size,
@@ -333,19 +336,6 @@
         if not specified -->
         <attr name="allAppsBorderSpaceTwoPanelLandscapeVertical" format="float" />
 
-        <!-- defaults to minCellHeight if not specified
-        when GridDisplayOption#isScalable is true. -->
-        <attr name="folderCellHeight" format="float" />
-        <!-- defaults to minCellWidth, if not specified -->
-        <attr name="folderCellWidth" format="float" />
-
-        <!-- defaults to borderSpace, if not specified -->
-        <!-- space to be used horizontally and vertically -->
-        <attr name="folderBorderSpace" format="float" />
-
-        <!-- defaults to folderBorderSpace vertical, if not specified -->
-        <attr name="folderTopPadding" format="float" />
-
         <!-- defaults to res.hotseat_bar_bottom_space_default, if not specified -->
         <attr name="hotseatBarBottomSpace" format="float" />
         <!-- defaults to hotseatBarBottomSpace, if not specified -->
@@ -394,6 +384,22 @@
 
     </declare-styleable>
 
+    <declare-styleable name="FolderDisplayStyle">
+        <!-- defaults to minCellHeight if not specified
+        when GridDisplayOption#isScalable is true. -->
+        <attr name="folderCellHeight" format="dimension" />
+        <!-- defaults to minCellWidth, if not specified -->
+        <attr name="folderCellWidth" format="dimension" />
+        <!-- space to be used horizontally and vertically between icons,
+        and to the left and right of folder -->
+        <attr name="folderBorderSpace" format="dimension" />
+        <!-- height of the footer of the folder -->
+        <attr name="folderFooterHeight" format="dimension" />
+        <!-- padding on top of the folder -->
+        <attr name="folderTopPadding" format="dimension" />
+    </declare-styleable>
+
+
     <declare-styleable name="CellLayout">
         <attr name="containerType" format="integer">
             <enum name="workspace" value="0" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 11b6e8c..d9b3da5 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -205,6 +205,4 @@
     <!-- The max scale for the wallpaper when it's zoomed in -->
     <item name="config_wallpaperMaxScale" format="float" type="dimen">0</item>
 
-    <string name="floating_task_package" translatable="false"></string>
-    <string name="floating_task_action" translatable="false"></string>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0a28b6c..033346a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -255,7 +255,7 @@
     <dimen name="folder_cell_y_padding">6dp</dimen>
     <!-- label text size = workspace text size multiplied by this scale -->
     <dimen name="folder_label_text_scale">1.14</dimen>
-    <dimen name="folder_label_height">56dp</dimen>
+    <dimen name="folder_footer_height_default">56dp</dimen>
 
     <dimen name="folder_content_padding_left_right">8dp</dimen>
     <dimen name="folder_content_padding_top">16dp</dimen>
@@ -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>
@@ -380,6 +382,11 @@
     <dimen name="taskbar_button_margin_6_5">0dp</dimen>
     <dimen name="taskbar_button_margin_4_5">0dp</dimen>
     <dimen name="taskbar_button_margin_4_4">0dp</dimen>
+    <!-- Taskbar swipe up thresholds threshold -->
+    <dimen name="taskbar_nav_threshold">0dp</dimen>
+    <dimen name="taskbar_app_window_threshold">0dp</dimen>
+    <dimen name="taskbar_home_overview_threshold">0dp</dimen>
+    <dimen name="taskbar_catch_up_threshold">0dp</dimen>
 
     <!-- Size of the maximum radius for the enforced rounded rectangles. -->
     <dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
@@ -434,4 +441,8 @@
 
     <!-- State transition -->
     <item name="workspace_content_scale" format="float" type="dimen">0.97</item>
+
+    <!--  Folder spaces  -->
+    <dimen name="folder_top_padding_default">24dp</dimen>
+    <dimen name="folder_footer_horiz_padding">20dp</dimen>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d0be420..9e75a31 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -309,4 +309,13 @@
         <item name="android:windowLightStatusBar">true</item>
         <item name="android:windowTranslucentStatus">true</item>
     </style>
+
+    <style name="FolderDefaultStyle">
+        <item name="folderTopPadding">24dp</item>
+        <item name="folderCellHeight">94dp</item>
+        <item name="folderCellWidth">80dp</item>
+        <item name="folderBorderSpace">16dp</item>
+        <item name="folderFooterHeight">56dp</item>
+    </style>
+
 </resources>
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 75d7b6b..2103593 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -192,7 +192,7 @@
     private final Rect mOccupiedRect = new Rect();
     private final int[] mDirectionVector = new int[2];
 
-    final int[] mPreviousReorderDirection = new int[2];
+    ItemConfiguration mPreviousSolution = null;
     private static final int INVALID_DIRECTION = -100;
 
     private final Rect mTempRect = new Rect();
@@ -246,9 +246,6 @@
         mOccupied =  new GridOccupancy(mCountX, mCountY);
         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
 
-        mPreviousReorderDirection[0] = INVALID_DIRECTION;
-        mPreviousReorderDirection[1] = INVALID_DIRECTION;
-
         mFolderLeaveBehind.mDelegateCellX = -1;
         mFolderLeaveBehind.mDelegateCellY = -1;
 
@@ -376,7 +373,8 @@
     private void resetCellSizeInternal(DeviceProfile deviceProfile) {
         switch (mContainerType) {
             case FOLDER:
-                mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx);
+                mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx,
+                        deviceProfile.folderCellLayoutBorderSpacePx);
                 break;
             case HOTSEAT:
                 mBorderSpace = new Point(deviceProfile.hotseatBorderSpace,
@@ -405,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();
@@ -941,7 +938,7 @@
         cellToRect(targetCell[0], targetCell[1], spanX, spanY, cellBoundsWithSpacing);
         cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2);
 
-        if (canCreateFolder(getChildAt(targetCell[0], targetCell[1]))) {
+        if (canCreateFolder(getChildAt(targetCell[0], targetCell[1])) && spanX == 1 && spanY == 1) {
             // Take only the circle in the smaller dimension, to ensure we don't start reordering
             // too soon before accepting a folder drop.
             int minRadius = centerPoint[0] - cellBoundsWithSpacing.left;
@@ -951,8 +948,9 @@
             return minRadius;
         }
         // Take up the entire cell, including space between this cell and the adjacent ones.
-        return (float) Math.hypot(cellBoundsWithSpacing.width() / 2f,
-                cellBoundsWithSpacing.height() / 2f);
+        // Multiply by span to scale radius
+        return (float) Math.hypot(spanX * cellBoundsWithSpacing.width() / 2f,
+                spanY * cellBoundsWithSpacing.height() / 2f);
     }
 
     public int getCellWidth() {
@@ -1249,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.
@@ -1283,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.
@@ -1354,9 +1335,6 @@
                         hitMaxY |= ySize >= spanY;
                         incX = !incX;
                     }
-                    incX = true;
-                    hitMaxX = xSize >= spanX;
-                    hitMaxY = ySize >= spanY;
                 }
                 final int[] cellXY = mTmpPoint;
                 cellToCenterPoint(x, y, cellXY);
@@ -1364,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)) {
@@ -1395,653 +1372,9 @@
             bestXY[0] = -1;
             bestXY[1] = -1;
         }
-        recycleTempRects(validRegions);
         return bestXY;
     }
 
-    /**
-     * Find a vacant area that will fit the given bounds nearest the requested
-     * cell location, and will also weigh in a suggested direction vector of the
-     * desired location. This method computers distance based on unit grid distances,
-     * not pixel distances.
-     *
-     * @param cellX The X cell nearest to which you want to search for a vacant area.
-     * @param cellY The Y cell nearest which you want to search for a vacant area.
-     * @param spanX Horizontal span of the object.
-     * @param spanY Vertical span of the object.
-     * @param direction The favored direction in which the views should move from x, y
-     * @param occupied The array which represents which cells in the CellLayout are occupied
-     * @param blockOccupied The array which represents which cells in the specified block (cellX,
-     *        cellY, spanX, spanY) are occupied. This is used when try to move a group of views.
-     * @param result Array in which to place the result, or null (in which case a new array will
-     *        be allocated)
-     * @return The X, Y cell of a vacant area that can contain this object,
-     *         nearest the requested location.
-     */
-    private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
-            boolean[][] occupied, boolean blockOccupied[][], int[] result) {
-        // Keep track of best-scoring drop area
-        final int[] bestXY = result != null ? result : new int[2];
-        float bestDistance = Float.MAX_VALUE;
-        int bestDirectionScore = Integer.MIN_VALUE;
-
-        final int countX = mCountX;
-        final int countY = mCountY;
-
-        for (int y = 0; y < countY - (spanY - 1); y++) {
-            inner:
-            for (int x = 0; x < countX - (spanX - 1); x++) {
-                // First, let's see if this thing fits anywhere
-                for (int i = 0; i < spanX; i++) {
-                    for (int j = 0; j < spanY; j++) {
-                        if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
-                            continue inner;
-                        }
-                    }
-                }
-
-                float distance = (float) Math.hypot(x - cellX, y - cellY);
-                int[] curDirection = mTmpPoint;
-                computeDirectionVector(x - cellX, y - cellY, curDirection);
-                // The direction score is just the dot product of the two candidate direction
-                // and that passed in.
-                int curDirectionScore = direction[0] * curDirection[0] +
-                        direction[1] * curDirection[1];
-                if (Float.compare(distance,  bestDistance) < 0 ||
-                        (Float.compare(distance, bestDistance) == 0
-                                && curDirectionScore > bestDirectionScore)) {
-                    bestDistance = distance;
-                    bestDirectionScore = curDirectionScore;
-                    bestXY[0] = x;
-                    bestXY[1] = y;
-                }
-            }
-        }
-
-        // Return -1, -1 if no suitable location found
-        if (bestDistance == Float.MAX_VALUE) {
-            bestXY[0] = -1;
-            bestXY[1] = -1;
-        }
-        return bestXY;
-    }
-
-    private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
-            int[] direction, ItemConfiguration currentState) {
-        CellAndSpan c = currentState.map.get(v);
-        boolean success = false;
-        mTmpOccupied.markCells(c, false);
-        mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
-
-        findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
-                mTmpOccupied.cells, null, mTempLocation);
-
-        if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
-            c.cellX = mTempLocation[0];
-            c.cellY = mTempLocation[1];
-            success = true;
-        }
-        mTmpOccupied.markCells(c, true);
-        return success;
-    }
-
-    /**
-     * This helper class defines a cluster of views. It helps with defining complex edges
-     * of the cluster and determining how those edges interact with other views. The edges
-     * essentially define a fine-grained boundary around the cluster of views -- like a more
-     * precise version of a bounding box.
-     */
-    private class ViewCluster {
-        final static int LEFT = 1 << 0;
-        final static int TOP = 1 << 1;
-        final static int RIGHT = 1 << 2;
-        final static int BOTTOM = 1 << 3;
-
-        final ArrayList<View> views;
-        final ItemConfiguration config;
-        final Rect boundingRect = new Rect();
-
-        final int[] leftEdge = new int[mCountY];
-        final int[] rightEdge = new int[mCountY];
-        final int[] topEdge = new int[mCountX];
-        final int[] bottomEdge = new int[mCountX];
-        int dirtyEdges;
-        boolean boundingRectDirty;
-
-        @SuppressWarnings("unchecked")
-        public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
-            this.views = (ArrayList<View>) views.clone();
-            this.config = config;
-            resetEdges();
-        }
-
-        void resetEdges() {
-            for (int i = 0; i < mCountX; i++) {
-                topEdge[i] = -1;
-                bottomEdge[i] = -1;
-            }
-            for (int i = 0; i < mCountY; i++) {
-                leftEdge[i] = -1;
-                rightEdge[i] = -1;
-            }
-            dirtyEdges = LEFT | TOP | RIGHT | BOTTOM;
-            boundingRectDirty = true;
-        }
-
-        void computeEdge(int which) {
-            int count = views.size();
-            for (int i = 0; i < count; i++) {
-                CellAndSpan cs = config.map.get(views.get(i));
-                switch (which) {
-                    case LEFT:
-                        int left = cs.cellX;
-                        for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
-                            if (left < leftEdge[j] || leftEdge[j] < 0) {
-                                leftEdge[j] = left;
-                            }
-                        }
-                        break;
-                    case RIGHT:
-                        int right = cs.cellX + cs.spanX;
-                        for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
-                            if (right > rightEdge[j]) {
-                                rightEdge[j] = right;
-                            }
-                        }
-                        break;
-                    case TOP:
-                        int top = cs.cellY;
-                        for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
-                            if (top < topEdge[j] || topEdge[j] < 0) {
-                                topEdge[j] = top;
-                            }
-                        }
-                        break;
-                    case BOTTOM:
-                        int bottom = cs.cellY + cs.spanY;
-                        for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
-                            if (bottom > bottomEdge[j]) {
-                                bottomEdge[j] = bottom;
-                            }
-                        }
-                        break;
-                }
-            }
-        }
-
-        boolean isViewTouchingEdge(View v, int whichEdge) {
-            CellAndSpan cs = config.map.get(v);
-
-            if ((dirtyEdges & whichEdge) == whichEdge) {
-                computeEdge(whichEdge);
-                dirtyEdges &= ~whichEdge;
-            }
-
-            switch (whichEdge) {
-                case LEFT:
-                    for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
-                        if (leftEdge[i] == cs.cellX + cs.spanX) {
-                            return true;
-                        }
-                    }
-                    break;
-                case RIGHT:
-                    for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
-                        if (rightEdge[i] == cs.cellX) {
-                            return true;
-                        }
-                    }
-                    break;
-                case TOP:
-                    for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
-                        if (topEdge[i] == cs.cellY + cs.spanY) {
-                            return true;
-                        }
-                    }
-                    break;
-                case BOTTOM:
-                    for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
-                        if (bottomEdge[i] == cs.cellY) {
-                            return true;
-                        }
-                    }
-                    break;
-            }
-            return false;
-        }
-
-        void shift(int whichEdge, int delta) {
-            for (View v: views) {
-                CellAndSpan c = config.map.get(v);
-                switch (whichEdge) {
-                    case LEFT:
-                        c.cellX -= delta;
-                        break;
-                    case RIGHT:
-                        c.cellX += delta;
-                        break;
-                    case TOP:
-                        c.cellY -= delta;
-                        break;
-                    case BOTTOM:
-                    default:
-                        c.cellY += delta;
-                        break;
-                }
-            }
-            resetEdges();
-        }
-
-        public void addView(View v) {
-            views.add(v);
-            resetEdges();
-        }
-
-        public Rect getBoundingRect() {
-            if (boundingRectDirty) {
-                config.getBoundingRectForViews(views, boundingRect);
-            }
-            return boundingRect;
-        }
-
-        final PositionComparator comparator = new PositionComparator();
-        class PositionComparator implements Comparator<View> {
-            int whichEdge = 0;
-            public int compare(View left, View right) {
-                CellAndSpan l = config.map.get(left);
-                CellAndSpan r = config.map.get(right);
-                switch (whichEdge) {
-                    case LEFT:
-                        return (r.cellX + r.spanX) - (l.cellX + l.spanX);
-                    case RIGHT:
-                        return l.cellX - r.cellX;
-                    case TOP:
-                        return (r.cellY + r.spanY) - (l.cellY + l.spanY);
-                    case BOTTOM:
-                    default:
-                        return l.cellY - r.cellY;
-                }
-            }
-        }
-
-        public void sortConfigurationForEdgePush(int edge) {
-            comparator.whichEdge = edge;
-            Collections.sort(config.sortedViews, comparator);
-        }
-    }
-
-    private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
-            int[] direction, View dragView, ItemConfiguration currentState) {
-
-        ViewCluster cluster = new ViewCluster(views, currentState);
-        Rect clusterRect = cluster.getBoundingRect();
-        int whichEdge;
-        int pushDistance;
-        boolean fail = false;
-
-        // Determine the edge of the cluster that will be leading the push and how far
-        // the cluster must be shifted.
-        if (direction[0] < 0) {
-            whichEdge = ViewCluster.LEFT;
-            pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
-        } else if (direction[0] > 0) {
-            whichEdge = ViewCluster.RIGHT;
-            pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
-        } else if (direction[1] < 0) {
-            whichEdge = ViewCluster.TOP;
-            pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
-        } else {
-            whichEdge = ViewCluster.BOTTOM;
-            pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
-        }
-
-        // Break early for invalid push distance.
-        if (pushDistance <= 0) {
-            return false;
-        }
-
-        // Mark the occupied state as false for the group of views we want to move.
-        for (View v: views) {
-            CellAndSpan c = currentState.map.get(v);
-            mTmpOccupied.markCells(c, false);
-        }
-
-        // We save the current configuration -- if we fail to find a solution we will revert
-        // to the initial state. The process of finding a solution modifies the configuration
-        // in place, hence the need for revert in the failure case.
-        currentState.save();
-
-        // The pushing algorithm is simplified by considering the views in the order in which
-        // they would be pushed by the cluster. For example, if the cluster is leading with its
-        // left edge, we consider sort the views by their right edge, from right to left.
-        cluster.sortConfigurationForEdgePush(whichEdge);
-
-        while (pushDistance > 0 && !fail) {
-            for (View v: currentState.sortedViews) {
-                // For each view that isn't in the cluster, we see if the leading edge of the
-                // cluster is contacting the edge of that view. If so, we add that view to the
-                // cluster.
-                if (!cluster.views.contains(v) && v != dragView) {
-                    if (cluster.isViewTouchingEdge(v, whichEdge)) {
-                        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams();
-                        if (!lp.canReorder) {
-                            // The push solution includes the all apps button, this is not viable.
-                            fail = true;
-                            break;
-                        }
-                        cluster.addView(v);
-                        CellAndSpan c = currentState.map.get(v);
-
-                        // Adding view to cluster, mark it as not occupied.
-                        mTmpOccupied.markCells(c, false);
-                    }
-                }
-            }
-            pushDistance--;
-
-            // The cluster has been completed, now we move the whole thing over in the appropriate
-            // direction.
-            cluster.shift(whichEdge, 1);
-        }
-
-        boolean foundSolution = false;
-        clusterRect = cluster.getBoundingRect();
-
-        // Due to the nature of the algorithm, the only check required to verify a valid solution
-        // is to ensure that completed shifted cluster lies completely within the cell layout.
-        if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
-                clusterRect.bottom <= mCountY) {
-            foundSolution = true;
-        } else {
-            currentState.restore();
-        }
-
-        // In either case, we set the occupied array as marked for the location of the views
-        for (View v: cluster.views) {
-            CellAndSpan c = currentState.map.get(v);
-            mTmpOccupied.markCells(c, true);
-        }
-
-        return foundSolution;
-    }
-
-    private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
-            int[] direction, View dragView, ItemConfiguration currentState) {
-        if (views.size() == 0) return true;
-
-        boolean success = false;
-        Rect boundingRect = new Rect();
-        // We construct a rect which represents the entire group of views passed in
-        currentState.getBoundingRectForViews(views, boundingRect);
-
-        // Mark the occupied state as false for the group of views we want to move.
-        for (View v: views) {
-            CellAndSpan c = currentState.map.get(v);
-            mTmpOccupied.markCells(c, false);
-        }
-
-        GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height());
-        int top = boundingRect.top;
-        int left = boundingRect.left;
-        // We mark more precisely which parts of the bounding rect are truly occupied, allowing
-        // for interlocking.
-        for (View v: views) {
-            CellAndSpan c = currentState.map.get(v);
-            blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
-        }
-
-        mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
-
-        findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
-                boundingRect.height(), direction,
-                mTmpOccupied.cells, blockOccupied.cells, mTempLocation);
-
-        // If we successfuly found a location by pushing the block of views, we commit it
-        if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
-            int deltaX = mTempLocation[0] - boundingRect.left;
-            int deltaY = mTempLocation[1] - boundingRect.top;
-            for (View v: views) {
-                CellAndSpan c = currentState.map.get(v);
-                c.cellX += deltaX;
-                c.cellY += deltaY;
-            }
-            success = true;
-        }
-
-        // In either case, we set the occupied array as marked for the location of the views
-        for (View v: views) {
-            CellAndSpan c = currentState.map.get(v);
-            mTmpOccupied.markCells(c, true);
-        }
-        return success;
-    }
-
-    // This method tries to find a reordering solution which satisfies the push mechanic by trying
-    // to push items in each of the cardinal directions, in an order based on the direction vector
-    // passed.
-    private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
-            int[] direction, View ignoreView, ItemConfiguration solution) {
-        if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
-            // If the direction vector has two non-zero components, we try pushing
-            // separately in each of the components.
-            int temp = direction[1];
-            direction[1] = 0;
-
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-            direction[1] = temp;
-            temp = direction[0];
-            direction[0] = 0;
-
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-            // Revert the direction
-            direction[0] = temp;
-
-            // Now we try pushing in each component of the opposite direction
-            direction[0] *= -1;
-            direction[1] *= -1;
-            temp = direction[1];
-            direction[1] = 0;
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-
-            direction[1] = temp;
-            temp = direction[0];
-            direction[0] = 0;
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-            // revert the direction
-            direction[0] = temp;
-            direction[0] *= -1;
-            direction[1] *= -1;
-
-        } else {
-            // If the direction vector has a single non-zero component, we push first in the
-            // direction of the vector
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-            // Then we try the opposite direction
-            direction[0] *= -1;
-            direction[1] *= -1;
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-            // Switch the direction back
-            direction[0] *= -1;
-            direction[1] *= -1;
-
-            // If we have failed to find a push solution with the above, then we try
-            // to find a solution by pushing along the perpendicular axis.
-
-            // Swap the components
-            int temp = direction[1];
-            direction[1] = direction[0];
-            direction[0] = temp;
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-
-            // Then we try the opposite direction
-            direction[0] *= -1;
-            direction[1] *= -1;
-            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
-                    ignoreView, solution)) {
-                return true;
-            }
-            // Switch the direction back
-            direction[0] *= -1;
-            direction[1] *= -1;
-
-            // Swap the components back
-            temp = direction[1];
-            direction[1] = direction[0];
-            direction[0] = temp;
-        }
-        return false;
-    }
-
-    private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
-            View ignoreView, ItemConfiguration solution) {
-        // Return early if get invalid cell positions
-        if (cellX < 0 || cellY < 0) return false;
-
-        mIntersectingViews.clear();
-        mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
-
-        // Mark the desired location of the view currently being dragged.
-        if (ignoreView != null) {
-            CellAndSpan c = solution.map.get(ignoreView);
-            if (c != null) {
-                c.cellX = cellX;
-                c.cellY = cellY;
-            }
-        }
-        Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
-        Rect r1 = new Rect();
-        for (View child: solution.map.keySet()) {
-            if (child == ignoreView) continue;
-            CellAndSpan c = solution.map.get(child);
-            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
-            r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
-            if (Rect.intersects(r0, r1)) {
-                if (!lp.canReorder) {
-                    return false;
-                }
-                mIntersectingViews.add(child);
-            }
-        }
-
-        solution.intersectingViews = new ArrayList<>(mIntersectingViews);
-
-        // First we try to find a solution which respects the push mechanic. That is,
-        // we try to find a solution such that no displaced item travels through another item
-        // without also displacing that item.
-        if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
-                solution)) {
-            return true;
-        }
-
-        // Next we try moving the views as a block, but without requiring the push mechanic.
-        if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
-                solution)) {
-            return true;
-        }
-
-        // Ok, they couldn't move as a block, let's move them individually
-        for (View v : mIntersectingViews) {
-            if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /*
-     * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
-     * the provided point and the provided cell
-     */
-    private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
-        double angle = Math.atan(deltaY / deltaX);
-
-        result[0] = 0;
-        result[1] = 0;
-        if (Math.abs(Math.cos(angle)) > 0.5f) {
-            result[0] = (int) Math.signum(deltaX);
-        }
-        if (Math.abs(Math.sin(angle)) > 0.5f) {
-            result[1] = (int) Math.signum(deltaY);
-        }
-    }
-
-    private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY,
-            int spanX, int spanY, int[] direction, View dragView, boolean decX,
-            ItemConfiguration solution) {
-        // Copy the current state into the solution. This solution will be manipulated as necessary.
-        copyCurrentStateToSolution(solution, false);
-        // Copy the current occupied array into the temporary occupied array. This array will be
-        // manipulated as necessary to find a solution.
-        mOccupied.copyTo(mTmpOccupied);
-
-        // We find the nearest cell into which we would place the dragged item, assuming there's
-        // nothing in its way.
-        int result[] = new int[2];
-        result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
-
-        boolean success;
-        // First we try the exact nearest position of the item being dragged,
-        // we will then want to try to move this around to other neighbouring positions
-        success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
-                solution);
-
-        if (!success) {
-            // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
-            // x, then 1 in y etc.
-            if (spanX > minSpanX && (minSpanY == spanY || decX)) {
-                return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY,
-                        direction, dragView, false, solution);
-            } else if (spanY > minSpanY) {
-                return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1,
-                        direction, dragView, true, solution);
-            }
-            solution.isSolution = false;
-        } else {
-            solution.isSolution = true;
-            solution.cellX = result[0];
-            solution.cellY = result[1];
-            solution.spanX = spanX;
-            solution.spanY = spanY;
-        }
-        return solution;
-    }
-
-    private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
-        int childCount = mShortcutsAndWidgets.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = mShortcutsAndWidgets.getChildAt(i);
-            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
-            CellAndSpan c;
-            if (temp) {
-                c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan);
-            } else {
-                c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan);
-            }
-            solution.add(child, c);
-        }
-    }
-
     private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
         mTmpOccupied.clear();
 
@@ -2338,54 +1671,6 @@
         return solution;
     }
 
-    /* This seems like it should be obvious and straight-forward, but when the direction vector
-    needs to match with the notion of the dragView pushing other views, we have to employ
-    a slightly more subtle notion of the direction vector. The question is what two points is
-    the vector between? The center of the dragView and its desired destination? Not quite, as
-    this doesn't necessarily coincide with the interaction of the dragView and items occupying
-    those cells. Instead we use some heuristics to often lock the vector to up, down, left
-    or right, which helps make pushing feel right.
-    */
-    private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
-            int spanY, View dragView, int[] resultDirection) {
-
-        //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
-        int[] targetDestination = new int[2];
-
-        findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
-        Rect dragRect = new Rect();
-        cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
-        dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
-
-        Rect dropRegionRect = new Rect();
-        getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
-                dragView, dropRegionRect, mIntersectingViews);
-
-        int dropRegionSpanX = dropRegionRect.width();
-        int dropRegionSpanY = dropRegionRect.height();
-
-        cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
-                dropRegionRect.height(), dropRegionRect);
-
-        int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
-        int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
-
-        if (dropRegionSpanX == mCountX || spanX == mCountX) {
-            deltaX = 0;
-        }
-        if (dropRegionSpanY == mCountY || spanY == mCountY) {
-            deltaY = 0;
-        }
-
-        if (deltaX == 0 && deltaY == 0) {
-            // No idea what to do, give a random direction.
-            resultDirection[0] = 1;
-            resultDirection[1] = 0;
-        } else {
-            computeDirectionVector(deltaX, deltaY, resultDirection);
-        }
-    }
-
     // For a given cell and span, fetch the set of views intersecting the region.
     private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY,
             View dragView, Rect boundingRect, ArrayList<View> intersectingViews) {
@@ -2469,32 +1754,745 @@
         return swapSolution.isSolution;
     }
 
-    int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
-            View dragView, int[] result, int resultSpan[], int mode) {
-        // First we determine if things have moved enough to cause a different layout
+    /**
+     * Find a vacant area that will fit the given bounds nearest the requested
+     * cell location, and will also weigh in a suggested direction vector of the
+     * desired location. This method computers distance based on unit grid distances,
+     * not pixel distances.
+     *
+     * @param cellX The X cell nearest to which you want to search for a vacant area.
+     * @param cellY The Y cell nearest which you want to search for a vacant area.
+     * @param spanX Horizontal span of the object.
+     * @param spanY Vertical span of the object.
+     * @param direction The favored direction in which the views should move from x, y
+     * @param occupied The array which represents which cells in the CellLayout are occupied
+     * @param blockOccupied The array which represents which cells in the specified block (cellX,
+     *        cellY, spanX, spanY) are occupied. This is used when try to move a group of views.
+     * @param result Array in which to place the result, or null (in which case a new array will
+     *        be allocated)
+     * @return The X, Y cell of a vacant area that can contain this object,
+     *         nearest the requested location.
+     */
+    private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
+            boolean[][] occupied, boolean blockOccupied[][], int[] result) {
+        // Keep track of best-scoring drop area
+        final int[] bestXY = result != null ? result : new int[2];
+        float bestDistance = Float.MAX_VALUE;
+        int bestDirectionScore = Integer.MIN_VALUE;
+
+        final int countX = mCountX;
+        final int countY = mCountY;
+
+        for (int y = 0; y < countY - (spanY - 1); y++) {
+            inner:
+            for (int x = 0; x < countX - (spanX - 1); x++) {
+                // First, let's see if this thing fits anywhere
+                for (int i = 0; i < spanX; i++) {
+                    for (int j = 0; j < spanY; j++) {
+                        if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
+                            continue inner;
+                        }
+                    }
+                }
+
+                float distance = (float) Math.hypot(x - cellX, y - cellY);
+                int[] curDirection = mTmpPoint;
+                computeDirectionVector(x - cellX, y - cellY, curDirection);
+                // The direction score is just the dot product of the two candidate direction
+                // and that passed in.
+                int curDirectionScore = direction[0] * curDirection[0] +
+                        direction[1] * curDirection[1];
+                if (Float.compare(distance,  bestDistance) < 0 ||
+                        (Float.compare(distance, bestDistance) == 0
+                                && curDirectionScore > bestDirectionScore)) {
+                    bestDistance = distance;
+                    bestDirectionScore = curDirectionScore;
+                    bestXY[0] = x;
+                    bestXY[1] = y;
+                }
+            }
+        }
+
+        // Return -1, -1 if no suitable location found
+        if (bestDistance == Float.MAX_VALUE) {
+            bestXY[0] = -1;
+            bestXY[1] = -1;
+        }
+        return bestXY;
+    }
+
+    private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
+            int[] direction, ItemConfiguration currentState) {
+        CellAndSpan c = currentState.map.get(v);
+        boolean success = false;
+        mTmpOccupied.markCells(c, false);
+        mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
+
+        findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
+                mTmpOccupied.cells, null, mTempLocation);
+
+        if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+            c.cellX = mTempLocation[0];
+            c.cellY = mTempLocation[1];
+            success = true;
+        }
+        mTmpOccupied.markCells(c, true);
+        return success;
+    }
+
+    private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+            int[] direction, View dragView, ItemConfiguration currentState) {
+
+        ViewCluster cluster = new ViewCluster(views, currentState);
+        Rect clusterRect = cluster.getBoundingRect();
+        int whichEdge;
+        int pushDistance;
+        boolean fail = false;
+
+        // Determine the edge of the cluster that will be leading the push and how far
+        // the cluster must be shifted.
+        if (direction[0] < 0) {
+            whichEdge = ViewCluster.LEFT;
+            pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
+        } else if (direction[0] > 0) {
+            whichEdge = ViewCluster.RIGHT;
+            pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
+        } else if (direction[1] < 0) {
+            whichEdge = ViewCluster.TOP;
+            pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
+        } else {
+            whichEdge = ViewCluster.BOTTOM;
+            pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
+        }
+
+        // Break early for invalid push distance.
+        if (pushDistance <= 0) {
+            return false;
+        }
+
+        // Mark the occupied state as false for the group of views we want to move.
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            mTmpOccupied.markCells(c, false);
+        }
+
+        // We save the current configuration -- if we fail to find a solution we will revert
+        // to the initial state. The process of finding a solution modifies the configuration
+        // in place, hence the need for revert in the failure case.
+        currentState.save();
+
+        // The pushing algorithm is simplified by considering the views in the order in which
+        // they would be pushed by the cluster. For example, if the cluster is leading with its
+        // left edge, we consider sort the views by their right edge, from right to left.
+        cluster.sortConfigurationForEdgePush(whichEdge);
+
+        while (pushDistance > 0 && !fail) {
+            for (View v: currentState.sortedViews) {
+                // For each view that isn't in the cluster, we see if the leading edge of the
+                // cluster is contacting the edge of that view. If so, we add that view to the
+                // cluster.
+                if (!cluster.views.contains(v) && v != dragView) {
+                    if (cluster.isViewTouchingEdge(v, whichEdge)) {
+                        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams();
+                        if (!lp.canReorder) {
+                            // The push solution includes the all apps button, this is not viable.
+                            fail = true;
+                            break;
+                        }
+                        cluster.addView(v);
+                        CellAndSpan c = currentState.map.get(v);
+
+                        // Adding view to cluster, mark it as not occupied.
+                        mTmpOccupied.markCells(c, false);
+                    }
+                }
+            }
+            pushDistance--;
+
+            // The cluster has been completed, now we move the whole thing over in the appropriate
+            // direction.
+            cluster.shift(whichEdge, 1);
+        }
+
+        boolean foundSolution = false;
+        clusterRect = cluster.getBoundingRect();
+
+        // Due to the nature of the algorithm, the only check required to verify a valid solution
+        // is to ensure that completed shifted cluster lies completely within the cell layout.
+        if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
+                clusterRect.bottom <= mCountY) {
+            foundSolution = true;
+        } else {
+            currentState.restore();
+        }
+
+        // In either case, we set the occupied array as marked for the location of the views
+        for (View v: cluster.views) {
+            CellAndSpan c = currentState.map.get(v);
+            mTmpOccupied.markCells(c, true);
+        }
+
+        return foundSolution;
+    }
+
+    /**
+     * This helper class defines a cluster of views. It helps with defining complex edges
+     * of the cluster and determining how those edges interact with other views. The edges
+     * essentially define a fine-grained boundary around the cluster of views -- like a more
+     * precise version of a bounding box.
+     */
+    private class ViewCluster {
+        final static int LEFT = 1 << 0;
+        final static int TOP = 1 << 1;
+        final static int RIGHT = 1 << 2;
+        final static int BOTTOM = 1 << 3;
+
+        final ArrayList<View> views;
+        final ItemConfiguration config;
+        final Rect boundingRect = new Rect();
+
+        final int[] leftEdge = new int[mCountY];
+        final int[] rightEdge = new int[mCountY];
+        final int[] topEdge = new int[mCountX];
+        final int[] bottomEdge = new int[mCountX];
+        int dirtyEdges;
+        boolean boundingRectDirty;
+
+        @SuppressWarnings("unchecked")
+        public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
+            this.views = (ArrayList<View>) views.clone();
+            this.config = config;
+            resetEdges();
+        }
+
+        void resetEdges() {
+            for (int i = 0; i < mCountX; i++) {
+                topEdge[i] = -1;
+                bottomEdge[i] = -1;
+            }
+            for (int i = 0; i < mCountY; i++) {
+                leftEdge[i] = -1;
+                rightEdge[i] = -1;
+            }
+            dirtyEdges = LEFT | TOP | RIGHT | BOTTOM;
+            boundingRectDirty = true;
+        }
+
+        void computeEdge(int which) {
+            int count = views.size();
+            for (int i = 0; i < count; i++) {
+                CellAndSpan cs = config.map.get(views.get(i));
+                switch (which) {
+                    case LEFT:
+                        int left = cs.cellX;
+                        for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
+                            if (left < leftEdge[j] || leftEdge[j] < 0) {
+                                leftEdge[j] = left;
+                            }
+                        }
+                        break;
+                    case RIGHT:
+                        int right = cs.cellX + cs.spanX;
+                        for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
+                            if (right > rightEdge[j]) {
+                                rightEdge[j] = right;
+                            }
+                        }
+                        break;
+                    case TOP:
+                        int top = cs.cellY;
+                        for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
+                            if (top < topEdge[j] || topEdge[j] < 0) {
+                                topEdge[j] = top;
+                            }
+                        }
+                        break;
+                    case BOTTOM:
+                        int bottom = cs.cellY + cs.spanY;
+                        for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
+                            if (bottom > bottomEdge[j]) {
+                                bottomEdge[j] = bottom;
+                            }
+                        }
+                        break;
+                }
+            }
+        }
+
+        boolean isViewTouchingEdge(View v, int whichEdge) {
+            CellAndSpan cs = config.map.get(v);
+
+            if ((dirtyEdges & whichEdge) == whichEdge) {
+                computeEdge(whichEdge);
+                dirtyEdges &= ~whichEdge;
+            }
+
+            switch (whichEdge) {
+                case LEFT:
+                    for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
+                        if (leftEdge[i] == cs.cellX + cs.spanX) {
+                            return true;
+                        }
+                    }
+                    break;
+                case RIGHT:
+                    for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
+                        if (rightEdge[i] == cs.cellX) {
+                            return true;
+                        }
+                    }
+                    break;
+                case TOP:
+                    for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
+                        if (topEdge[i] == cs.cellY + cs.spanY) {
+                            return true;
+                        }
+                    }
+                    break;
+                case BOTTOM:
+                    for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
+                        if (bottomEdge[i] == cs.cellY) {
+                            return true;
+                        }
+                    }
+                    break;
+            }
+            return false;
+        }
+
+        void shift(int whichEdge, int delta) {
+            for (View v: views) {
+                CellAndSpan c = config.map.get(v);
+                switch (whichEdge) {
+                    case LEFT:
+                        c.cellX -= delta;
+                        break;
+                    case RIGHT:
+                        c.cellX += delta;
+                        break;
+                    case TOP:
+                        c.cellY -= delta;
+                        break;
+                    case BOTTOM:
+                    default:
+                        c.cellY += delta;
+                        break;
+                }
+            }
+            resetEdges();
+        }
+
+        public void addView(View v) {
+            views.add(v);
+            resetEdges();
+        }
+
+        public Rect getBoundingRect() {
+            if (boundingRectDirty) {
+                config.getBoundingRectForViews(views, boundingRect);
+            }
+            return boundingRect;
+        }
+
+        final PositionComparator comparator = new PositionComparator();
+        class PositionComparator implements Comparator<View> {
+            int whichEdge = 0;
+            public int compare(View left, View right) {
+                CellAndSpan l = config.map.get(left);
+                CellAndSpan r = config.map.get(right);
+                switch (whichEdge) {
+                    case LEFT:
+                        return (r.cellX + r.spanX) - (l.cellX + l.spanX);
+                    case RIGHT:
+                        return l.cellX - r.cellX;
+                    case TOP:
+                        return (r.cellY + r.spanY) - (l.cellY + l.spanY);
+                    case BOTTOM:
+                    default:
+                        return l.cellY - r.cellY;
+                }
+            }
+        }
+
+        public void sortConfigurationForEdgePush(int edge) {
+            comparator.whichEdge = edge;
+            Collections.sort(config.sortedViews, comparator);
+        }
+    }
+
+    // This method tries to find a reordering solution which satisfies the push mechanic by trying
+    // to push items in each of the cardinal directions, in an order based on the direction vector
+    // passed.
+    private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
+            int[] direction, View ignoreView, ItemConfiguration solution) {
+        if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
+            // If the direction vector has two non-zero components, we try pushing
+            // separately in each of the components.
+            int temp = direction[1];
+            direction[1] = 0;
+
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            direction[1] = temp;
+            temp = direction[0];
+            direction[0] = 0;
+
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Revert the direction
+            direction[0] = temp;
+
+            // Now we try pushing in each component of the opposite direction
+            direction[0] *= -1;
+            direction[1] *= -1;
+            temp = direction[1];
+            direction[1] = 0;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+
+            direction[1] = temp;
+            temp = direction[0];
+            direction[0] = 0;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // revert the direction
+            direction[0] = temp;
+            direction[0] *= -1;
+            direction[1] *= -1;
+
+        } else {
+            // If the direction vector has a single non-zero component, we push first in the
+            // direction of the vector
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Then we try the opposite direction
+            direction[0] *= -1;
+            direction[1] *= -1;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Switch the direction back
+            direction[0] *= -1;
+            direction[1] *= -1;
+
+            // If we have failed to find a push solution with the above, then we try
+            // to find a solution by pushing along the perpendicular axis.
+
+            // Swap the components
+            int temp = direction[1];
+            direction[1] = direction[0];
+            direction[0] = temp;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+
+            // Then we try the opposite direction
+            direction[0] *= -1;
+            direction[1] *= -1;
+            if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+                    ignoreView, solution)) {
+                return true;
+            }
+            // Switch the direction back
+            direction[0] *= -1;
+            direction[1] *= -1;
+
+            // Swap the components back
+            temp = direction[1];
+            direction[1] = direction[0];
+            direction[0] = temp;
+        }
+        return false;
+    }
+
+    /*
+     * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
+     * the provided point and the provided cell
+     */
+    private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
+        double angle = Math.atan(deltaY / deltaX);
+
+        result[0] = 0;
+        result[1] = 0;
+        if (Math.abs(Math.cos(angle)) > 0.5f) {
+            result[0] = (int) Math.signum(deltaX);
+        }
+        if (Math.abs(Math.sin(angle)) > 0.5f) {
+            result[1] = (int) Math.signum(deltaY);
+        }
+    }
+
+    /* This seems like it should be obvious and straight-forward, but when the direction vector
+    needs to match with the notion of the dragView pushing other views, we have to employ
+    a slightly more subtle notion of the direction vector. The question is what two points is
+    the vector between? The center of the dragView and its desired destination? Not quite, as
+    this doesn't necessarily coincide with the interaction of the dragView and items occupying
+    those cells. Instead we use some heuristics to often lock the vector to up, down, left
+    or right, which helps make pushing feel right.
+    */
+    private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
+            int spanY, View dragView, int[] resultDirection) {
+
+        //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
+        int[] targetDestination = new int[2];
+
+        findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
+        Rect dragRect = new Rect();
+        cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+        dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
+
+        Rect dropRegionRect = new Rect();
+        getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
+                dragView, dropRegionRect, mIntersectingViews);
+
+        int dropRegionSpanX = dropRegionRect.width();
+        int dropRegionSpanY = dropRegionRect.height();
+
+        cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+                dropRegionRect.height(), dropRegionRect);
+
+        int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
+        int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
+
+        if (dropRegionSpanX == mCountX || spanX == mCountX) {
+            deltaX = 0;
+        }
+        if (dropRegionSpanY == mCountY || spanY == mCountY) {
+            deltaY = 0;
+        }
+
+        if (deltaX == 0 && deltaY == 0) {
+            // No idea what to do, give a random direction.
+            resultDirection[0] = 1;
+            resultDirection[1] = 0;
+        } else {
+            computeDirectionVector(deltaX, deltaY, resultDirection);
+        }
+    }
+
+    private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+            int[] direction, View dragView, ItemConfiguration currentState) {
+        if (views.size() == 0) return true;
+
+        boolean success = false;
+        Rect boundingRect = new Rect();
+        // We construct a rect which represents the entire group of views passed in
+        currentState.getBoundingRectForViews(views, boundingRect);
+
+        // Mark the occupied state as false for the group of views we want to move.
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            mTmpOccupied.markCells(c, false);
+        }
+
+        GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height());
+        int top = boundingRect.top;
+        int left = boundingRect.left;
+        // We mark more precisely which parts of the bounding rect are truly occupied, allowing
+        // for interlocking.
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
+        }
+
+        mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
+
+        findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
+                boundingRect.height(), direction,
+                mTmpOccupied.cells, blockOccupied.cells, mTempLocation);
+
+        // If we successfully found a location by pushing the block of views, we commit it
+        if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+            int deltaX = mTempLocation[0] - boundingRect.left;
+            int deltaY = mTempLocation[1] - boundingRect.top;
+            for (View v: views) {
+                CellAndSpan c = currentState.map.get(v);
+                c.cellX += deltaX;
+                c.cellY += deltaY;
+            }
+            success = true;
+        }
+
+        // In either case, we set the occupied array as marked for the location of the views
+        for (View v: views) {
+            CellAndSpan c = currentState.map.get(v);
+            mTmpOccupied.markCells(c, true);
+        }
+        return success;
+    }
+
+    private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
+            View ignoreView, ItemConfiguration solution) {
+        // Return early if get invalid cell positions
+        if (cellX < 0 || cellY < 0) return false;
+
+        mIntersectingViews.clear();
+        mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
+
+        // Mark the desired location of the view currently being dragged.
+        if (ignoreView != null) {
+            CellAndSpan c = solution.map.get(ignoreView);
+            if (c != null) {
+                c.cellX = cellX;
+                c.cellY = cellY;
+            }
+        }
+        Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
+        Rect r1 = new Rect();
+        for (View child: solution.map.keySet()) {
+            if (child == ignoreView) continue;
+            CellAndSpan c = solution.map.get(child);
+            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
+            r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
+            if (Rect.intersects(r0, r1)) {
+                if (!lp.canReorder) {
+                    return false;
+                }
+                mIntersectingViews.add(child);
+            }
+        }
+
+        solution.intersectingViews = new ArrayList<>(mIntersectingViews);
+
+        // First we try to find a solution which respects the push mechanic. That is,
+        // we try to find a solution such that no displaced item travels through another item
+        // without also displacing that item.
+        if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+                solution)) {
+            return true;
+        }
+
+        // Next we try moving the views as a block, but without requiring the push mechanic.
+        if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+                solution)) {
+            return true;
+        }
+
+        // Ok, they couldn't move as a block, let's move them individually
+        for (View v : mIntersectingViews) {
+            if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY,
+            int spanX, int spanY, int[] direction, View dragView, boolean decX,
+            ItemConfiguration solution) {
+        // Copy the current state into the solution. This solution will be manipulated as necessary.
+        copyCurrentStateToSolution(solution, false);
+        // Copy the current occupied array into the temporary occupied array. This array will be
+        // manipulated as necessary to find a solution.
+        mOccupied.copyTo(mTmpOccupied);
+
+        // We find the nearest cell into which we would place the dragged item, assuming there's
+        // nothing in its way.
+        int result[] = new int[2];
         result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
 
-        if (resultSpan == null) {
-            resultSpan = new int[2];
-        }
+        boolean success;
+        // First we try the exact nearest position of the item being dragged,
+        // we will then want to try to move this around to other neighbouring positions
+        success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
+                solution);
 
-        // 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)
-                && mPreviousReorderDirection[0] != INVALID_DIRECTION) {
-            mDirectionVector[0] = mPreviousReorderDirection[0];
-            mDirectionVector[1] = mPreviousReorderDirection[1];
-            // We reset this vector after drop
-            if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
-                mPreviousReorderDirection[0] = INVALID_DIRECTION;
-                mPreviousReorderDirection[1] = INVALID_DIRECTION;
+        if (!success) {
+            // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
+            // x, then 1 in y etc.
+            if (spanX > minSpanX && (minSpanY == spanY || decX)) {
+                return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY,
+                        direction, dragView, false, solution);
+            } else if (spanY > minSpanY) {
+                return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1,
+                        direction, dragView, true, solution);
             }
+            solution.isSolution = false;
         } else {
-            getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
-            mPreviousReorderDirection[0] = mDirectionVector[0];
-            mPreviousReorderDirection[1] = mDirectionVector[1];
+            solution.isSolution = true;
+            solution.cellX = result[0];
+            solution.cellY = result[1];
+            solution.spanX = spanX;
+            solution.spanY = spanY;
         }
+        return solution;
+    }
+
+    private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
+        int childCount = mShortcutsAndWidgets.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = mShortcutsAndWidgets.getChildAt(i);
+            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
+            CellAndSpan c;
+            if (temp) {
+                c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan);
+            } else {
+                c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan);
+            }
+            solution.add(child, c);
+        }
+    }
+
+    /**
+     * Returns a "reorder" where we simply drop the item in the closest empty space, without moving
+     * any other item in the way.
+     *
+     * @param pixelX X coordinate in pixels in the screen
+     * @param pixelY Y coordinate in pixels in the screen
+     * @param spanX horizontal cell span
+     * @param spanY vertical cell span
+     * @return the configuration that represents the found reorder
+     */
+    public ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int spanX,
+            int spanY) {
+        int[] result = new int[2];
+        result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+        ItemConfiguration solution = new ItemConfiguration();
+        copyCurrentStateToSolution(solution, false);
+        solution.isSolution = result[0] != -1;
+        if (!solution.isSolution) {
+            return solution;
+        }
+        solution.cellX = result[0];
+        solution.cellY = result[1];
+        solution.spanX = spanX;
+        solution.spanY = spanY;
+        return solution;
+    }
+
+    /**
+     * When the user drags an Item in the workspace sometimes we need to move the items already in
+     * the workspace to make space for the new item, this function return a solution for that
+     * reorder.
+     *
+     * @param pixelX X coordinate in the screen of the dragView in pixels
+     * @param pixelY Y coordinate in the screen of the dragView in pixels
+     * @param minSpanX minimum horizontal span the item can be shrunk to
+     * @param minSpanY minimum vertical span the item can be shrunk to
+     * @param spanX occupied horizontal span
+     * @param spanY occupied vertical span
+     * @param dragView the view of the item being draged
+     * @return returns a solution for the given parameters, the solution contains all the icons and
+     *         the locations they should be in the given solution.
+     */
+    public ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
+            int spanX, int spanY, View dragView) {
+        getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
+
+        ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder(pixelX, pixelY, spanX,
+                spanY);
 
         // Find a solution involving pushing / displacing any items in the way
         ItemConfiguration swapSolution = findReorderSolution(pixelX, pixelY, minSpanX, minSpanY,
@@ -2504,73 +2502,104 @@
         ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX,
                 minSpanY, spanX, spanY, dragView, new ItemConfiguration());
 
-        ItemConfiguration finalSolution = null;
-
         // If the reorder solution requires resizing (shrinking) the item being dropped, we instead
         // favor a solution in which the item is not resized, but
         if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) {
-            finalSolution = swapSolution;
+            return swapSolution;
         } else if (noShuffleSolution.isSolution) {
-            finalSolution = noShuffleSolution;
+            return noShuffleSolution;
+        } else if (closestSpaceSolution.isSolution) {
+            return closestSpaceSolution;
+        }
+        return null;
+    }
+
+    int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
+            View dragView, int[] result, int[] resultSpan, int mode) {
+        if (resultSpan == null) {
+            resultSpan = new int[]{-1, -1};
+        }
+        if (result == null) {
+            result = new int[]{-1, -1};
         }
 
-        if (mode == MODE_SHOW_REORDER_HINT) {
-            if (finalSolution != null) {
-                beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
-                        ReorderPreviewAnimation.MODE_HINT);
-                result[0] = finalSolution.cellX;
-                result[1] = finalSolution.cellY;
-                resultSpan[0] = finalSolution.spanX;
-                resultSpan[1] = finalSolution.spanY;
-            } else {
-                result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+        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;
             }
-            return result;
         }
 
-        boolean foundSolution = true;
-        if (!DESTRUCTIVE_REORDER) {
-            setUseTempCoords(true);
-        }
-
-        if (finalSolution != null) {
+        if (finalSolution == null || !finalSolution.isSolution) {
+            result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+        } else {
             result[0] = finalSolution.cellX;
             result[1] = finalSolution.cellY;
             resultSpan[0] = finalSolution.spanX;
             resultSpan[1] = finalSolution.spanY;
+        }
+        performReorder(finalSolution, dragView, mode);
+        return result;
+    }
 
-            // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
-            // committing anything or animating anything as we just want to determine if a solution
-            // exists
-            if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
-                if (!DESTRUCTIVE_REORDER) {
-                    copySolutionToTempState(finalSolution, dragView);
-                }
-                setItemPlacementDirty(true);
-                animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP);
-
-                if (!DESTRUCTIVE_REORDER &&
-                        (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
-                    // Since the temp solution didn't update dragView, don't commit it either
-                    commitTempPlacement(dragView);
-                    completeAndClearReorderPreviewAnimations();
-                    setItemPlacementDirty(false);
-                } else {
-                    beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
-                            ReorderPreviewAnimation.MODE_PREVIEW);
-                }
+    /**
+     * Animates and submits in the DB the given ItemConfiguration depending of the mode.
+     *
+     * @param solution represents widgets on the screen which the Workspace will animate to and
+     * would be submitted to the database.
+     * @param dragView view which is being dragged over the workspace that trigger the reorder
+     * @param mode depending on the mode different animations would be played and depending on the
+     *             mode the solution would be submitted or not the database.
+     *             The possible modes are {@link MODE_SHOW_REORDER_HINT}, {@link MODE_DRAG_OVER},
+     *             {@link MODE_ON_DROP}, {@link MODE_ON_DROP_EXTERNAL}, {@link  MODE_ACCEPT_DROP}
+     *             defined in {@link CellLayout}.
+     */
+    void performReorder(ItemConfiguration solution, View dragView, int mode) {
+        if (mode == MODE_SHOW_REORDER_HINT) {
+            beginOrAdjustReorderPreviewAnimations(solution, dragView,
+                    ReorderPreviewAnimation.MODE_HINT);
+            return;
+        }
+        // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
+        // committing anything or animating anything as we just want to determine if a solution
+        // exists
+        if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+            if (!DESTRUCTIVE_REORDER) {
+                setUseTempCoords(true);
             }
-        } else {
-            foundSolution = false;
-            result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+
+            if (!DESTRUCTIVE_REORDER) {
+                copySolutionToTempState(solution, dragView);
+            }
+            setItemPlacementDirty(true);
+            animateItemsToSolution(solution, dragView, mode == MODE_ON_DROP);
+
+            if (!DESTRUCTIVE_REORDER
+                    && (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
+                // Since the temp solution didn't update dragView, don't commit it either
+                commitTempPlacement(dragView);
+                completeAndClearReorderPreviewAnimations();
+                setItemPlacementDirty(false);
+            } else {
+                beginOrAdjustReorderPreviewAnimations(solution, dragView,
+                        ReorderPreviewAnimation.MODE_PREVIEW);
+            }
         }
 
-        if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) {
+        if (mode == MODE_ON_DROP && !DESTRUCTIVE_REORDER) {
             setUseTempCoords(false);
         }
 
         mShortcutsAndWidgets.requestLayout();
-        return result;
     }
 
     void setItemPlacementDirty(boolean dirty) {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index edd809c..c91e3eb 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -25,12 +25,14 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
+import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -61,6 +63,7 @@
 
     private static final int DEFAULT_DOT_SIZE = 100;
     private static final float ALL_APPS_TABLET_MAX_ROWS = 5.5f;
+    private static final float MIN_FOLDER_TEXT_SIZE_SP = 16f;
 
     public static final PointF DEFAULT_SCALE = new PointF(1.0f, 1.0f);
     public static final ViewScaleProvider DEFAULT_PROVIDER = itemInfo -> DEFAULT_SCALE;
@@ -147,11 +150,12 @@
     // Folder
     public float folderLabelTextScale;
     public int folderLabelTextSizePx;
+    public int folderFooterHeightPx;
     public int folderIconSizePx;
     public int folderIconOffsetYPx;
 
     // Folder content
-    public Point folderCellLayoutBorderSpacePx;
+    public int folderCellLayoutBorderSpacePx;
     public int folderContentPaddingLeftRight;
     public int folderContentPaddingTop;
 
@@ -312,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);
@@ -348,17 +356,34 @@
         }
 
         folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
-        folderContentPaddingLeftRight =
-                res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
-        folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top);
+
+        if (inv.folderStyle != INVALID_RESOURCE_HANDLE) {
+            TypedArray folderStyle = context.obtainStyledAttributes(inv.folderStyle,
+                    R.styleable.FolderDisplayStyle);
+            // These are re-set in #updateFolderCellSize if the grid is not scalable
+            folderCellHeightPx = folderStyle.getDimensionPixelSize(
+                    R.styleable.FolderDisplayStyle_folderCellHeight, 0);
+            folderCellWidthPx = folderStyle.getDimensionPixelSize(
+                    R.styleable.FolderDisplayStyle_folderCellWidth, 0);
+
+            folderContentPaddingTop = folderStyle.getDimensionPixelSize(
+                    R.styleable.FolderDisplayStyle_folderTopPadding, 0);
+            folderCellLayoutBorderSpacePx = folderStyle.getDimensionPixelSize(
+                    R.styleable.FolderDisplayStyle_folderBorderSpace, 0);
+            folderFooterHeightPx = folderStyle.getDimensionPixelSize(
+                    R.styleable.FolderDisplayStyle_folderFooterHeight, 0);
+            folderStyle.recycle();
+        } else {
+            folderCellLayoutBorderSpacePx = 0;
+            folderFooterHeightPx = 0;
+            folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_top_padding_default);
+        }
 
         cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv);
+        cellLayoutBorderSpaceOriginalPx = new Point(cellLayoutBorderSpacePx);
         allAppsBorderSpacePx = new Point(
                 pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].x, mMetrics),
                 pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].y, mMetrics));
-        cellLayoutBorderSpaceOriginalPx = new Point(cellLayoutBorderSpacePx);
-        folderCellLayoutBorderSpacePx = new Point(pxFromDp(inv.folderBorderSpaces.x, mMetrics),
-                pxFromDp(inv.folderBorderSpaces.y, mMetrics));
 
         workspacePageIndicatorHeight = res.getDimensionPixelSize(
                 R.dimen.workspace_page_indicator_height);
@@ -927,22 +952,20 @@
     private void updateAvailableFolderCellDimensions(Resources res) {
         updateFolderCellSize(1f, res);
 
-        final int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_height);
-
         // Don't let the folder get too close to the edges of the screen.
         int folderMargin = edgeMarginPx * 2;
         Point totalWorkspacePadding = getTotalWorkspacePadding();
 
         // Check if the icons fit within the available height.
         float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
-                + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx.y);
-        int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
+                + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx);
+        int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderFooterHeightPx
                 - folderMargin - folderContentPaddingTop;
         float scaleY = contentMaxHeight / contentUsedHeight;
 
         // Check if the icons fit within the available width.
         float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
-                + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx.x);
+                + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx);
         int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin
                 - folderContentPaddingLeftRight * 2;
         float scaleX = contentMaxWidth / contentUsedWidth;
@@ -960,19 +983,18 @@
         folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale));
         folderChildTextSizePx =
                 pxFromSp(inv.iconTextSize[INDEX_DEFAULT], mMetrics, scale);
-        folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale);
+        folderLabelTextSizePx = Math.max(pxFromSp(MIN_FOLDER_TEXT_SIZE_SP, mMetrics),
+                (int) (folderChildTextSizePx * folderLabelTextScale));
 
         int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
 
         if (isScalableGrid) {
-            folderCellWidthPx = pxFromDp(inv.folderCellSize.x, mMetrics, scale);
-            folderCellHeightPx = pxFromDp(inv.folderCellSize.y, mMetrics, scale);
+            if (inv.folderStyle == INVALID_RESOURCE_HANDLE) {
+                folderCellWidthPx = pxFromDp(getCellSize().x, mMetrics, scale);
+                folderCellHeightPx = pxFromDp(getCellSize().y, mMetrics, scale);
+            }
 
-            folderCellLayoutBorderSpacePx = new Point(
-                    pxFromDp(inv.folderBorderSpaces.x, mMetrics, scale),
-                    pxFromDp(inv.folderBorderSpaces.y, mMetrics, scale));
-            folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx.x;
-            folderContentPaddingTop = pxFromDp(inv.folderTopPadding, mMetrics, scale);
+            folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx;
         } else {
             int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding)
                     * scale);
@@ -981,6 +1003,10 @@
 
             folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX;
             folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight;
+            folderContentPaddingLeftRight =
+                    res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
+            folderFooterHeightPx =
+                    res.getDimensionPixelSize(R.dimen.folder_footer_height_default);
         }
 
         folderChildDrawablePaddingPx = Math.max(0,
@@ -1464,13 +1490,12 @@
         writer.println(prefix + pxToDpStr("folderChildTextSizePx", folderChildTextSizePx));
         writer.println(prefix + pxToDpStr("folderChildDrawablePaddingPx",
                 folderChildDrawablePaddingPx));
-        writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx Horizontal",
-                folderCellLayoutBorderSpacePx.x));
-        writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx Vertical",
-                folderCellLayoutBorderSpacePx.y));
+        writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx",
+                folderCellLayoutBorderSpacePx));
         writer.println(prefix + pxToDpStr("folderContentPaddingLeftRight",
                 folderContentPaddingLeftRight));
         writer.println(prefix + pxToDpStr("folderTopPadding", folderContentPaddingTop));
+        writer.println(prefix + pxToDpStr("folderFooterHeight", folderFooterHeightPx));
 
         writer.println(prefix + pxToDpStr("bottomSheetTopPadding", bottomSheetTopPadding));
         writer.println(prefix + "\tbottomSheetOpenDuration: " + bottomSheetOpenDuration);
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index 98ecf3a..5225731 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_DEFAULT;
 import static com.android.launcher3.anim.AlphaUpdateListener.updateVisibility;
+import static com.android.launcher3.config.FeatureFlags.HOME_GARDENING_WORKSPACE_BUTTONS;
 
 import android.animation.TimeInterpolator;
 import android.content.Context;
@@ -118,7 +119,13 @@
             lp.rightMargin = (grid.widthPx - lp.width) / 2;
         }
         lp.height = grid.dropTargetBarSizePx;
-        lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+        // TODO: Add tablet support for DropTargetBar when HOME_GARDENING_WORKSPACE_BUTTONS flag
+        //  is on
+        if (HOME_GARDENING_WORKSPACE_BUTTONS.get()) {
+            lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+        } else {
+            lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+        }
 
         DeviceProfile dp = mLauncher.getDeviceProfile();
         int horizontalPadding = dp.dropTargetHorizontalPaddingPx;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index a8d371e..0c6f340 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME;
+import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
@@ -43,8 +44,10 @@
 import android.util.Xml;
 import android.view.Display;
 
+import androidx.annotation.DimenRes;
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.content.res.ResourcesCompat;
 
@@ -64,6 +67,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -127,11 +132,9 @@
     public PointF[] minCellSize;
 
     public PointF[] borderSpaces;
-    public int inlineNavButtonsEndSpacing;
+    public @DimenRes int inlineNavButtonsEndSpacing;
 
-    public PointF folderBorderSpaces;
-    public PointF folderCellSize;
-    public float folderTopPadding;
+    public @StyleRes int folderStyle;
 
     public float[] horizontalMargin;
 
@@ -331,8 +334,11 @@
         dbFile = closestProfile.dbFile;
         defaultLayoutId = closestProfile.defaultLayoutId;
         demoModeLayoutId = closestProfile.demoModeLayoutId;
+
         numFolderRows = closestProfile.numFolderRows;
         numFolderColumns = closestProfile.numFolderColumns;
+        folderStyle = closestProfile.folderStyle;
+
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         this.deviceType = deviceType;
@@ -355,10 +361,6 @@
 
         borderSpaces = displayOption.borderSpaces;
 
-        folderBorderSpaces = displayOption.folderBorderSpaces;
-        folderCellSize = displayOption.folderCellSize;
-        folderTopPadding = displayOption.folderTopPadding;
-
         horizontalMargin = displayOption.horizontalMargin;
 
         numShownHotseatIcons = closestProfile.numHotseatIcons;
@@ -641,6 +643,18 @@
         float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density;
         int rotation = WindowManagerProxy.INSTANCE.get(context).getRotation(context);
 
+        if (Utilities.IS_DEBUG_DEVICE) {
+            StringWriter stringWriter = new StringWriter();
+            PrintWriter printWriter = new PrintWriter(stringWriter);
+            DisplayController.INSTANCE.get(context).dump(printWriter);
+            printWriter.flush();
+            Log.d("b/253338238", "getDeviceProfile -"
+                            + "\nconfig: " + config
+                            + "\ndisplayMetrics: " + res.getDisplayMetrics()
+                            + "\nrotation: " + rotation
+                            + "\n" + stringWriter,
+                    new Exception());
+        }
         return getBestMatch(screenWidth, screenHeight, rotation);
     }
 
@@ -735,6 +749,7 @@
 
         private final int numFolderRows;
         private final int numFolderColumns;
+        private final @StyleRes int folderStyle;
 
         private final int numAllAppsColumns;
         private final int numDatabaseAllAppsColumns;
@@ -745,7 +760,7 @@
 
         private final boolean[] inlineQsb = new boolean[COUNT_SIZES];
 
-        private int inlineNavButtonsEndSpacing;
+        private @DimenRes int inlineNavButtonsEndSpacing;
         private final String dbFile;
 
         private final int defaultLayoutId;
@@ -797,11 +812,15 @@
             inlineNavButtonsEndSpacing =
                     a.getResourceId(R.styleable.GridDisplayOption_inlineNavButtonsEndSpacing,
                     R.dimen.taskbar_button_margin_default);
+
             numFolderRows = a.getInt(
                     R.styleable.GridDisplayOption_numFolderRows, numRows);
             numFolderColumns = a.getInt(
                     R.styleable.GridDisplayOption_numFolderColumns, numColumns);
 
+            folderStyle = a.getResourceId(R.styleable.GridDisplayOption_folderStyle,
+                    INVALID_RESOURCE_HANDLE);
+
             isScalable = a.getBoolean(
                     R.styleable.GridDisplayOption_isScalable, false);
             devicePaddingId = a.getResourceId(
@@ -846,10 +865,6 @@
 
         private final PointF[] minCellSize = new PointF[COUNT_SIZES];
 
-        private final PointF folderCellSize;
-        private final PointF folderBorderSpaces;
-        private float folderTopPadding;
-
         private final PointF[] borderSpaces = new PointF[COUNT_SIZES];
         private final float[] horizontalMargin = new float[COUNT_SIZES];
         private final float[] hotseatBarBottomSpace = new float[COUNT_SIZES];
@@ -932,21 +947,6 @@
                     borderSpaceTwoPanelLandscape);
             borderSpaces[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y);
 
-            x = a.getFloat(R.styleable.ProfileDisplayOption_folderCellWidth,
-                    minCellSize[INDEX_DEFAULT].x);
-            y = a.getFloat(R.styleable.ProfileDisplayOption_folderCellHeight,
-                    minCellSize[INDEX_DEFAULT].y);
-            folderCellSize = new PointF(x, y);
-
-            float folderBorderSpace = a.getFloat(R.styleable.ProfileDisplayOption_folderBorderSpace,
-                    borderSpace);
-
-            x = y = folderBorderSpace;
-            folderBorderSpaces = new PointF(x, y);
-
-            folderTopPadding = a.getFloat(R.styleable.ProfileDisplayOption_folderTopPadding,
-                    folderBorderSpaces.y);
-
             x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellWidth,
                     minCellSize[INDEX_DEFAULT].x);
             y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellHeight,
@@ -1119,9 +1119,6 @@
                 allAppsIconTextSizes[i] = 0;
                 allAppsBorderSpaces[i] = new PointF();
             }
-            folderBorderSpaces = new PointF();
-            folderCellSize = new PointF();
-            folderTopPadding = 0f;
         }
 
         private DisplayOption multiply(float w) {
@@ -1142,11 +1139,6 @@
                 allAppsBorderSpaces[i].x *= w;
                 allAppsBorderSpaces[i].y *= w;
             }
-            folderBorderSpaces.x *= w;
-            folderBorderSpaces.y *= w;
-            folderCellSize.x *= w;
-            folderCellSize.y *= w;
-            folderTopPadding *= w;
 
             return this;
         }
@@ -1169,11 +1161,6 @@
                 allAppsBorderSpaces[i].x += p.allAppsBorderSpaces[i].x;
                 allAppsBorderSpaces[i].y += p.allAppsBorderSpaces[i].y;
             }
-            folderBorderSpaces.x += p.folderBorderSpaces.x;
-            folderBorderSpaces.y += p.folderBorderSpaces.y;
-            folderCellSize.x += p.folderCellSize.x;
-            folderCellSize.y += p.folderCellSize.y;
-            folderTopPadding += p.folderTopPadding;
 
             return this;
         }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7269d14..ab7f622 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -44,6 +44,7 @@
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
 import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
 import static com.android.launcher3.config.FeatureFlags.SHOW_DELIGHTFUL_PAGINATION;
+import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
 import static com.android.launcher3.logging.StatsLogManager.EventEnum;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
@@ -52,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;
@@ -142,6 +145,7 @@
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.LauncherDragController;
+import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BitmapRenderer;
@@ -181,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;
@@ -220,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;
@@ -240,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<>();
@@ -480,6 +482,9 @@
 
         mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
 
+        // TODO: move the SearchConfig to SearchState when new LauncherState is created.
+        mBaseSearchConfig = new BaseSearchConfig();
+
         mAppWidgetManager = new WidgetManagerHelper(this);
         mAppWidgetHolder = createAppWidgetHolder();
         mAppWidgetHolder.startListening();
@@ -546,9 +551,6 @@
             getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
         }
         setTitle(R.string.home_screen);
-
-        // TODO: move the SearchConfig to SearchState when new LauncherState is created.
-        mBaseSearchConfig = new BaseSearchConfig();
     }
 
     protected LauncherOverlayManager getDefaultOverlay() {
@@ -694,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;
@@ -1191,7 +1185,6 @@
             mOverlayManager.onActivityResumed(this);
         }
 
-        AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
         DragView.removeAllViews(this);
         TraceHelper.INSTANCE.endSection(traceToken);
     }
@@ -1213,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.
@@ -1300,7 +1281,7 @@
 
     @Override
     public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
-        if (SHOW_DELIGHTFUL_PAGINATION.get()
+        if ((SHOW_DOT_PAGINATION.get() || SHOW_DELIGHTFUL_PAGINATION.get())
                 && WorkspacePageIndicator.class.getName().equals(name)) {
             return LayoutInflater.from(context).inflate(R.layout.page_indicator_dots,
                     (ViewGroup) parent, false);
@@ -1331,7 +1312,7 @@
         BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
                 .inflate(R.layout.app_icon, parent, false);
         favorite.applyFromWorkspaceItem(info);
-        favorite.setOnClickListener(ItemClickHandler.INSTANCE);
+        favorite.setOnClickListener(getItemOnClickListener());
         favorite.setOnFocusChangeListener(mFocusHandler);
         return favorite;
     }
@@ -1617,7 +1598,6 @@
                 && AbstractFloatingView.getTopOpenView(this) == null;
         boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
         boolean internalStateHandled = ACTIVITY_TRACKER.handleNewIntent(this);
-        hideKeyboard();
 
         if (isActionMain) {
             if (!internalStateHandled) {
@@ -1704,6 +1684,10 @@
             outState.remove(RUNTIME_STATE_WIDGET_PANEL);
         }
 
+        // We close any open folders and shortcut containers that are not safe for rebind,
+        // and we need to make sure this state is reflected.
+        AbstractFloatingView.closeAllOpenViewsExcept(
+                this, isStarted() && !isForceInvisible(), TYPE_REBIND_SAFE);
         finishAutoCancelActionMode();
 
         if (mPendingRequestArgs != null) {
@@ -2814,16 +2798,30 @@
             }
 
             return v;
-        } else {
-            List<ViewGroup> containers = new ArrayList<>(mWorkspace.getPanelCount() + 1);
-            containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets());
-            mWorkspace.forEachVisiblePage(page
-                    -> containers.add(((CellLayout) page).getShortcutsAndWidgets()));
-
-            // Order: Preferred item by itself or in folder, then by matching package/user
-            return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem),
-                    packageAndUserAndApp, forFolderMatch(packageAndUserAndApp));
         }
+
+        // Look for the item inside the folder at the current page
+        Folder folder = Folder.getOpen(this);
+        if (folder != null) {
+            View v = getFirstMatch(Collections.singletonList(
+                    folder.getContent().getCurrentCellLayout().getShortcutsAndWidgets()),
+                    preferredItem,
+                    packageAndUserAndApp);
+            if (v == null) {
+                folder.close(isStarted() && !isForceInvisible());
+            } else {
+                return v;
+            }
+        }
+
+        List<ViewGroup> containers = new ArrayList<>(mWorkspace.getPanelCount() + 1);
+        containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets());
+        mWorkspace.forEachVisiblePage(page
+                -> containers.add(((CellLayout) page).getShortcutsAndWidgets()));
+
+        // Order: Preferred item by itself or in folder, then by matching package/user
+        return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem),
+                packageAndUserAndApp, forFolderMatch(packageAndUserAndApp));
     }
 
     /**
@@ -2876,7 +2874,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/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 8b342ea..7a74d7e 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -152,7 +152,7 @@
             // No need to add padding when cell layout border spacing is present.
             boolean noPaddingX =
                     (dp.cellLayoutBorderSpacePx.x > 0 && mContainerType == WORKSPACE)
-                            || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER)
+                            || (dp.folderCellLayoutBorderSpacePx > 0 && mContainerType == FOLDER)
                             || (dp.hotseatBorderSpace > 0 && mContainerType == HOTSEAT);
             int cellPaddingX = noPaddingX
                     ? 0
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index f834cce..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,20 +2384,20 @@
         final View child = (mDragInfo == null) ? null : mDragInfo.cell;
         int reorderX = mTargetCell[0];
         int reorderY = mTargetCell[1];
+        if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
+                && (mLastReorderX != reorderX || mLastReorderY != reorderY)
+                && targetCellDistance < mDragTargetLayout.getReorderRadius(mTargetCell, item.spanX,
+                item.spanY)) {
+            mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
+                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
+                    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()
-                && (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);
-
+                && !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,
@@ -3467,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/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index fa2c6e9..9930abe 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -141,6 +141,9 @@
     private final Launcher mLauncher;
     private boolean mIsVerticalLayout;
 
+    // Whether this class should take care of closing the keyboard.
+    private boolean mShouldControlKeyboard;
+
     // Animation in this class is controlled by a single variable {@link mProgress}.
     // Visually, it represents top y coordinate of the all apps container if multiplied with
     // {@link mShiftRange}.
@@ -238,7 +241,7 @@
             StateAnimationConfig config, PendingAnimation builder) {
         if (mLauncher.isInState(ALL_APPS) && !ALL_APPS.equals(toState)) {
             // For atomic animations, we close the keyboard immediately.
-            if (!config.userControlled && !mLauncher.getSearchConfig().isKeyboardSyncEnabled()) {
+            if (!config.userControlled && mShouldControlKeyboard) {
                 mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
             }
 
@@ -252,7 +255,7 @@
                 // the keyboard open and then changes their mind and swipes back up, we want the
                 // keyboard to remain open. However an onCancel signal is sent to the listeners
                 // (success = false), so we need to check for that.
-                if (config.userControlled && success) {
+                if (config.userControlled && success && mShouldControlKeyboard) {
                     mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
                 }
             });
@@ -317,6 +320,8 @@
         mAppsViewAlpha.setUpdateVisibility(true);
         mAppsViewTranslationY = new MultiPropertyFactory<>(
                 mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
+
+        mShouldControlKeyboard = !mLauncher.getSearchConfig().isKeyboardSyncEnabled();
     }
 
     /**
@@ -333,7 +338,9 @@
     private void onProgressAnimationEnd() {
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.reset(false /* animate */);
-            mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
+            if (mShouldControlKeyboard) {
+                mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
index 2ff992e..da86d98 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
@@ -107,8 +107,7 @@
             new RecyclerView.OnScrollListener() {
                 @Override
                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
-                    updateHeaderScroll(
-                            ((AllAppsRecyclerView) recyclerView).computeVerticalScrollOffset());
+                    updateHeaderScroll(recyclerView.computeVerticalScrollOffset());
                 }
             };
 
@@ -192,7 +191,6 @@
                 reset(true);
             }
         }
-
     }
 
     @Override
@@ -815,6 +813,10 @@
         }
     }
 
+    protected void onInitializeRecyclerView(RecyclerView rv) {
+        rv.addOnScrollListener(mScrollListener);
+    }
+
     /** Holds a {@link BaseAllAppsAdapter} and related fields. */
     public class AdapterHolder {
         public static final int MAIN = 0;
@@ -851,7 +853,7 @@
             mRecyclerView.setHasFixedSize(true);
             // No animations will occur when changes occur to the items in this RecyclerView.
             mRecyclerView.setItemAnimator(null);
-            mRecyclerView.addOnScrollListener(mScrollListener);
+            onInitializeRecyclerView(mRecyclerView);
             FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView);
             mRecyclerView.addItemDecoration(focusedItemDecorator);
             mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
diff --git a/src/com/android/launcher3/allapps/BaseSearchConfig.java b/src/com/android/launcher3/allapps/BaseSearchConfig.java
index 9f47e8d..3900954 100644
--- a/src/com/android/launcher3/allapps/BaseSearchConfig.java
+++ b/src/com/android/launcher3/allapps/BaseSearchConfig.java
@@ -25,4 +25,11 @@
     public boolean isKeyboardSyncEnabled() {
         return false;
     }
+
+    /**
+     * Returns whether IME is enabled on swipe up.
+     */
+    public boolean isImeEnabledOnSwipeUp() {
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index 20f5e74..92e29bb 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -16,19 +16,50 @@
 package com.android.launcher3.allapps;
 
 import android.content.Context;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.WindowInsets;
 
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 
 /**
  * AllAppsContainerView with launcher specific callbacks
  */
 public class LauncherAllAppsContainerView extends ActivityAllAppsContainerView<Launcher> {
 
+    private final RecyclerView.OnScrollListener mActivityScrollListener =
+            new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                    int scrolledOffset = recyclerView.computeVerticalScrollOffset();
+                    ExtendedEditText input = mSearchUiManager.getEditText();
+                    // Scroll up and scroll to top
+                    if (dy < 0 && scrolledOffset == 0 && input != null) {
+                        boolean isImeEnabledOnSwipeUp = Launcher.getLauncher(mActivityContext)
+                                .getSearchConfig().isImeEnabledOnSwipeUp();
+                        if (isImeEnabledOnSwipeUp || !TextUtils.isEmpty(input.getText())) {
+                            input.showKeyboard();
+                        }
+                    }
+                }
+            };
+
+    @Override
+    protected void onInitializeRecyclerView(RecyclerView rv) {
+        super.onInitializeRecyclerView(rv);
+        if (FeatureFlags.SCROLL_TOP_TO_RESET.get()) {
+            rv.addOnScrollListener(mActivityScrollListener);
+        }
+    }
+
     public LauncherAllAppsContainerView(Context context) {
         this(context, null);
     }
diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java
index b0ed2d2..6ba58b6 100644
--- a/src/com/android/launcher3/anim/PropertySetter.java
+++ b/src/com/android/launcher3/anim/PropertySetter.java
@@ -38,6 +38,7 @@
         public void add(Animator animatorSet) {
             animatorSet.setDuration(0);
             animatorSet.start();
+            animatorSet.end();
         }
     };
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 037a77e..4287779 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -96,6 +96,10 @@
             "ENABLE_QUICK_LAUNCH_V2", false, "Use quick launch v2 "
             + "behavior. Quick search and quick launch v1 would be unavailable if this is enabled");
 
+    public static final BooleanFlag GBOARD_UPDATE_ENTER_KEY = new DeviceFlag(
+            "GBOARD_UPDATE_ENTER_KEY", false, "Update gBoard enter key "
+            + "icon dynamically based on top search content for Quick Launch V2");
+
     public static final BooleanFlag ENABLE_HIDE_HEADER = new DeviceFlag("ENABLE_HIDE_HEADER",
             true, "Hide header on keyboard before typing in all apps");
 
@@ -206,10 +210,6 @@
     public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = new DeviceFlag(
             "ENABLE_ENFORCED_ROUNDED_CORNERS", true, "Enforce rounded corners on all App Widgets");
 
-    public static final BooleanFlag ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER = new DeviceFlag(
-            "ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER", true,
-            "Enables a local filter for recommended widgets.");
-
     public static final BooleanFlag NOTIFY_CRASHES = getDebugFlag("NOTIFY_CRASHES", false,
             "Sends a notification whenever launcher encounters an uncaught exception.");
 
@@ -253,6 +253,10 @@
             "ENABLE_SPLIT_FROM_WORKSPACE", true,
             "Enable initiating split screen from workspace.");
 
+    public static final BooleanFlag ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS =
+            getDebugFlag("ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", false,
+                    "Enable splitting from fullscreen app with keyboard shortcuts");
+
     public static final BooleanFlag ENABLE_NEW_MIGRATION_LOGIC = getDebugFlag(
             "ENABLE_NEW_MIGRATION_LOGIC", true,
             "Enable the new grid migration logic, keeping pages when src < dest");
@@ -290,6 +294,10 @@
     public static final BooleanFlag ENABLE_WIDGET_PICKER_DEPTH = new DeviceFlag(
             "ENABLE_WIDGET_PICKER_DEPTH", true, "Enable changing depth in widget picker.");
 
+    public static final BooleanFlag SCROLL_TOP_TO_RESET = new DeviceFlag(
+            "SCROLL_TOP_TO_RESET", false, "Bring up IME and focus on "
+            + "input when scroll to top if 'Always show keyboard' is enabled or in prefix state");
+
     public static final BooleanFlag SHOW_DELIGHTFUL_PAGINATION = getDebugFlag(
             "SHOW_DELIGHTFUL_PAGINATION", false,
             "Enable showing the new 'delightful pagination' which is a brand"
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/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7e14912..99822da 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -292,7 +292,7 @@
         mFolderName.forceDisableSuggestions(true);
 
         mFooter = findViewById(R.id.folder_footer);
-        mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
+        mFooterHeight = dp.folderFooterHeightPx;
 
         if (Utilities.ATLEAST_R) {
             mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
@@ -1167,14 +1167,6 @@
         mContent.setFixedSize(contentWidth, contentHeight);
         mContent.measure(contentAreaWidthSpec, contentAreaHeightSpec);
 
-        if (mContent.getChildCount() > 0) {
-            int cellIconGap = (mContent.getPageAt(0).getCellWidth()
-                    - mActivityContext.getDeviceProfile().iconSizePx) / 2;
-            mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
-                    mFooter.getPaddingTop(),
-                    mContent.getPaddingRight() + cellIconGap,
-                    mFooter.getPaddingBottom());
-        }
         mFooter.measure(contentAreaWidthSpec,
                 MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
 
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index d5ef9df..4829c42 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -234,9 +234,9 @@
                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
 
         // Create reveal animator for the folder content (capture the top 4 icons 2x2)
-        int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x
+        int width = mDeviceProfile.folderCellLayoutBorderSpacePx
                 + mDeviceProfile.folderCellWidthPx * 2;
-        int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y
+        int height = mDeviceProfile.folderCellLayoutBorderSpacePx
                 + mDeviceProfile.folderCellHeightPx * 2;
         int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
         int left = mContent.getPaddingLeft() + page * lp.width;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 05f53fd..fcc5d86 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -617,6 +617,11 @@
         @UiEvent(doc = "Number of apps in A-Z list (personal and work profile)")
         LAUNCHER_ALLAPPS_COUNT(1225),
 
+        @UiEvent(doc = "User has invoked split to right half with a keyboard shortcut.")
+        LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM(1232),
+
+        @UiEvent(doc = "User has invoked split to left half with a keyboard shortcut.")
+        LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP(1233)
         ;
 
         // ADD MORE
@@ -781,7 +786,6 @@
             public int getId() {
                 return mId;
             }
-
         }
 
         /**
@@ -814,6 +818,13 @@
         }
 
         /**
+         * Sets sub event type.
+         */
+        default StatsLatencyLogger withSubEventType(int type) {
+            return this;
+        }
+
+        /**
          * Sets packageId of log message.
          */
         default StatsLatencyLogger withPackageId(int packageId) {
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 3770de8..e9b6606 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.pageindicators;
 
 import static com.android.launcher3.config.FeatureFlags.SHOW_DELIGHTFUL_PAGINATION;
+import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -182,7 +183,9 @@
 
     @Override
     public void setScroll(int currentScroll, int totalScroll) {
-        animatePaginationToAlpha(VISIBLE_ALPHA);
+        if (SHOW_DELIGHTFUL_PAGINATION.get() || SHOW_DOT_PAGINATION.get()) {
+            animatePaginationToAlpha(VISIBLE_ALPHA);
+        }
 
         if (mNumPages <= 1) {
             mCurrentScroll = 0;
@@ -193,9 +196,9 @@
             currentScroll = totalScroll - currentScroll;
         }
 
+        mTotalScroll = totalScroll;
         if (SHOW_DELIGHTFUL_PAGINATION.get()) {
             mCurrentScroll = currentScroll;
-            mTotalScroll = totalScroll;
             invalidate();
 
             if (mShouldAutoHide
@@ -214,9 +217,15 @@
         if (currentScroll < pageToLeftScroll + scrollThreshold) {
             // scroll is within the left page's threshold
             animateToPosition(pageToLeft);
+            if (SHOW_DOT_PAGINATION.get()) {
+                hideAfterDelay();
+            }
         } else if (currentScroll > pageToRightScroll - scrollThreshold) {
             // scroll is far enough from left page to go to the right page
             animateToPosition(pageToLeft + 1);
+            if (SHOW_DOT_PAGINATION.get()) {
+                hideAfterDelay();
+            }
         } else {
             // scroll is between left and right page
             animateToPosition(pageToLeft + SHIFT_PER_ANIMATION);
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 49d27b7..70956a3 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,6 +18,7 @@
 
 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
 
+import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 
 import android.content.Intent;
@@ -207,7 +208,11 @@
             PreferenceScreen screen = getPreferenceScreen();
             for (int i = screen.getPreferenceCount() - 1; i >= 0; i--) {
                 Preference preference = screen.getPreference(i);
-                if (!initPreference(preference)) {
+                if (initPreference(preference)) {
+                    if (IS_STUDIO_BUILD && preference == mDeveloperOptionPref) {
+                        preference.setOrder(0);
+                    }
+                } else {
                     screen.removePreference(preference);
                 }
             }
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 2a890c3..520f33c 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -231,4 +231,10 @@
      * etc.)
      */
     protected abstract void onHandleConfigurationChanged();
+
+    /**
+     * Enter staged split directly from the current running app.
+     * @param leftOrTop if the staged split will be positioned left or top.
+     */
+    public void enterStageSplitFromRunningApp(boolean leftOrTop) { }
 }
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 269baf0..acb7eb3 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -47,7 +47,9 @@
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -214,11 +216,15 @@
             }
 
             case TestProtocol.REQUEST_HAS_TIS: {
-                response.putBoolean(
-                        TestProtocol.REQUEST_HAS_TIS, false);
+                response.putBoolean(TestProtocol.REQUEST_HAS_TIS, false);
                 return response;
             }
 
+            case TestProtocol.REQUEST_ALL_APPS_TOP_PADDING: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        l -> l.getAppsView().getActiveRecyclerView().getClipBounds().top);
+            }
+
             default:
                 return null;
         }
@@ -261,17 +267,24 @@
      */
     private static <S, T> Bundle getUIProperty(
             BundleSetter<T> bundleSetter, Function<S, T> provider, Supplier<S> targetSupplier) {
+        return getFromExecutorSync(MAIN_EXECUTOR, () -> {
+            S target = targetSupplier.get();
+            if (target == null) {
+                return null;
+            }
+            T value = provider.apply(target);
+            Bundle response = new Bundle();
+            bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value);
+            return response;
+        });
+    }
+
+    /**
+     * Executes the callback on the executor and waits for the result
+     */
+    protected static <T> T getFromExecutorSync(ExecutorService executor, Callable<T> callback) {
         try {
-            return MAIN_EXECUTOR.submit(() -> {
-                S target = targetSupplier.get();
-                if (target == null) {
-                    return null;
-                }
-                T value = provider.apply(target);
-                Bundle response = new Bundle();
-                bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value);
-                return response;
-            }).get();
+            return executor.submit(callback).get();
         } catch (ExecutionException | InterruptedException e) {
             throw new RuntimeException(e);
         }
diff --git a/src/com/android/launcher3/testing/shared/TestProtocol.java b/src/com/android/launcher3/testing/shared/TestProtocol.java
index 91b7b2d..3fbce88 100644
--- a/src/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/src/com/android/launcher3/testing/shared/TestProtocol.java
@@ -112,6 +112,9 @@
             "get-activities-created-count";
     public static final String REQUEST_GET_ACTIVITIES = "get-activities";
     public static final String REQUEST_HAS_TIS = "has-touch-interaction-service";
+    public static final String REQUEST_TASKBAR_ALL_APPS_TOP_PADDING =
+            "taskbar-all-apps-top-padding";
+    public static final String REQUEST_ALL_APPS_TOP_PADDING = "all-apps-top-padding";
 
     public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size";
     public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center";
@@ -125,6 +128,7 @@
     public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing";
     public static final String REQUEST_ENABLE_ROTATION = "enable_rotation";
     public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion";
+    public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared";
 
     public static boolean sDebugTracing = false;
     public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 3eff783..19a3948 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -182,4 +182,12 @@
                 ? LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP
                 : LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM;
     }
+
+    public static @StagePosition int getOppositeStagePosition(@StagePosition int position) {
+        if (position == STAGE_POSITION_UNDEFINED) {
+            return position;
+        }
+        return position == STAGE_POSITION_TOP_OR_LEFT ? STAGE_POSITION_BOTTOM_OR_RIGHT
+                : STAGE_POSITION_TOP_OR_LEFT;
+    }
 }
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/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index ae1060e..bedf277 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -24,6 +24,7 @@
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
     <uses-permission android:name="android.permission.READ_LOGS"/>
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <application android:debuggable="true" android:extractNativeLibs="true">
         <uses-library android:name="android.test.runner"/>
@@ -62,6 +63,17 @@
         </receiver>
 
         <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetWithDialog"
+            android:exported="true"
+            android:label="With Dialog">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                android:resource="@xml/appwidget_no_config"/>
+        </receiver>
+
+        <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
             android:exported="true"
             android:label="Dynamic Colors">
@@ -276,7 +288,17 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
-
+        <activity
+            android:name="com.android.launcher3.testcomponent.DialogTestActivity"
+            android:label="Dialog Activity"
+            android:theme="@android:style/Theme.Dialog"
+            android:exported="true"
+            android:taskAffinity="com.android.launcher3.testcomponent.Affinity2">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
         <activity android:name="com.android.launcher3.testcomponent.ImeTestActivity"
             android:label="ImeTestActivity"
             android:icon="@drawable/test_theme_icon"
diff --git a/tests/res/layout/test_layout_appwidget_blue.xml b/tests/res/layout/test_layout_appwidget_blue.xml
index 8111978..f33e575 100644
--- a/tests/res/layout/test_layout_appwidget_blue.xml
+++ b/tests/res/layout/test_layout_appwidget_blue.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/content"
     android:orientation="vertical"
     android:background="#FF0000FF"
     android:layout_width="match_parent"
diff --git a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt b/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
index 2c1cbdf..fe30fdc 100644
--- a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
+++ b/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
@@ -16,11 +16,12 @@
 package com.android.launcher3
 
 import android.content.Context
+import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
 import android.util.SparseArray
 import androidx.test.core.app.ApplicationProvider
-import com.android.launcher3.DeviceProfile.DEFAULT_PROVIDER;
+import com.android.launcher3.DeviceProfile.DEFAULT_PROVIDER
 import com.android.launcher3.util.DisplayController.Info
 import com.android.launcher3.util.WindowBounds
 import org.junit.Before
@@ -116,10 +117,7 @@
 
             numFolderRows = 3
             numFolderColumns = 3
-            folderBorderSpaces = PointF(16f, 16f)
-            folderTopPadding = 24f
-            folderCellSize = PointF(80f, 94f)
-
+            folderStyle = R.style.FolderDefaultStyle
 
             inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_4_5
 
@@ -207,9 +205,7 @@
 
             numFolderRows = 3
             numFolderColumns = 3
-            folderBorderSpaces = PointF(16f, 16f)
-            folderTopPadding = 24f
-            folderCellSize = PointF(120f, 104f)
+            folderStyle = R.style.FolderDefaultStyle
 
             inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_6_5
 
@@ -241,7 +237,7 @@
             numAllAppsColumns = 6
 
             isScalable = true
-            devicePaddingId = 2132148242 // "@xml/paddings_6x5"
+            devicePaddingId = R.xml.paddings_6x5
 
             inlineQsb = booleanArrayOf(
                     false,
@@ -305,9 +301,7 @@
 
             numFolderRows = 3
             numFolderColumns = 3
-            folderBorderSpaces = PointF(16f, 16f)
-            folderTopPadding = 24f
-            folderCellSize = PointF(80f, 94f)
+            folderStyle = R.style.FolderDefaultStyle
 
             inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_4_4
 
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index 2846cae..9ba3f7b 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -116,7 +116,7 @@
         FavoriteItemsTransaction transaction =
                 new FavoriteItemsTransaction(mTargetContext, this);
         mWorkspaceBuilder.buildFromBoard(testCase.mStart, transaction).commit();
-
+        waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
         Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.x,
                 mainWidgetCellPos.y);
         assertNotNull(widget);
diff --git a/tests/src/com/android/launcher3/testcomponent/AppWidgetWithDialog.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetWithDialog.java
new file mode 100644
index 0000000..6d617fa
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetWithDialog.java
@@ -0,0 +1,41 @@
+/*
+ * 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.launcher3.testcomponent;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+
+/**
+ * A simple app widget with shows a dialog on clicking.
+ */
+public class AppWidgetWithDialog extends AppWidgetNoConfig {
+
+    @Override
+    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+        int layoutId = context.getResources().getIdentifier(
+                "test_layout_appwidget_blue", "layout", context.getPackageName());
+        RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
+
+        PendingIntent pi = PendingIntent.getActivity(context, 0,
+                new Intent(context, DialogTestActivity.class), PendingIntent.FLAG_IMMUTABLE);
+        views.setOnClickPendingIntent(android.R.id.content, pi);
+        AppWidgetManager.getInstance(context).updateAppWidget(appWidgetIds, views);
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/DialogTestActivity.java b/tests/src/com/android/launcher3/testcomponent/DialogTestActivity.java
new file mode 100644
index 0000000..9e5a274
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/DialogTestActivity.java
@@ -0,0 +1,23 @@
+/*
+ * 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.launcher3.testcomponent;
+
+
+/**
+ * Extension of BaseTestingActivity with a Dialog theme
+ */
+public class DialogTestActivity extends BaseTestingActivity {}
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index cf5f5fc..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.
@@ -478,7 +479,7 @@
     @Test
     @PortraitLandscape
     public void testUninstallFromWorkspace() throws Exception {
-        TestUtil.installDummyApp();
+        installDummyAppAndWaitForUIUpdate();
         try {
             verifyAppUninstalledFromAllApps(
                     createShortcutInCenterIfNotExist(DUMMY_APP_NAME).uninstall(), DUMMY_APP_NAME);
@@ -489,8 +490,9 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecord // (b/256659409)
     public void testUninstallFromAllApps() throws Exception {
-        TestUtil.installDummyApp();
+        installDummyAppAndWaitForUIUpdate();
         try {
             Workspace workspace = mLauncher.getWorkspace();
             final HomeAllApps allApps = workspace.switchToAllApps();
@@ -535,7 +537,7 @@
         Point[] gridPositions = getCornersAndCenterPositions();
         createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
         createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
-        TestUtil.installDummyApp();
+        installDummyAppAndWaitForUIUpdate();
         try {
             createShortcutIfNotExist(DUMMY_APP_NAME, gridPositions[2]);
             Map<String, Point> initialPositions =
@@ -586,6 +588,17 @@
                 mLauncher.getWorkspace().getHotseatAppIcon(APP_NAME));
     }
 
+    private void installDummyAppAndWaitForUIUpdate() throws IOException {
+        TestUtil.installDummyApp();
+        // Wait for model thread completion as it may be processing
+        // the install event from the SystemService
+        mLauncher.waitForModelQueueCleared();
+        // Wait for Launcher UI thread completion, as it may be processing updating the UI in
+        // response to the model update. Not that `waitForLauncherInitialized` is just a proxy
+        // method, we can use any method which touches Launcher UI thread,
+        mLauncher.waitForLauncherInitialized();
+    }
+
     /**
      * @return List of workspace grid coordinates. Those are not pixels. See {@link
      *     Workspace#getIconGridDimensions()}
diff --git a/tests/src/com/android/launcher3/util/TestUtil.java b/tests/src/com/android/launcher3/util/TestUtil.java
index 67f3902..d7c6c4f 100644
--- a/tests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/src/com/android/launcher3/util/TestUtil.java
@@ -17,8 +17,13 @@
 
 import static androidx.test.InstrumentationRegistry.getContext;
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
 
+import android.content.pm.LauncherApps;
 import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
 
 import androidx.test.uiautomator.UiDevice;
 
@@ -27,6 +32,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
 
 public class TestUtil {
     public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
@@ -40,24 +46,77 @@
         final String apkFilename = getInstrumentation().getTargetContext().
                 getFilesDir().getPath() + "/dummy_app.apk";
 
-        final FileOutputStream out = new FileOutputStream(apkFilename);
-        byte[] buff = new byte[1024];
-        int read;
+        try (PackageInstallCheck pic = new PackageInstallCheck()) {
+            final FileOutputStream out = new FileOutputStream(apkFilename);
+            byte[] buff = new byte[1024];
+            int read;
 
-        while ((read = in.read(buff)) > 0) {
-            out.write(buff, 0, read);
+            while ((read = in.read(buff)) > 0) {
+                out.write(buff, 0, read);
+            }
+            in.close();
+            out.close();
+
+            final String result = UiDevice.getInstance(getInstrumentation())
+                    .executeShellCommand("pm install " + apkFilename);
+            Assert.assertTrue(
+                    "Failed to install wellbeing test apk; make sure the device is rooted",
+                    "Success".equals(result.replaceAll("\\s+", "")));
+            pic.mAddWait.await();
+        } catch (InterruptedException e) {
+            throw new IOException(e);
         }
-        in.close();
-        out.close();
-
-        final String result = UiDevice.getInstance(getInstrumentation())
-                .executeShellCommand("pm install " + apkFilename);
-        Assert.assertTrue("Failed to install wellbeing test apk; make sure the device is rooted",
-                "Success".equals(result.replaceAll("\\s+", "")));
     }
 
     public static void uninstallDummyApp() throws IOException {
         UiDevice.getInstance(getInstrumentation()).executeShellCommand(
                 "pm uninstall " + DUMMY_PACKAGE);
     }
+
+    private static class PackageInstallCheck extends LauncherApps.Callback
+            implements AutoCloseable {
+
+        final CountDownLatch mAddWait = new CountDownLatch(1);
+        final LauncherApps mLauncherApps;
+
+        PackageInstallCheck() {
+            mLauncherApps = getTargetContext().getSystemService(LauncherApps.class);
+            mLauncherApps.registerCallback(this, new Handler(Looper.getMainLooper()));
+        }
+
+        private void verifyPackage(String packageName) {
+            if (DUMMY_PACKAGE.equals(packageName)) {
+                mAddWait.countDown();
+            }
+        }
+
+        @Override
+        public void onPackageAdded(String packageName, UserHandle user) {
+            verifyPackage(packageName);
+        }
+
+        @Override
+        public void onPackageChanged(String packageName, UserHandle user) {
+            verifyPackage(packageName);
+        }
+
+        @Override
+        public void onPackageRemoved(String packageName, UserHandle user) { }
+
+        @Override
+        public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
+            for (String packageName : packageNames) {
+                verifyPackage(packageName);
+            }
+        }
+
+        @Override
+        public void onPackagesUnavailable(String[] packageNames, UserHandle user,
+                boolean replacing) { }
+
+        @Override
+        public void close() {
+            mLauncherApps.unregisterCallback(this);
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 4791846..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;
@@ -46,8 +52,7 @@
         super(launcher);
         final UiObject2 allAppsContainer = verifyActiveContainer();
         mHeight = mLauncher.getVisibleBounds(allAppsContainer).height();
-        final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
-                "apps_list_view");
+        final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
         // Wait for the recycler to populate.
         mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class));
         verifyNotFrozen("All apps freeze flags upon opening all apps");
@@ -78,6 +83,11 @@
             LauncherInstrumentation.log("hasClickableIcon: icon center is under search box");
             return false;
         }
+        if (iconCenterInRecyclerTopPadding(appListRecycler, icon)) {
+            LauncherInstrumentation.log(
+                    "hasClickableIcon: icon center is under the app list recycler's top padding.");
+            return false;
+        }
         if (iconBounds.bottom > displayBottom) {
             LauncherInstrumentation.log("hasClickableIcon: icon bottom below bottom offset");
             return false;
@@ -92,6 +102,13 @@
                 iconCenter.x, iconCenter.y);
     }
 
+    private boolean iconCenterInRecyclerTopPadding(UiObject2 appListRecycler, UiObject2 icon) {
+        final Point iconCenter = icon.getVisibleCenter();
+
+        return iconCenter.y <= mLauncher.getVisibleBounds(appListRecycler).top
+                + getAppsListRecyclerTopPadding();
+    }
+
     /**
      * Finds an icon. If the icon doesn't exist, return null.
      * Scrolls the app list when needed to make sure the icon is visible.
@@ -105,9 +122,7 @@
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "getting app icon " + appName + " on all apps")) {
             final UiObject2 allAppsContainer = verifyActiveContainer();
-            final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
-                    "apps_list_view");
-            final UiObject2 searchBox = hasSearchBox() ? getSearchBox(allAppsContainer) : null;
+            final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
 
             int deviceHeight = mLauncher.getRealDisplaySize().y;
             int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen();
@@ -128,10 +143,9 @@
                                                 mLauncher.getVisibleBounds(icon).top
                                                         < bottomGestureStartOnScreen)
                                         .collect(Collectors.toList()),
-                                hasSearchBox()
-                                        ? mLauncher.getVisibleBounds(searchBox).bottom
-                                        - mLauncher.getVisibleBounds(allAppsContainer).top
-                                        : 0);
+                                mLauncher.getVisibleBounds(appListRecycler).top
+                                        + getAppsListRecyclerTopPadding()
+                                        - mLauncher.getVisibleBounds(allAppsContainer).top);
                         verifyActiveContainer();
                         final int newScroll = getAllAppsScroll();
                         mLauncher.assertTrue(
@@ -180,16 +194,22 @@
 
     protected abstract boolean hasSearchBox();
 
+    protected abstract int getAppsListRecyclerTopPadding();
+
     private void scrollBackToBeginning() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to scroll back in all apps")) {
             LauncherInstrumentation.log("Scrolling to the beginning");
             final UiObject2 allAppsContainer = verifyActiveContainer();
-            final UiObject2 searchBox = hasSearchBox() ? getSearchBox(allAppsContainer) : null;
+            final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
 
             int attempts = 0;
             final Rect margins = new Rect(
-                    0, hasSearchBox() ? mLauncher.getVisibleBounds(searchBox).bottom + 1 : 0, 0, 5);
+                    /* left= */ 0,
+                    mLauncher.getVisibleBounds(appListRecycler).top
+                            + getAppsListRecyclerTopPadding() + 1,
+                    /* right= */ 0,
+                    /* bottom= */ 5);
 
             for (int scroll = getAllAppsScroll();
                     scroll != 0;
@@ -220,6 +240,10 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    private UiObject2 getAppListRecycler(UiObject2 allAppsContainer) {
+        return mLauncher.waitForObjectInContainer(allAppsContainer, "apps_list_view");
+    }
+
     private UiObject2 getSearchBox(UiObject2 allAppsContainer) {
         return mLauncher.waitForObjectInContainer(allAppsContainer, "search_container_all_apps");
     }
@@ -274,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/AllAppsFromTaskbar.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
index 5164025..f804e28 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
@@ -18,6 +18,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.shared.TestProtocol;
+
 /**
  * Operations on AllApps opened from the Taskbar.
  */
@@ -48,4 +50,10 @@
     protected boolean hasSearchBox() {
         return false;
     }
+
+    @Override
+    protected int getAppsListRecyclerTopPadding() {
+        return mLauncher.getTestInfo(TestProtocol.REQUEST_TASKBAR_ALL_APPS_TOP_PADDING)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 15705e7..5a96d95 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -58,7 +58,7 @@
                      "want to switch from background to overview")) {
             verifyActiveContainer();
             goToOverviewUnchecked();
-            return mLauncher.isFallbackOverview()
+            return mLauncher.is3PLauncher()
                     ? new BaseOverview(mLauncher) : new Overview(mLauncher);
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index f47f710..afeb8d7 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -244,43 +244,39 @@
      * Returns if clear all button is visible.
      */
     public boolean isClearAllVisible() {
-        return mLauncher.hasLauncherObject(mLauncher.getOverviewObjectSelector("clear_all"));
+        return verifyActiveContainer().hasObject(
+                mLauncher.getOverviewObjectSelector("clear_all"));
     }
 
     protected boolean isActionsViewVisible() {
+        if (!hasTasks() || isClearAllVisible()) {
+            return false;
+        }
         OverviewTask task = mLauncher.isTablet() ? getFocusedTaskForTablet() : getCurrentTask();
         if (task == null) {
             return false;
         }
+        // In tablets, if focused task is not in center, overview actions aren't visible.
+        if (mLauncher.isTablet()
+                && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
+            return false;
+        }
+        // Overview actions aren't visible for split screen tasks.
         return !task.isTaskSplit();
     }
 
     private void verifyActionsViewVisibility() {
-        if (!hasTasks() || !isActionsViewVisible()) {
-            return;
-        }
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {
-            if (mLauncher.isTablet() && !isOverviewSnappedToFocusedTaskForTablet()) {
-                mLauncher.waitUntilOverviewObjectGone("action_buttons");
-            } else {
+            if (isActionsViewVisible()) {
                 mLauncher.waitForOverviewObject("action_buttons");
+            } else {
+                mLauncher.waitUntilOverviewObjectGone("action_buttons");
             }
         }
     }
 
     /**
-     * Returns if focused task is currently snapped task in tablet grid overview.
-     */
-    private boolean isOverviewSnappedToFocusedTaskForTablet() {
-        OverviewTask focusedTask = getFocusedTaskForTablet();
-        if (focusedTask == null) {
-            return false;
-        }
-        return Math.abs(focusedTask.getExactCenterX() - mLauncher.getExactScreenCenterX()) < 1;
-    }
-
-    /**
      * Returns Overview focused task if it exists.
      *
      * @throws IllegalStateException if not run on a tablet device.
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index 7123de4..9a4c6d4 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -18,6 +18,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.shared.TestProtocol;
+
 public class HomeAllApps extends AllApps {
     private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background";
 
@@ -47,6 +49,12 @@
         return true;
     }
 
+    @Override
+    protected int getAppsListRecyclerTopPadding() {
+        return mLauncher.getTestInfo(TestProtocol.REQUEST_ALL_APPS_TOP_PADDING)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     /**
      * Taps outside bottom sheet to dismiss and return to workspace. Available on tablets only.
      * @param tapRight Tap on the right of bottom sheet if true, or left otherwise.
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 4b02ecc..f23a38c 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -55,7 +55,7 @@
     public Taskbar getTaskbar() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get the taskbar")) {
-            mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+            mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
 
             return new Taskbar(mLauncher);
         }
@@ -67,7 +67,7 @@
     public void assertTaskbarHidden() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "waiting for taskbar to be hidden")) {
-            mLauncher.waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+            mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
         }
     }
 
@@ -77,7 +77,7 @@
     public void assertTaskbarVisible() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "waiting for taskbar to be visible")) {
-            mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+            mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
         }
     }
 
@@ -92,7 +92,7 @@
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                      "want to show the taskbar")) {
-            mLauncher.waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+            mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
 
             final long downTime = SystemClock.uptimeMillis();
             final int unstashTargetY = mLauncher.getRealDisplaySize().y
@@ -106,7 +106,7 @@
             LauncherInstrumentation.log("showTaskbar: sent down");
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) {
-                mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+                mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
                 mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, unstashTarget,
                         LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 449b7b7..1212c3d 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";
 
@@ -559,7 +561,7 @@
     private String getVisibleStateMessage() {
         if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
-        if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
+        if (hasSystemLauncherObject(OVERVIEW_RES_ID)) return "Overview";
         if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
         if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
         return "LaunchedApp (" + getVisiblePackages() + ")";
@@ -771,73 +773,89 @@
             switch (containerType) {
                 case WORKSPACE: {
                     waitUntilLauncherObjectGone(APPS_RES_ID);
-                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    waitUntilLauncherObjectGone(TASKBAR_RES_ID);
-                    waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+
+                    if (is3PLauncher() && isTablet()) {
+                        waitForSystemLauncherObject(TASKBAR_RES_ID);
+                    } else {
+                        waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+                    }
 
                     return waitForLauncherObject(WORKSPACE_RES_ID);
                 }
                 case WIDGETS: {
                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
                     waitUntilLauncherObjectGone(APPS_RES_ID);
-                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
-                    waitUntilLauncherObjectGone(TASKBAR_RES_ID);
-                    waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+
+                    if (is3PLauncher() && isTablet()) {
+                        waitForSystemLauncherObject(TASKBAR_RES_ID);
+                    } else {
+                        waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+                    }
 
                     return waitForLauncherObject(WIDGETS_RES_ID);
                 }
-                case TASKBAR_ALL_APPS:
-                case HOME_ALL_APPS: {
+                case TASKBAR_ALL_APPS: {
                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
-                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    waitUntilLauncherObjectGone(TASKBAR_RES_ID);
-                    waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+                    waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
 
                     return waitForLauncherObject(APPS_RES_ID);
                 }
-                case OVERVIEW: {
+                case HOME_ALL_APPS: {
+                    waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
+                    waitUntilLauncherObjectGone(WIDGETS_RES_ID);
+                    waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+
+                    if (is3PLauncher() && isTablet()) {
+                        waitForSystemLauncherObject(TASKBAR_RES_ID);
+                    } else {
+                        waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+                    }
+
+                    return waitForLauncherObject(APPS_RES_ID);
+                }
+                case OVERVIEW:
+                case FALLBACK_OVERVIEW: {
                     waitUntilLauncherObjectGone(APPS_RES_ID);
                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    waitUntilLauncherObjectGone(TASKBAR_RES_ID);
-                    waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+                    waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
 
-                    return waitForLauncherObject(OVERVIEW_RES_ID);
+                    return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
                 case SPLIT_SCREEN_SELECT: {
                     waitUntilLauncherObjectGone(APPS_RES_ID);
                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+                    waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
 
-                    waitForLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
-                    return waitForLauncherObject(OVERVIEW_RES_ID);
-                }
-                case FALLBACK_OVERVIEW: {
-                    waitUntilLauncherObjectGone(APPS_RES_ID);
-                    waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
-                    waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    waitUntilLauncherObjectGone(TASKBAR_RES_ID);
-                    waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
-
-                    return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
+                    waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
+                    return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
                 case LAUNCHED_APP: {
                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
                     waitUntilLauncherObjectGone(APPS_RES_ID);
-                    waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
-                    waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+                    waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
 
                     if (mIgnoreTaskbarVisibility) {
                         return null;
                     }
-                    if (isTablet() && !isFallbackOverview()) {
-                        waitForLauncherObject(TASKBAR_RES_ID);
+
+                    if (isTablet()) {
+                        waitForSystemLauncherObject(TASKBAR_RES_ID);
                     } else {
-                        waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+                        waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     }
                     return null;
                 }
@@ -848,6 +866,10 @@
         }
     }
 
+    public void waitForModelQueueCleared() {
+        getTestInfo(TestProtocol.REQUEST_MODEL_QUEUE_CLEARED);
+    }
+
     public void waitForLauncherInitialized() {
         for (int i = 0; i < 100; ++i) {
             if (getTestInfo(
@@ -968,14 +990,9 @@
                 checkForAnomaly(false, true);
 
                 final Point displaySize = getRealDisplaySize();
-                // The swipe up to home gesture starts from inside the launcher when the user is
-                // already home. Otherwise, the gesture can start inside the launcher process if the
-                // taskbar is visible.
-                boolean gestureStartFromLauncher = isTablet()
-                        ? !isLauncher3()
-                        || hasLauncherObject(WORKSPACE_RES_ID)
-                        || hasLauncherObject(TASKBAR_RES_ID)
-                        : isLauncherVisible();
+
+                boolean gestureStartFromLauncher =
+                        isTablet() ? !isLauncher3() : isLauncherVisible();
 
                 // CLose floating views before going back to home.
                 swipeUpToCloseFloatingView(gestureStartFromLauncher);
@@ -1008,7 +1025,7 @@
                         NORMAL_STATE_ORDINAL,
                         !hasLauncherObject(WORKSPACE_RES_ID)
                                 && (hasLauncherObject(APPS_RES_ID)
-                                || hasLauncherObject(OVERVIEW_RES_ID)),
+                                || hasSystemLauncherObject(OVERVIEW_RES_ID)),
                         action);
             }
             try (LauncherInstrumentation.Closable c1 = addContextLayer(
@@ -1064,7 +1081,8 @@
 
     boolean isLauncherContainerVisible() {
         final String[] containerResources = {WORKSPACE_RES_ID, OVERVIEW_RES_ID, APPS_RES_ID};
-        return Arrays.stream(containerResources).anyMatch(r -> hasLauncherObject(r));
+        return Arrays.stream(containerResources).anyMatch(
+                r -> r.equals(OVERVIEW_RES_ID) ? hasSystemLauncherObject(r) : hasLauncherObject(r));
     }
 
     /**
@@ -1147,6 +1165,14 @@
         waitUntilGoneBySelector(getOverviewObjectSelector(resId));
     }
 
+    void waitUntilSystemLauncherObjectGone(String resId) {
+        if (is3PLauncher()) {
+            waitUntilOverviewObjectGone(resId);
+        } else {
+            waitUntilLauncherObjectGone(resId);
+        }
+    }
+
     void waitUntilLauncherObjectGone(BySelector selector) {
         waitUntilGoneBySelector(makeLauncherSelector(selector));
     }
@@ -1277,6 +1303,11 @@
         return mDevice.hasObject(getLauncherObjectSelector(resId));
     }
 
+    private boolean hasSystemLauncherObject(String resId) {
+        return mDevice.hasObject(is3PLauncher() ? getOverviewObjectSelector(resId)
+                : getLauncherObjectSelector(resId));
+    }
+
     boolean hasLauncherObject(BySelector selector) {
         return mDevice.hasObject(makeLauncherSelector(selector));
     }
@@ -1296,6 +1327,12 @@
     }
 
     @NonNull
+    UiObject2 waitForSystemLauncherObject(String resName) {
+        return is3PLauncher() ? waitForOverviewObject(resName)
+                : waitForLauncherObject(resName);
+    }
+
+    @NonNull
     UiObject2 waitForLauncherObject(BySelector selector) {
         return waitForObjectBySelector(makeLauncherSelector(selector));
     }
@@ -1306,11 +1343,6 @@
     }
 
     @NonNull
-    UiObject2 waitForFallbackLauncherObject(String resName) {
-        return waitForObjectBySelector(getOverviewObjectSelector(resName));
-    }
-
-    @NonNull
     UiObject2 waitForAndroidObject(String resId) {
         final UiObject2 object = TestHelpers.wait(
                 Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
@@ -1347,7 +1379,7 @@
         return mDevice.getLauncherPackageName();
     }
 
-    boolean isFallbackOverview() {
+    boolean is3PLauncher() {
         return !getOverviewPackageName().equals(getLauncherPackageName());
     }
 
@@ -1987,4 +2019,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;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index 0f9d5f5..6ca7f4b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -52,7 +52,7 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get a taskbar icon")) {
             return new TaskbarAppIcon(mLauncher, mLauncher.waitForObjectInContainer(
-                    mLauncher.waitForLauncherObject(TASKBAR_RES_ID),
+                    mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
                     AppIcon.getAppIconSelector(appName, mLauncher)));
         }
     }
@@ -68,7 +68,7 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to hide the taskbar");
              LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+            mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
 
             final long downTime = SystemClock.uptimeMillis();
             Point stashTarget = new Point(
@@ -79,7 +79,7 @@
             LauncherInstrumentation.log("hideTaskbar: sent down");
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) {
-                mLauncher.waitUntilLauncherObjectGone("taskbar_view");
+                mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                 mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, stashTarget,
                         LauncherInstrumentation.GestureScope.INSIDE);
             }
@@ -97,7 +97,8 @@
              LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
 
             mLauncher.clickLauncherObject(mLauncher.waitForObjectInContainer(
-                    mLauncher.waitForLauncherObject(TASKBAR_RES_ID), getAllAppsButtonSelector()));
+                    mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
+                    getAllAppsButtonSelector()));
 
             return new AllAppsFromTaskbar(mLauncher);
         }
@@ -108,7 +109,7 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get all taskbar icons")) {
             return mLauncher.waitForObjectsInContainer(
-                    mLauncher.waitForLauncherObject(TASKBAR_RES_ID),
+                    mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
                     AppIcon.getAnyAppIconSelector())
                     .stream()
                     .map(UiObject2::getText)