Merge "Fix NPE from null thumbnail data when canceling recents animation" into sc-v2-dev
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 0f92274..25b39ed 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
     <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS"/>
     <uses-permission android:name="android.permission.REMOVE_TASKS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
     <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
     <uses-permission android:name="android.permission.STATUS_BAR"/>
     <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index e82c900..f9a0bb1 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -161,8 +161,7 @@
             }
             InstanceId instanceId = new InstanceIdSequence().newInstanceId();
             for (ItemInfo info : itemsIdMap) {
-                FolderInfo parent = info.container > 0
-                        ? (FolderInfo) itemsIdMap.get(info.container) : null;
+                FolderInfo parent = getContainer(info, itemsIdMap);
                 StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
             }
             additionalSnapshotEvents(instanceId);
@@ -199,8 +198,7 @@
                         }
 
                         for (ItemInfo info : itemsIdMap) {
-                            FolderInfo parent = info.container > 0
-                                    ? (FolderInfo) itemsIdMap.get(info.container) : null;
+                            FolderInfo parent = getContainer(info, itemsIdMap);
                             LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
                             Log.d(TAG, itemInfo.toString());
                             StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
@@ -222,6 +220,22 @@
         }
     }
 
+    private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap) {
+        if (info.container > 0) {
+            ItemInfo containerInfo = itemsIdMap.get(info.container);
+
+            if (!(containerInfo instanceof FolderInfo)) {
+                Log.e(TAG, String.format(
+                        "Item info: %s found with invalid container: %s",
+                        info,
+                        containerInfo));
+            } else {
+                return (FolderInfo) containerInfo;
+            }
+        }
+        return null;
+    }
+
     @Override
     public void validateData() {
         super.validateData();
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 9d70cfa..e1d89a1 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -44,7 +44,6 @@
 import com.android.systemui.shared.system.BlurUtils;
 import com.android.systemui.shared.system.WallpaperManagerCompat;
 
-import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.function.Consumer;
 
@@ -156,6 +155,10 @@
     // Workaround for animating the depth when multiwindow mode changes.
     private boolean mIgnoreStateChangesDuringMultiWindowAnimation = false;
 
+    // Hints that there is potentially content behind Launcher and that we shouldn't optimize by
+    // marking the launcher surface as opaque.  Only used in certain Launcher states.
+    private boolean mHasContentBehindLauncher;
+
     private View.OnAttachStateChangeListener mOnAttachListener;
 
     public DepthController(Launcher l) {
@@ -199,6 +202,10 @@
         mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
     }
 
+    public void setHasContentBehindLauncher(boolean hasContentBehindLauncher) {
+        mHasContentBehindLauncher = hasContentBehindLauncher;
+    }
+
     /**
      * Sets if the underlying activity is started or not
      */
@@ -311,13 +318,14 @@
         }
 
         if (supportsBlur) {
-            boolean opaque = mLauncher.getScrimView().isFullyOpaque();
+            boolean hasOpaqueBg = mLauncher.getScrimView().isFullyOpaque();
+            boolean isSurfaceOpaque = !mHasContentBehindLauncher && hasOpaqueBg;
 
-            mCurrentBlur = !mCrossWindowBlursEnabled || mBlurDisabledForAppLaunch
+            mCurrentBlur = !mCrossWindowBlursEnabled || mBlurDisabledForAppLaunch || hasOpaqueBg
                     ? 0 : (int) (depth * mMaxBlurRadius);
             SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()
                     .setBackgroundBlurRadius(mSurface, mCurrentBlur)
-                    .setOpaque(mSurface, opaque);
+                    .setOpaque(mSurface, isSurfaceOpaque);
 
             // Set early wake-up flags when we know we're executing an expensive operation, this way
             // SurfaceFlinger will adjust its internal offsets to avoid jank.
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 24a88a4..f1e6747 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -46,16 +46,13 @@
                 }
             };
 
-    // Initialized in init.
-    TaskbarControllers mControllers;
-
     public FallbackTaskbarUIController(RecentsActivity recentsActivity) {
         mRecentsActivity = recentsActivity;
     }
 
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
-        mControllers = taskbarControllers;
+        super.init(taskbarControllers);
 
         mRecentsActivity.setTaskbarUIController(this);
         mRecentsActivity.getStateManager().addStateListener(mStateListener);
@@ -63,6 +60,7 @@
 
     @Override
     protected void onDestroy() {
+        super.onDestroy();
         mRecentsActivity.setTaskbarUIController(null);
         mRecentsActivity.getStateManager().removeStateListener(mStateListener);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 1c0c773..2622700 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -25,7 +25,6 @@
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.TaskTransitionSpec;
-import android.view.View;
 import android.view.WindowManagerGlobal;
 
 import androidx.annotation.NonNull;
@@ -62,7 +61,6 @@
             this::onStashedInAppChanged;
 
     // Initialized in init.
-    private TaskbarControllers mControllers;
     private AnimatedFloat mTaskbarOverrideBackgroundAlpha;
     private TaskbarKeyguardController mKeyguardController;
     private final TaskbarLauncherStateController
@@ -83,7 +81,7 @@
 
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
-        mControllers = taskbarControllers;
+        super.init(taskbarControllers);
 
         mTaskbarLauncherStateController.init(mControllers, mLauncher);
         mTaskbarOverrideBackgroundAlpha = mControllers.taskbarDragLayerController
@@ -101,6 +99,7 @@
 
     @Override
     protected void onDestroy() {
+        super.onDestroy();
         onLauncherResumedOrPaused(false);
         mTaskbarLauncherStateController.onDestroy();
 
@@ -168,10 +167,6 @@
         return mControllers.taskbarDragController.isDragging();
     }
 
-    public View getRootView() {
-        return mControllers.taskbarActivityContext.getDragLayer();
-    }
-
     @Override
     protected void onStashedInAppChanged() {
         onStashedInAppChanged(mLauncher.getDeviceProfile());
@@ -251,4 +246,12 @@
         mLauncher.logAppLaunch(mControllers.taskbarActivityContext.getStatsLogManager(), item,
                 instanceId);
     }
+
+    @Override
+    public void setSystemGestureInProgress(boolean inProgress) {
+        super.setSystemGestureInProgress(inProgress);
+        // Launcher's ScrimView will draw the background throughout the gesture. But once the
+        // gesture ends, start drawing taskbar's background again since launcher might stop drawing.
+        forceHideBackground(inProgress);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 0565f7e..ce1e8b6b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
-import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_IME;
 import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_KEYGUARD;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
@@ -32,12 +31,14 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
 import android.annotation.DrawableRes;
 import android.annotation.IdRes;
 import android.annotation.LayoutRes;
+import android.content.pm.ActivityInfo.Config;
 import android.content.res.ColorStateList;
 import android.graphics.Rect;
 import android.graphics.Region;
@@ -86,6 +87,7 @@
     private static final int FLAG_DISABLE_RECENTS = 1 << 8;
     private static final int FLAG_DISABLE_BACK = 1 << 9;
     private static final int FLAG_NOTIFICATION_SHADE_EXPANDED = 1 << 10;
+    private static final int FLAG_SCREEN_PINNING_ACTIVE = 1 << 10;
 
     private static final int MASK_IME_SWITCHER_VISIBLE = FLAG_SWITCHER_SUPPORTED | FLAG_IME_VISIBLE;
 
@@ -140,11 +142,6 @@
         mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarSize;
         mNavButtonTranslationYMultiplier.value = 1;
 
-        mPropertyHolders.add(new StatePropertyHolder(
-                mControllers.taskbarViewController.getTaskbarIconAlpha()
-                        .getProperty(ALPHA_INDEX_IME),
-                flags -> (flags & FLAG_IME_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0));
-
         boolean isThreeButtonNav = mContext.isThreeButtonNav();
         // IME switcher
         View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
@@ -157,7 +154,9 @@
         mPropertyHolders.add(new StatePropertyHolder(
                 mControllers.taskbarViewController.getTaskbarIconAlpha()
                         .getProperty(ALPHA_INDEX_KEYGUARD),
-                flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0));
+                flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0
+                        && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0,
+                MultiValueAlpha.VALUE, 1, 0));
 
         mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
                 .getKeyguardBgTaskbar(),
@@ -196,7 +195,7 @@
             }
 
             // Animate taskbar background when any of these flags are enabled
-            int flagsToShowBg = FLAG_IME_VISIBLE | FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE
+            int flagsToShowBg = FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE
                     | FLAG_NOTIFICATION_SHADE_EXPANDED;
             mPropertyHolders.add(new StatePropertyHolder(
                     mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
@@ -291,6 +290,7 @@
         int shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
                 | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
         boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0;
+        boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
 
         // TODO(b/202218289) we're getting IME as not visible on lockscreen from system
         updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible);
@@ -300,6 +300,7 @@
         updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled);
         updateStateForFlag(FLAG_DISABLE_BACK, isBackDisabled);
         updateStateForFlag(FLAG_NOTIFICATION_SHADE_EXPANDED, isNotificationShadeExpanded);
+        updateStateForFlag(FLAG_SCREEN_PINNING_ACTIVE, isScreenPinningActive);
 
         if (mA11yButton != null) {
             // Only used in 3 button
@@ -451,6 +452,12 @@
         return mFloatingRotationButtonBounds.contains((int) ev.getX(), (int) ev.getY());
     }
 
+    public void onConfigurationChanged(@Config int configChanges) {
+        if (mFloatingRotationButton != null) {
+            mFloatingRotationButton.onConfigurationChanged(configChanges);
+        }
+    }
+
     public void onDestroy() {
         mPropertyHolders.clear();
         mControllers.rotationButtonController.unregisterListeners();
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 2c80f06..22ca63f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -16,6 +16,8 @@
 package com.android.launcher3.taskbar;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.graphics.Outline;
@@ -23,8 +25,6 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.RevealOutlineAnimation;
@@ -66,7 +66,10 @@
     private final Rect mStashedHandleBounds = new Rect();
     private float mStashedHandleRadius;
 
-    private boolean mIsAtStashedRevealBounds = true;
+    // When the reveal animation is cancelled, we can assume it's about to create a new animation,
+    // which should start off at the same point the cancelled one left off.
+    private float mStartProgressForNextRevealAnim;
+    private boolean mWasLastRevealAnimReversed;
 
     public StashedHandleViewController(TaskbarActivityContext activity,
             StashedHandleView stashedHandleView) {
@@ -148,15 +151,27 @@
      * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
      * morphs into the size of where the taskbar icons will be.
      */
-    public @Nullable Animator createRevealAnimToIsStashed(boolean isStashed) {
-        if (mIsAtStashedRevealBounds == isStashed) {
-            return null;
-        }
-        mIsAtStashedRevealBounds = isStashed;
+    public Animator createRevealAnimToIsStashed(boolean isStashed) {
         final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
                 mStashedHandleRadius, mStashedHandleRadius,
                 mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds);
-        return handleRevealProvider.createRevealAnimator(mStashedHandleView, !isStashed);
+
+        boolean isReversed = !isStashed;
+        boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
+        mWasLastRevealAnimReversed = isReversed;
+        if (changingDirection) {
+            mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
+        }
+
+        ValueAnimator revealAnim = handleRevealProvider.createRevealAnimator(mStashedHandleView,
+                isReversed, mStartProgressForNextRevealAnim);
+        revealAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStartProgressForNextRevealAnim = ((ValueAnimator) animation).getAnimatedFraction();
+            }
+        });
+        return revealAnim;
     }
 
     public void onIsStashed(boolean isStashed) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 2e1e5bb..692352b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -30,6 +30,7 @@
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo.Config;
 import android.content.pm.LauncherApps;
 import android.graphics.Insets;
 import android.graphics.PixelFormat;
@@ -205,6 +206,10 @@
         mWindowManager.addView(mDragLayer, mWindowLayoutParams);
     }
 
+    public void onConfigurationChanged(@Config int configChanges) {
+        mControllers.onConfigurationChanged(configChanges);
+    }
+
     public boolean isThreeButtonNav() {
         return mNavMode == Mode.THREE_BUTTONS;
     }
@@ -356,6 +361,7 @@
         mControllers.taskbarStashController.updateStateForSysuiFlags(systemUiStateFlags, fromInit);
         mControllers.taskbarScrimViewController.updateStateForSysuiFlags(systemUiStateFlags,
                 fromInit);
+        mControllers.navButtonController.updateSysuiFlags(systemUiStateFlags);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 56730db..c43fbf9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -15,10 +15,15 @@
  */
 package com.android.launcher3.taskbar;
 
+import android.content.pm.ActivityInfo.Config;
+
 import androidx.annotation.NonNull;
 
 import com.android.systemui.shared.rotation.RotationButtonController;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Hosts various taskbar controllers to facilitate passing between one another.
  */
@@ -43,6 +48,9 @@
     /** Do not store this controller, as it may change at runtime. */
     @NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT;
 
+    private boolean mAreAllControllersInitialized;
+    private final List<Runnable> mPostInitCallbacks = new ArrayList<>();
+
     public TaskbarControllers(TaskbarActivityContext taskbarActivityContext,
             TaskbarDragController taskbarDragController,
             TaskbarNavButtonController navButtonController,
@@ -81,6 +89,8 @@
      * in constructors for now, as some controllers may still be waiting for init().
      */
     public void init(TaskbarSharedState sharedState) {
+        mAreAllControllersInitialized = false;
+
         taskbarDragController.init(this);
         navbarButtonsViewController.init(this);
         rotationButtonController.init();
@@ -92,6 +102,16 @@
         stashedHandleViewController.init(this);
         taskbarStashController.init(this, sharedState);
         taskbarEduController.init(this);
+
+        mAreAllControllersInitialized = true;
+        for (Runnable postInitCallback : mPostInitCallbacks) {
+            postInitCallback.run();
+        }
+        mPostInitCallbacks.clear();
+    }
+
+    public void onConfigurationChanged(@Config int configChanges) {
+        navbarButtonsViewController.onConfigurationChanged(configChanges);
     }
 
     /**
@@ -108,4 +128,17 @@
         stashedHandleViewController.onDestroy();
         taskbarAutohideSuspendController.onDestroy();
     }
+
+    /**
+     * If all controllers are already initialized, runs the given callback immediately. Otherwise,
+     * queues it to run after calling init() on all controllers. This should likely be used in any
+     * case where one controller is telling another controller to do something inside init().
+     */
+    public void runAfterInit(Runnable callback) {
+        if (mAreAllControllersInitialized) {
+            callback.run();
+        } else {
+            mPostInitCallbacks.add(callback);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 248c40d..806b390 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -44,6 +44,7 @@
     private final AnimatedFloat mKeyguardBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
     private final AnimatedFloat mNotificationShadeBgTaskbar = new AnimatedFloat(
             this::updateBackgroundAlpha);
+    private final AnimatedFloat mImeBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
     // Used to hide our background color when someone else (e.g. ScrimView) is handling it.
     private final AnimatedFloat mBgOverride = new AnimatedFloat(this::updateBackgroundAlpha);
 
@@ -74,6 +75,7 @@
         mBgTaskbar.value = 1;
         mKeyguardBgTaskbar.value = 1;
         mNotificationShadeBgTaskbar.value = 1;
+        mImeBgTaskbar.value = 1;
         mBgOverride.value = 1;
         updateBackgroundAlpha();
     }
@@ -108,6 +110,10 @@
         return mNotificationShadeBgTaskbar;
     }
 
+    public AnimatedFloat getImeBgTaskbar() {
+        return mImeBgTaskbar;
+    }
+
     public AnimatedFloat getOverrideBackgroundAlpha() {
         return mBgOverride;
     }
@@ -119,7 +125,7 @@
     private void updateBackgroundAlpha() {
         final float bgNavbar = mBgNavbar.value;
         final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
-                * mNotificationShadeBgTaskbar.value;
+                * mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value;
         mLastSetBackgroundAlpha = mBgOverride.value * Math.max(bgNavbar, bgTaskbar);
         mTaskbarDragLayer.setTaskbarBackgroundAlpha(mLastSetBackgroundAlpha);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 152b255..f3c8cf3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.taskbar;
 
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
 import static com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION;
@@ -68,9 +67,6 @@
     private int mPrevState;
     private int mState;
 
-    private LauncherState mTargetStateOverride = null;
-    private LauncherState mTargetStateOverrideForStateTransition = null;
-
     private boolean mIsAnimatingToLauncherViaGesture;
     private boolean mIsAnimatingToLauncherViaResume;
 
@@ -79,7 +75,6 @@
 
                 @Override
                 public void onStateTransitionStart(LauncherState toState) {
-                    mTargetStateOverrideForStateTransition = toState;
                     updateStateForFlag(FLAG_TRANSITION_STATE_START_STASHED,
                             toState.isTaskbarStashed());
                     applyState();
@@ -134,18 +129,6 @@
         updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, true);
         animatorSet.play(stashController.applyStateWithoutStart(duration));
         animatorSet.play(applyState(duration, false));
-        animatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animator) {
-                mTargetStateOverride = null;
-                animator.removeListener(this);
-            }
-
-            @Override
-            public void onAnimationStart(Animator animator) {
-                mTargetStateOverride = toState;
-            }
-        });
 
         TaskBarRecentsAnimationListener listener = new TaskBarRecentsAnimationListener(callbacks);
         callbacks.addListener(listener);
@@ -284,7 +267,6 @@
                         // Reset hotseat alpha to default
                         mLauncher.getHotseat().setIconsAlpha(1);
                     }
-                    mTargetStateOverrideForStateTransition = null;
                 }
 
                 @Override
@@ -296,8 +278,6 @@
             animatorSet.play(mIconAlignmentForLauncherState.animateToValue(
                     getCurrentIconAlignmentRatioForLauncherState(),
                     isTransitionStateStashed ? 0 : 1));
-        } else {
-            mTargetStateOverrideForStateTransition = null;
         }
     }
 
@@ -318,20 +298,14 @@
     }
 
     private void onIconAlignmentRatioChangedForStateTransition() {
-        onIconAlignmentRatioChanged(
-                mTargetStateOverrideForStateTransition != null
-                        ? mTargetStateOverrideForStateTransition
-                        : mLauncher.getStateManager().getState(),
-                this::getCurrentIconAlignmentRatioForLauncherState);
+        onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioForLauncherState);
     }
 
     private void onIconAlignmentRatioChanged() {
-        onIconAlignmentRatioChanged(mTargetStateOverride != null ? mTargetStateOverride
-                : mLauncher.getStateManager().getState(), this::getCurrentIconAlignmentRatio);
+        onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatio);
     }
 
-    private void onIconAlignmentRatioChanged(LauncherState state,
-            Supplier<Float> alignmentSupplier) {
+    private void onIconAlignmentRatioChanged(Supplier<Float> alignmentSupplier) {
         if (mControllers == null) {
             return;
         }
@@ -341,7 +315,8 @@
 
         mTaskbarBackgroundAlpha.updateValue(1 - alignment);
 
-        setIconAlpha(state, alignment);
+        // Switch taskbar and hotseat in last frame
+        setTaskbarViewVisible(alignment < 1);
     }
 
     private float getCurrentIconAlignmentRatio() {
@@ -352,15 +327,6 @@
         return mIconAlignmentForLauncherState.value;
     }
 
-    private void setIconAlpha(LauncherState state, float progress) {
-        if ((state.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-            // If the hotseat icons are visible, then switch taskbar in last frame
-            setTaskbarViewVisible(progress < 1);
-        } else {
-            mIconAlphaForHome.setValue(1 - progress);
-        }
-    }
-
     private void setTaskbarViewVisible(boolean isVisible) {
         mIconAlphaForHome.setValue(isVisible ? 1 : 0);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index a65cc56..3cdcdf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -29,8 +29,8 @@
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
 import android.net.Uri;
+import android.os.Handler;
 import android.provider.Settings;
-import android.util.Log;
 import android.view.Display;
 
 import androidx.annotation.NonNull;
@@ -93,7 +93,8 @@
         Display display =
                 service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
         mContext = service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null);
-        mNavButtonController = new TaskbarNavButtonController(service);
+        mNavButtonController = new TaskbarNavButtonController(service,
+                SystemUiProxy.INSTANCE.get(mContext), new Handler());
         mUserSetupCompleteListener = isUserSetupComplete -> recreateTaskbar();
         mComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
@@ -106,6 +107,11 @@
                 if ((configDiff & configsRequiringRecreate) != 0) {
                     // Color has changed, recreate taskbar to reload background color & icons.
                     recreateTaskbar();
+                } else {
+                    // Config change might be handled without re-creating the taskbar
+                    if (mTaskbarActivityContext != null) {
+                        mTaskbarActivityContext.onConfigurationChanged(configDiff);
+                    }
                 }
                 mOldConfig = newConfig;
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 6dcfe56..37a9b5e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -184,9 +184,12 @@
         }
         mContainer.updateHotseatItems(hotseatItemInfos);
 
-        mControllers.taskbarStashController.updateStateForFlag(
-                TaskbarStashController.FLAG_STASHED_IN_APP_EMPTY, isHotseatEmpty);
-        mControllers.taskbarStashController.applyState();
+        final boolean finalIsHotseatEmpty = isHotseatEmpty;
+        mControllers.runAfterInit(() -> {
+            mControllers.taskbarStashController.updateStateForFlag(
+                    TaskbarStashController.FLAG_STASHED_IN_APP_EMPTY, finalIsHotseatEmpty);
+            mControllers.taskbarStashController.applyState();
+        });
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index ae23eda..d233365 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -19,8 +19,10 @@
 
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import android.os.Bundle;
+import android.os.Handler;
 
 import androidx.annotation.IntDef;
 
@@ -40,6 +42,13 @@
  */
 public class TaskbarNavButtonController {
 
+    /** Allow some time in between the long press for back and recents. */
+    static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200;
+    static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100;
+
+    private long mLastScreenPinLongPress;
+    private boolean mScreenPinned;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             BUTTON_BACK,
@@ -57,10 +66,20 @@
     static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1;
     static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1;
 
-    private final TouchInteractionService mService;
+    private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS;
+    private int mLongPressedButtons = 0;
 
-    public TaskbarNavButtonController(TouchInteractionService service) {
+    private final TouchInteractionService mService;
+    private final SystemUiProxy mSystemUiProxy;
+    private final Handler mHandler;
+
+    private final Runnable mResetLongPress = this::resetScreenUnpin;
+
+    public TaskbarNavButtonController(TouchInteractionService service,
+            SystemUiProxy systemUiProxy, Handler handler) {
         mService = service;
+        mSystemUiProxy = systemUiProxy;
+        mHandler = handler;
     }
 
     public void onButtonClick(@TaskbarButton int buttonType) {
@@ -72,13 +91,13 @@
                 navigateHome();
                 break;
             case BUTTON_RECENTS:
-                navigateToOverview();;
+                navigateToOverview();
                 break;
             case BUTTON_IME_SWITCH:
                 showIMESwitcher();
                 break;
             case BUTTON_A11Y:
-                notifyImeClick(false /* longClick */);
+                notifyA11yClick(false /* longClick */);
                 break;
         }
     }
@@ -89,46 +108,98 @@
                 startAssistant();
                 return true;
             case BUTTON_A11Y:
-                notifyImeClick(true /* longClick */);
+                notifyA11yClick(true /* longClick */);
                 return true;
             case BUTTON_BACK:
-            case BUTTON_IME_SWITCH:
             case BUTTON_RECENTS:
+                mLongPressedButtons |= buttonType;
+                return determineScreenUnpin();
+            case BUTTON_IME_SWITCH:
             default:
                 return false;
         }
     }
 
+    /**
+     * Checks if the user has long pressed back and recents buttons
+     * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms
+     * If so, then requests the system to turn off screen pinning.
+     *
+     * @return true if the long press is a valid user action in attempting to unpin an app
+     *         Will always return {@code false} when screen pinning is not active.
+     *         NOTE: Returning true does not mean that screen pinning has stopped
+     */
+    private boolean determineScreenUnpin() {
+        long timeNow = System.currentTimeMillis();
+        if (!mScreenPinned) {
+            return false;
+        }
+
+        if (mLastScreenPinLongPress == 0) {
+            // First button long press registered, just mark time and wait for second button press
+            mLastScreenPinLongPress = System.currentTimeMillis();
+            mHandler.postDelayed(mResetLongPress, SCREEN_PIN_LONG_PRESS_RESET);
+            return true;
+        }
+
+        if ((timeNow - mLastScreenPinLongPress) > SCREEN_PIN_LONG_PRESS_THRESHOLD) {
+            // Too long in-between presses, reset the clock
+            resetScreenUnpin();
+            return false;
+        }
+
+        if ((mLongPressedButtons & SCREEN_UNPIN_COMBO) == SCREEN_UNPIN_COMBO) {
+            // Hooray! They did it (finally...)
+            mSystemUiProxy.stopScreenPinning();
+            mHandler.removeCallbacks(mResetLongPress);
+            resetScreenUnpin();
+        }
+        return true;
+    }
+
+    private void resetScreenUnpin() {
+        mLongPressedButtons = 0;
+        mLastScreenPinLongPress = 0;
+    }
+
+    public void updateSysuiFlags(int sysuiFlags) {
+        mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
+    }
+
     private void navigateHome() {
         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME);
     }
 
     private void navigateToOverview() {
+        if (mScreenPinned) {
+            return;
+        }
         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE);
     }
 
     private void executeBack() {
-        SystemUiProxy.INSTANCE.getNoCreate().onBackPressed();
+        mSystemUiProxy.onBackPressed();
     }
 
     private void showIMESwitcher() {
-        SystemUiProxy.INSTANCE.getNoCreate().onImeSwitcherPressed();
+        mSystemUiProxy.onImeSwitcherPressed();
     }
 
-    private void notifyImeClick(boolean longClick) {
-        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
+    private void notifyA11yClick(boolean longClick) {
         if (longClick) {
-            systemUiProxy.notifyAccessibilityButtonLongClicked();
+            mSystemUiProxy.notifyAccessibilityButtonLongClicked();
         } else {
-            systemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
+            mSystemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
         }
     }
 
     private void startAssistant() {
+        if (mScreenPinned) {
+            return;
+        }
         Bundle args = new Bundle();
         args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
-        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
-        systemUiProxy.startAssistant(args);
+        mSystemUiProxy.startAssistant(args);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 60842c8..8965dc4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -19,6 +19,7 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_SHOW;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import android.animation.Animator;
@@ -48,11 +49,13 @@
     public static final int FLAG_STASHED_IN_APP_PINNED = 1 << 2; // app pinning
     public static final int FLAG_STASHED_IN_APP_EMPTY = 1 << 3; // no hotseat icons
     public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 4; // setup wizard and AllSetActivity
-    public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 5;
+    public static final int FLAG_STASHED_IN_APP_IME = 1 << 5; // IME is visible
+    public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 6;
 
     // If we're in an app and any of these flags are enabled, taskbar should be stashed.
     public 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_PINNED | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP
+            | FLAG_STASHED_IN_APP_IME;
 
     /**
      * How long to stash/unstash when manually invoked via long press.
@@ -60,6 +63,11 @@
     public static final long TASKBAR_STASH_DURATION = 300;
 
     /**
+     * How long to stash/unstash when keyboard is appearing/disappearing.
+     */
+    private static final long TASKBAR_STASH_DURATION_FOR_IME = 80;
+
+    /**
      * The scale TaskbarView animates to when being stashed.
      */
     private static final float STASHED_TASKBAR_SCALE = 0.5f;
@@ -100,6 +108,7 @@
     private TaskbarControllers mControllers;
     // Taskbar background properties.
     private AnimatedFloat mTaskbarBackgroundOffset;
+    private AnimatedFloat mTaskbarImeBgAlpha;
     // TaskbarView icon properties.
     private AlphaProperty mIconAlphaForStash;
     private AnimatedFloat mIconScaleForStash;
@@ -113,6 +122,8 @@
     private int mState;
 
     private @Nullable AnimatorSet mAnimator;
+    private boolean mIsSystemGestureInProgress;
+    private boolean mIsImeShowing;
 
     // Evaluate whether the handle should be stashed
     private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
@@ -137,6 +148,7 @@
 
         TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController;
         mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset();
+        mTaskbarImeBgAlpha = dragLayerController.getImeBgTaskbar();
 
         TaskbarViewController taskbarViewController = controllers.taskbarViewController;
         mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty(
@@ -271,17 +283,21 @@
      * Create a stash animation and save to {@link #mAnimator}.
      * @param isStashed whether it's a stash animation or an unstash animation
      * @param duration duration of the animation
+     * @param startDelay how many milliseconds to delay the animation after starting it.
      */
-    private void createAnimToIsStashed(boolean isStashed, long duration) {
+    private void createAnimToIsStashed(boolean isStashed, long duration, long startDelay) {
         if (mAnimator != null) {
             mAnimator.cancel();
         }
         mAnimator = new AnimatorSet();
 
         if (!supportsVisualStashing()) {
-            // Just hide/show the icons instead of stashing into a handle.
+            // Just hide/show the icons and background instead of stashing into a handle.
             mAnimator.play(mIconAlphaForStash.animateToValue(isStashed ? 0 : 1)
                     .setDuration(duration));
+            mAnimator.play(mTaskbarImeBgAlpha.animateToValue(
+                    hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration));
+            mAnimator.setStartDelay(startDelay);
             mAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -332,11 +348,8 @@
             );
         }
 
-        Animator stashedHandleRevealAnim = mControllers.stashedHandleViewController
-                .createRevealAnimToIsStashed(isStashed);
-        if (stashedHandleRevealAnim != null) {
-            fullLengthAnimatorSet.play(stashedHandleRevealAnim);
-        }
+        fullLengthAnimatorSet.play(mControllers.stashedHandleViewController
+                .createRevealAnimToIsStashed(isStashed));
         // Return the stashed handle to its default scale in case it was changed as part of the
         // feedforward hint. Note that the reveal animation above also visually scales it.
         fullLengthAnimatorSet.play(mTaskbarStashedHandleHintScale.animateToValue(1f));
@@ -348,6 +361,7 @@
 
         mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
                 secondHalfAnimatorSet);
+        mAnimator.setStartDelay(startDelay);
         mAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -406,6 +420,10 @@
         mStatePropertyHolder.setState(mState, duration, true);
     }
 
+    public void applyState(long duration, long startDelay) {
+        mStatePropertyHolder.setState(mState, duration, startDelay, true);
+    }
+
     public Animator applyStateWithoutStart() {
         return applyStateWithoutStart(TASKBAR_STASH_DURATION);
     }
@@ -414,11 +432,50 @@
         return mStatePropertyHolder.setState(mState, duration, false);
     }
 
+    /**
+     * Should be called when a system gesture starts and settles, so we can defer updating
+     * FLAG_STASHED_IN_APP_IME until after the gesture transition completes.
+     */
+    public void setSystemGestureInProgress(boolean inProgress) {
+        mIsSystemGestureInProgress = inProgress;
+        // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress.
+        if (!mIsSystemGestureInProgress) {
+            updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing);
+            applyState(TASKBAR_STASH_DURATION_FOR_IME, getTaskbarStashStartDelayForIme());
+        }
+    }
+
+    /**
+     * When hiding the IME, delay the unstash animation to align with the end of the transition.
+     */
+    private long getTaskbarStashStartDelayForIme() {
+        if (mIsImeShowing) {
+            // Only delay when IME is exiting, not entering.
+            return 0;
+        }
+        // This duration is based on input_method_extract_exit.xml.
+        long imeExitDuration = mControllers.taskbarActivityContext.getResources()
+                .getInteger(android.R.integer.config_shortAnimTime);
+        return imeExitDuration - TASKBAR_STASH_DURATION_FOR_IME;
+    }
+
     /** Called when some system ui state has changed. (See SYSUI_STATE_... in QuickstepContract) */
     public void updateStateForSysuiFlags(int systemUiStateFlags, boolean skipAnim) {
+        long animDuration = TASKBAR_STASH_DURATION;
+        long startDelay = 0;
+
         updateStateForFlag(FLAG_STASHED_IN_APP_PINNED,
                 hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING));
-        applyState(skipAnim ? 0 : TASKBAR_STASH_DURATION);
+
+        // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress.
+        mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING);
+        if (!mIsSystemGestureInProgress) {
+            updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing);
+            animDuration = TASKBAR_STASH_DURATION_FOR_IME;
+            startDelay = getTaskbarStashStartDelayForIme();
+        }
+
+        applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
     }
 
     /**
@@ -474,16 +531,34 @@
             mStashCondition = stashCondition;
         }
 
+        /**
+         * @see #setState(int, long, long, boolean) with a default startDelay = 0.
+         */
         public Animator setState(int flags, long duration, boolean start) {
+            return setState(flags, duration, 0 /* startDelay */, start);
+        }
+
+        /**
+         * Applies the latest state, potentially calling onStateChangeApplied() and creating a new
+         * animation (stored in mAnimator) which is started if {@param start} is true.
+         * @param flags The latest flags to apply (see the top of this file).
+         * @param duration The length of the animation.
+         * @param startDelay How long to delay the animation after calling start().
+         * @param start Whether to start mAnimator immediately.
+         * @return mAnimator if mIsStashed changed, else null.
+         */
+        public Animator setState(int flags, long duration, long startDelay, boolean start) {
+            int changedFlags = mPrevFlags ^ flags;
             if (mPrevFlags != flags) {
-                int changedFlags = mPrevFlags ^ flags;
                 onStateChangeApplied(changedFlags);
                 mPrevFlags = flags;
             }
             boolean isStashed = mStashCondition.test(flags);
             if (mIsStashed != isStashed) {
                 mIsStashed = isStashed;
-                createAnimToIsStashed(mIsStashed, duration);
+
+                // This sets mAnimator.
+                createAnimToIsStashed(mIsStashed, duration, startDelay);
                 if (start) {
                     mAnimator.start();
                 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index d8360e0..f6bc785 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -16,6 +16,9 @@
 package com.android.launcher3.taskbar;
 
 import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.CallSuper;
 
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -29,9 +32,18 @@
 
     public static final TaskbarUIController DEFAULT = new TaskbarUIController();
 
-    protected void init(TaskbarControllers taskbarControllers) { }
+    // Initialized in init.
+    protected TaskbarControllers mControllers;
 
-    protected void onDestroy() { }
+    @CallSuper
+    protected void init(TaskbarControllers taskbarControllers) {
+        mControllers = taskbarControllers;
+    }
+
+    @CallSuper
+    protected void onDestroy() {
+        mControllers = null;
+    }
 
     protected boolean isTaskbarTouchable() {
         return true;
@@ -46,4 +58,16 @@
     }
 
     public void onTaskbarIconLaunched(WorkspaceItemInfo item) { }
+
+    public View getRootView() {
+        return mControllers.taskbarActivityContext.getDragLayer();
+    }
+
+    /**
+     * Called when swiping from the bottom nav region in fully gestural mode.
+     * @param inProgress True if the animation started, false if we just settled on an end target.
+     */
+    public void setSystemGestureInProgress(boolean inProgress) {
+        mControllers.taskbarStashController.setSystemGestureInProgress(inProgress);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index c47bde9..445a23b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -45,12 +45,11 @@
     private static final Runnable NO_OP = () -> { };
 
     public static final int ALPHA_INDEX_HOME = 0;
-    public static final int ALPHA_INDEX_IME = 1;
-    public static final int ALPHA_INDEX_KEYGUARD = 2;
-    public static final int ALPHA_INDEX_STASH = 3;
-    public static final int ALPHA_INDEX_RECENTS_DISABLED = 4;
-    public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 5;
-    private static final int NUM_ALPHA_CHANNELS = 6;
+    public static final int ALPHA_INDEX_KEYGUARD = 1;
+    public static final int ALPHA_INDEX_STASH = 2;
+    public static final int ALPHA_INDEX_RECENTS_DISABLED = 3;
+    public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4;
+    private static final int NUM_ALPHA_CHANNELS = 5;
 
     private final TaskbarActivityContext mActivity;
     private final TaskbarView mTaskbarView;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 2fa8b07..c85b256 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
@@ -35,6 +34,7 @@
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.util.FloatProperty;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 
@@ -73,8 +73,6 @@
         getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
         RECENTS_GRID_PROGRESS.set(mRecentsView,
                 state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
-
-        applySplitScrollOffset(state);
     }
 
     @Override
@@ -104,11 +102,13 @@
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
         PagedOrientationHandler orientationHandler =
                 ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
-        FloatProperty taskViewsFloat = orientationHandler.getSplitSelectTaskOffset(
-                TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
-                mLauncher.getDeviceProfile());
-        setter.setFloat(mRecentsView, taskViewsFloat,
+        Pair<FloatProperty, FloatProperty> taskViewsFloat =
+                orientationHandler.getSplitSelectTaskOffset(
+                        TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
+                        mLauncher.getDeviceProfile());
+        setter.setFloat(mRecentsView, taskViewsFloat.first,
                 toState.getSplitSelectTranslation(mLauncher), LINEAR);
+        setter.setFloat(mRecentsView, taskViewsFloat.second, 0, LINEAR);
 
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
@@ -120,16 +120,6 @@
         boolean showAsGrid = toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile());
         setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS, showAsGrid ? 1f : 0f,
                 showAsGrid ? INSTANT : FINAL_FRAME);
-
-        applySplitScrollOffset(toState);
-    }
-
-    private void applySplitScrollOffset(@NonNull final LauncherState state) {
-        if (state == OVERVIEW_SPLIT_SELECT) {
-            mRecentsView.applySplitPrimaryScrollOffset();
-        } else {
-            mRecentsView.resetSplitPrimaryScrollOffset();
-        }
     }
 
     abstract FloatProperty getTaskModalnessProperty();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 1f744e1..19897a1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -63,6 +63,15 @@
         }
         setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
+        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+        // DepthController to prevent optimizations which might occlude the layers behind
+        mLauncher.getDepthController().setHasContentBehindLauncher(state.overviewUi);
+
+        if (isSplitSelectionState(state)) {
+            mRecentsView.applySplitPrimaryScrollOffset();
+        } else {
+            mRecentsView.resetSplitPrimaryScrollOffset();
+        }
     }
 
     @Override
@@ -78,13 +87,20 @@
             builder.addListener(
                     AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals));
         }
+        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+        // DepthController to prevent optimizations which might occlude the layers behind
+        builder.addListener(AnimatorListeners.forSuccessCallback(() ->
+                mLauncher.getDepthController().setHasContentBehindLauncher(toState.overviewUi)));
 
         // Create or dismiss split screen select animations
         LauncherState currentState = mLauncher.getStateManager().getState();
         if (isSplitSelectionState(toState) && !isSplitSelectionState(currentState)) {
             builder.add(mRecentsView.createSplitSelectInitAnimation().buildAnim());
-        } else if (!isSplitSelectionState(toState) && isSplitSelectionState(currentState)) {
-            builder.add(mRecentsView.cancelSplitSelect(true).buildAnim());
+        }
+        if (isSplitSelectionState(toState)) {
+            mRecentsView.applySplitPrimaryScrollOffset();
+        } else {
+            mRecentsView.resetSplitPrimaryScrollOffset();
         }
 
         setAlphas(builder, config, toState);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index d396018..a4eff87 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -21,13 +21,11 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.SystemProperties;
-import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.util.LayoutUtils;
@@ -66,10 +64,7 @@
     @Override
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
         RecentsView recentsView = launcher.getOverviewPanel();
-        Workspace workspace = launcher.getWorkspace();
-        View workspacePage = workspace.getPageAt(workspace.getCurrentPage());
-        float workspacePageWidth = workspacePage != null && workspacePage.getWidth() != 0
-                ? workspacePage.getWidth() : launcher.getDeviceProfile().availableWidthPx;
+        float workspacePageWidth = launcher.getDeviceProfile().getWorkspaceWidth();
         recentsView.getTaskSize(sTempRect);
         float scale = (float) sTempRect.width() / workspacePageWidth;
         float parallaxFactor = 0.5f;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
index 106375a..4f5f27a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -31,11 +31,6 @@
     }
 
     @Override
-    public void onBackPressed(Launcher launcher) {
-        launcher.getStateManager().goToState(OVERVIEW);
-    }
-
-    @Override
     public int getVisibleElements(Launcher launcher) {
         return SPLIT_PLACHOLDER_VIEW;
     }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 45b2081..c9cbba1 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -121,8 +121,8 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.function.Consumer;
 
 /**
@@ -687,14 +687,17 @@
     }
 
     private void onAnimatorPlaybackControllerCreated(AnimatorControllerWithResistance anim) {
+        boolean isFirstCreation = mLauncherTransitionController == null;
         mLauncherTransitionController = anim;
-        mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, () -> {
-            // Wait until the gesture is started (touch slop was passed) to start in sync with
-            // mWindowTransitionController. This ensures we don't hide the taskbar background when
-            // long pressing to stash it, for instance.
-            mLauncherTransitionController.getNormalController().dispatchOnStart();
-            updateLauncherTransitionProgress();
-        });
+        if (isFirstCreation) {
+            mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, () -> {
+                // Wait until the gesture is started (touch slop was passed) to start in sync with
+                // mWindowTransitionController. This ensures we don't hide the taskbar background
+                // when long pressing to stash it, for instance.
+                mLauncherTransitionController.getNormalController().dispatchOnStart();
+                updateLauncherTransitionProgress();
+            });
+        }
     }
 
     public Intent getLaunchIntent() {
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index e15aa92..a566765 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -30,6 +30,7 @@
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -398,6 +399,11 @@
      * (This is a hack to ensure Taskbar draws its background first to avoid flickering.)
      */
     public @Nullable View onSettledOnEndTarget(GestureState.GestureEndTarget endTarget) {
+        TaskbarUIController taskbarUIController = getTaskbarController();
+        if (taskbarUIController != null) {
+            taskbarUIController.setSystemGestureInProgress(false);
+            return taskbarUIController.getRootView();
+        }
         return null;
     }
 
@@ -533,6 +539,16 @@
             pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
                     recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
             pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
+
+            pa.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    TaskbarUIController taskbarUIController = getTaskbarController();
+                    if (taskbarUIController != null) {
+                        taskbarUIController.setSystemGestureInProgress(true);
+                    }
+                }
+            });
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index aa9435b..719c2ae 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -25,12 +25,10 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.content.Context;
 import android.graphics.Rect;
 import android.view.MotionEvent;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -132,19 +130,6 @@
                 pa.addFloat(getDepthController(),
                         new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
                         fromDepthRatio, toDepthRatio, LINEAR);
-
-                pa.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        LauncherTaskbarUIController taskbarUIController =
-                                activity.getTaskbarUIController();
-                        if (taskbarUIController != null) {
-                            // Launcher's ScrimView will draw the background throughout the gesture.
-                            taskbarUIController.forceHideBackground(true);
-                        }
-                    }
-                });
-
             }
         };
 
@@ -366,16 +351,4 @@
                 return NORMAL;
         }
     }
-
-    @Override
-    public View onSettledOnEndTarget(@Nullable GestureEndTarget endTarget) {
-        View superRet = super.onSettledOnEndTarget(endTarget);
-        LauncherTaskbarUIController taskbarUIController = getTaskbarController();
-        if (taskbarUIController != null) {
-            // Start drawing taskbar's background again since launcher might stop drawing.
-            taskbarUIController.forceHideBackground(false);
-            return taskbarUIController.getRootView();
-        }
-        return superRet;
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 097850f..c5f4a53 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -36,7 +36,6 @@
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.StagedSplitBounds;
 
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.function.Consumer;
@@ -220,26 +219,6 @@
         return newTasks;
     }
 
-    public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "RecentTasksList:");
-        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"));
-        }
-        writer.println(prefix + "  ]");
-        int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks =
-                mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
-        writer.println(prefix + "  rawTasks=[");
-        for (GroupedRecentTaskInfo task : rawTasks) {
-            writer.println(prefix + "    t1=" + task.mTaskInfo1.taskId
-                    + " t2=" + (task.mTaskInfo2 != null ? task.mTaskInfo2.taskId : "-1"));
-        }
-        writer.println(prefix + "  ]");
-    }
-
     private static class TaskLoadResult extends ArrayList<GroupTask> {
 
         final int mRequestId;
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 5d77a6e..e539a8c 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,7 +43,6 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -221,11 +220,6 @@
         mThumbnailChangeListeners.remove(listener);
     }
 
-    public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "RecentsModel:");
-        mTaskList.dump("  ", writer);
-    }
-
     /**
      * Listener for receiving various task properties changes
      */
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index c8abd14..6c623bc 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -83,16 +83,14 @@
         MAIN_EXECUTOR.execute(() -> clearProxy());
     };
 
-    // Save the listeners passed into the proxy since OverviewProxyService may not have been bound
-    // yet, and we'll need to set/register these listeners with SysUI when they do.  Note that it is
-    // up to the caller to clear the listeners to prevent leaks as these can be held indefinitely
-    // in case SysUI needs to rebind.
-    private IPipAnimationListener mPipAnimationListener;
-    private ISplitScreenListener mSplitScreenListener;
-    private IStartingWindowListener mStartingWindowListener;
-    private ISmartspaceCallback mSmartspaceCallback;
-    private IRecentTasksListener mRecentTasksListener;
-    private final ArrayList<RemoteTransitionCompat> mRemoteTransitions = new ArrayList<>();
+    // Save the listeners passed into the proxy since when set/register these listeners,
+    // setProxy may not have been called, eg. OverviewProxyService is not connected yet.
+    private IPipAnimationListener mPendingPipAnimationListener;
+    private ISplitScreenListener mPendingSplitScreenListener;
+    private IStartingWindowListener mPendingStartingWindowListener;
+    private ISmartspaceCallback mPendingSmartspaceCallback;
+    private IRecentTasksListener mPendingRecentTasksListener;
+    private final ArrayList<RemoteTransitionCompat> mPendingRemoteTransitions = new ArrayList<>();
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -169,23 +167,29 @@
         mRecentTasks = recentTasks;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
-        if (mPipAnimationListener != null && mPip != null) {
-            setPinnedStackAnimationListener(mPipAnimationListener);
+        if (mPendingPipAnimationListener != null && mPip != null) {
+            setPinnedStackAnimationListener(mPendingPipAnimationListener);
+            mPendingPipAnimationListener = null;
         }
-        if (mSplitScreenListener != null && mSplitScreen != null) {
-            registerSplitScreenListener(mSplitScreenListener);
+        if (mPendingSplitScreenListener != null && mSplitScreen != null) {
+            registerSplitScreenListener(mPendingSplitScreenListener);
+            mPendingSplitScreenListener = null;
         }
-        if (mStartingWindowListener != null && mStartingWindow != null) {
-            setStartingWindowListener(mStartingWindowListener);
+        if (mPendingStartingWindowListener != null && mStartingWindow != null) {
+            setStartingWindowListener(mPendingStartingWindowListener);
+            mPendingStartingWindowListener = null;
         }
-        if (mSmartspaceCallback != null && mSmartspaceTransitionController != null) {
-            setSmartspaceCallback(mSmartspaceCallback);
+        if (mPendingSmartspaceCallback != null && mSmartspaceTransitionController != null) {
+            setSmartspaceCallback(mPendingSmartspaceCallback);
+            mPendingSmartspaceCallback = null;
         }
-        for (int i = mRemoteTransitions.size() - 1; i >= 0; --i) {
-            registerRemoteTransition(mRemoteTransitions.get(i));
+        for (int i = mPendingRemoteTransitions.size() - 1; i >= 0; --i) {
+            registerRemoteTransition(mPendingRemoteTransitions.get(i));
         }
-        if (mRecentTasksListener != null && mRecentTasks != null) {
-            registerRecentTasksListener(mRecentTasksListener);
+        mPendingRemoteTransitions.clear();
+        if (mPendingRecentTasksListener != null && mRecentTasks != null) {
+            registerRecentTasksListener(mPendingRecentTasksListener);
+            mPendingRecentTasksListener = null;
         }
 
         if (mPendingSetNavButtonAlpha != null) {
@@ -509,8 +513,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
             }
+        } else {
+            mPendingPipAnimationListener = listener;
         }
-        mPipAnimationListener = listener;
     }
 
     public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
@@ -548,8 +553,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerSplitScreenListener");
             }
+        } else {
+            mPendingSplitScreenListener = listener;
         }
-        mSplitScreenListener = listener;
     }
 
     public void unregisterSplitScreenListener(ISplitScreenListener listener) {
@@ -560,17 +566,17 @@
                 Log.w(TAG, "Failed call unregisterSplitScreenListener");
             }
         }
-        mSplitScreenListener = null;
+        mPendingSplitScreenListener = null;
     }
 
     /** Start multiple tasks in split-screen simultaneously. */
     public void startTasks(int mainTaskId, Bundle mainOptions, int sideTaskId, Bundle sideOptions,
-            @SplitConfigurationOptions.StagePosition int sidePosition,
+            @SplitConfigurationOptions.StagePosition int sidePosition, float splitRatio,
             RemoteTransitionCompat remoteTransition) {
         if (mSystemUiProxy != null) {
             try {
                 mSplitScreen.startTasks(mainTaskId, mainOptions, sideTaskId, sideOptions,
-                        sidePosition, remoteTransition.getTransition());
+                        sidePosition, splitRatio, remoteTransition.getTransition());
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startTask");
             }
@@ -582,22 +588,22 @@
      */
     public void startTasksWithLegacyTransition(int mainTaskId, Bundle mainOptions, int sideTaskId,
             Bundle sideOptions, @SplitConfigurationOptions.StagePosition int sidePosition,
-            RemoteAnimationAdapter adapter) {
+            float splitRatio, RemoteAnimationAdapter adapter) {
         if (mSystemUiProxy != null) {
             try {
                 mSplitScreen.startTasksWithLegacyTransition(mainTaskId, mainOptions, sideTaskId,
-                        sideOptions, sidePosition, adapter);
+                        sideOptions, sidePosition, splitRatio, adapter);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startTasksWithLegacyTransition");
             }
         }
     }
 
-    public void startShortcut(String packageName, String shortcutId, int stage, int position,
+    public void startShortcut(String packageName, String shortcutId, int position,
             Bundle options, UserHandle user) {
         if (mSplitScreen != null) {
             try {
-                mSplitScreen.startShortcut(packageName, shortcutId, stage, position, options,
+                mSplitScreen.startShortcut(packageName, shortcutId, position, options,
                         user);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startShortcut");
@@ -605,11 +611,11 @@
         }
     }
 
-    public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
+    public void startIntent(PendingIntent intent, Intent fillInIntent, int position,
             Bundle options) {
         if (mSplitScreen != null) {
             try {
-                mSplitScreen.startIntent(intent, fillInIntent, stage, position, options);
+                mSplitScreen.startIntent(intent, fillInIntent, position, options);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startIntent");
             }
@@ -681,8 +687,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
+        } else {
+            mPendingRemoteTransitions.add(remoteTransition);
         }
-        mRemoteTransitions.add(remoteTransition);
     }
 
     public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
@@ -693,7 +700,7 @@
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
         }
-        mRemoteTransitions.remove(remoteTransition);
+        mPendingRemoteTransitions.remove(remoteTransition);
     }
 
     //
@@ -710,8 +717,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
+        } else {
+            mPendingStartingWindowListener = listener;
         }
-        mStartingWindowListener = listener;
     }
 
     //
@@ -725,8 +733,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
+        } else {
+            mPendingSmartspaceCallback = callback;
         }
-        mSmartspaceCallback = callback;
     }
 
     //
@@ -740,8 +749,9 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRecentTasksListener", e);
             }
+        } else {
+            mPendingRecentTasksListener = listener;
         }
-        mRecentTasksListener = listener;
     }
 
     public void unregisterRecentTasksListener(IRecentTasksListener listener) {
@@ -752,7 +762,7 @@
                 Log.w(TAG, "Failed call unregisterRecentTasksListener");
             }
         }
-        mRecentTasksListener = null;
+        mPendingRecentTasksListener = null;
     }
 
     public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index f6f2cf9..2c7fd71 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -973,7 +973,6 @@
             pw.println("  resumed=" + resumed);
             pw.println("  mConsumer=" + mConsumer.getName());
             ActiveGestureLog.INSTANCE.dump("", pw);
-            RecentsModel.INSTANCE.get(this).dump("", pw);
             pw.println("ProtoTrace:");
             pw.println("  file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 95095fa..169b208 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -16,7 +16,6 @@
 package com.android.quickstep.fallback;
 
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
-import static com.android.quickstep.ViewUtils.postFrameDrawn;
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
 import static com.android.quickstep.fallback.RecentsState.HOME;
 import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
@@ -32,18 +31,19 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 
@@ -74,6 +74,7 @@
     @Override
     public void startHome() {
         mActivity.startHome();
+        AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
 
     /**
@@ -207,10 +208,6 @@
 
     @Override
     public void onStateTransitionStart(RecentsState toState) {
-        if (toState == HOME) {
-            // Clean-up logic that occurs when recents is no longer in use/visible.
-            reset();
-        }
         setOverviewStateEnabled(true);
         setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
         setOverviewFullscreenEnabled(toState.isFullScreen());
@@ -219,13 +216,17 @@
 
     @Override
     public void onStateTransitionComplete(RecentsState finalState) {
+        if (finalState == HOME) {
+            // Clean-up logic that occurs when recents is no longer in use/visible.
+            reset();
+        }
         boolean isOverlayEnabled = finalState == DEFAULT || finalState == MODAL_TASK;
         setOverlayEnabled(isOverlayEnabled);
         setFreezeViewVisibility(false);
 
         if (isOverlayEnabled) {
-            postFrameDrawn(this, () -> runActionOnRemoteHandles(remoteTargetHandle ->
-                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)));
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java b/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
new file mode 100644
index 0000000..861ff96
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 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 android.annotation.CallSuper;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator;
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Animation that moves launcher icons and widgets from center to the sides (final position)
+ */
+public abstract class BaseUnfoldMoveFromCenterAnimator implements TransitionProgressListener {
+
+    private final UnfoldMoveFromCenterAnimator mMoveFromCenterAnimation;
+
+    private final Map<ViewGroup, Boolean> mOriginalClipToPadding = new HashMap<>();
+    private final Map<ViewGroup, Boolean> mOriginalClipChildren = new HashMap<>();
+
+    public BaseUnfoldMoveFromCenterAnimator(WindowManager windowManager) {
+        mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
+                new LauncherViewsMoveFromCenterTranslationApplier());
+    }
+
+    @CallSuper
+    @Override
+    public void onTransitionStarted() {
+        mMoveFromCenterAnimation.updateDisplayProperties();
+        onPrepareViewsForAnimation();
+        onTransitionProgress(0f);
+    }
+
+    @CallSuper
+    @Override
+    public void onTransitionProgress(float progress) {
+        mMoveFromCenterAnimation.onTransitionProgress(progress);
+    }
+
+    @CallSuper
+    @Override
+    public void onTransitionFinished() {
+        mMoveFromCenterAnimation.onTransitionFinished();
+        mMoveFromCenterAnimation.clearRegisteredViews();
+
+        mOriginalClipChildren.clear();
+        mOriginalClipToPadding.clear();
+    }
+
+    protected void onPrepareViewsForAnimation() {
+
+    }
+
+    protected void registerViewForAnimation(View view) {
+        mMoveFromCenterAnimation.registerViewForAnimation(view);
+    }
+
+    protected void disableClipping(ViewGroup view) {
+        mOriginalClipToPadding.put(view, view.getClipToPadding());
+        mOriginalClipChildren.put(view, view.getClipChildren());
+        view.setClipToPadding(false);
+        view.setClipChildren(false);
+    }
+
+    protected void restoreClipping(ViewGroup view) {
+        final Boolean originalClipToPadding = mOriginalClipToPadding.get(view);
+        if (originalClipToPadding != null) {
+            view.setClipToPadding(originalClipToPadding);
+        }
+        final Boolean originalClipChildren = mOriginalClipChildren.get(view);
+        if (originalClipChildren != null) {
+            view.setClipChildren(originalClipChildren);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
index b39412b..6b6bd6a 100644
--- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
+++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
@@ -18,14 +18,17 @@
 import static com.android.launcher3.Utilities.comp;
 
 import android.annotation.Nullable;
-import android.view.ViewTreeObserver;
 import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.core.view.OneShotPreDrawListener;
 
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.util.HorizontalInsettableView;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
+import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
 
 /**
@@ -43,6 +46,7 @@
     private HorizontalInsettableView mQsbInsettable;
 
     private final ScopedUnfoldTransitionProgressProvider mProgressProvider;
+    private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider;
 
     public LauncherUnfoldAnimationController(
             Launcher launcher,
@@ -51,10 +55,19 @@
         mLauncher = launcher;
         mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
                 unfoldTransitionProgressProvider);
+        mNaturalOrientationProgressProvider = new NaturalRotationUnfoldProgressProvider(launcher,
+                WindowManagerGlobal.getWindowManagerService(), mProgressProvider);
+        mNaturalOrientationProgressProvider.init();
 
+        // Animated in all orientations
         mProgressProvider.addCallback(new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
                 windowManager));
-        mProgressProvider.addCallback(new QsbAnimationListener());
+
+        // Animated only in natural orientation
+        mNaturalOrientationProgressProvider
+                .addCallback(new QsbAnimationListener());
+        mNaturalOrientationProgressProvider
+                .addCallback(new UnfoldMoveFromCenterHotseatAnimator(launcher, windowManager));
     }
 
     /**
@@ -66,17 +79,8 @@
             mQsbInsettable = (HorizontalInsettableView) hotseat.getQsb();
         }
 
-        final ViewTreeObserver obs = mLauncher.getWorkspace().getViewTreeObserver();
-        obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-            @Override
-            public boolean onPreDraw() {
-                if (obs.isAlive()) {
-                    mProgressProvider.setReadyToHandleTransition(true);
-                    obs.removeOnPreDrawListener(this);
-                }
-                return true;
-            }
-        });
+        OneShotPreDrawListener.add(mLauncher.getWorkspace(),
+                () -> mProgressProvider.setReadyToHandleTransition(true));
     }
 
     /**
@@ -92,6 +96,7 @@
      */
     public void onDestroy() {
         mProgressProvider.destroy();
+        mNaturalOrientationProgressProvider.destroy();
     }
 
     private class QsbAnimationListener implements TransitionProgressListener {
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index c784d82..d310893 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -18,6 +18,7 @@
 
 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.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
 
@@ -30,7 +31,6 @@
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
 
-
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.statehandlers.DepthController;
@@ -95,7 +95,7 @@
     public void setSecondTaskId(Task task, Consumer<Boolean> callback) {
         mSecondTask = task;
         launchTasks(mInitialTask, mSecondTask, mStagePosition, callback,
-                false /* freezeTaskList */);
+                false /* freezeTaskList */, DEFAULT_SPLIT_RATIO);
     }
 
     /**
@@ -107,14 +107,15 @@
         TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers =
                 groupedTaskView.getTaskIdAttributeContainers();
         launchTasks(taskIdAttributeContainers[0].getTask(), taskIdAttributeContainers[1].getTask(),
-                taskIdAttributeContainers[0].getStagePosition(), callback, freezeTaskList);
+                taskIdAttributeContainers[0].getStagePosition(), callback, freezeTaskList,
+                groupedTaskView.getSplitRatio());
     }
 
     /**
      * @param stagePosition representing location of task1
      */
     public void launchTasks(Task task1, Task task2, @StagePosition int stagePosition,
-            Consumer<Boolean> callback, boolean freezeTaskList) {
+            Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
         // Assume initial task is for top/left part of screen
         final int[] taskIds = stagePosition == STAGE_POSITION_TOP_OR_LEFT
                 ? new int[]{task1.key.id, task2.key.id}
@@ -123,7 +124,7 @@
             RemoteSplitLaunchTransitionRunner animationRunner =
                     new RemoteSplitLaunchTransitionRunner(task1, task2);
             mSystemUiProxy.startTasks(taskIds[0], null /* mainOptions */, taskIds[1],
-                    null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT,
+                    null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT, splitRatio,
                     new RemoteTransitionCompat(animationRunner, MAIN_EXECUTOR,
                             ActivityThread.currentActivityThread().getApplicationThread()));
         } else {
@@ -140,7 +141,7 @@
             }
             mSystemUiProxy.startTasksWithLegacyTransition(taskIds[0], mainOpts.toBundle(),
                     taskIds[1], null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT,
-                    adapter);
+                    splitRatio, adapter);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
new file mode 100644
index 0000000..dc97dd6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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 android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+
+/**
+ * Animation that moves hotseat icons from center to the sides (final position)
+ */
+public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCenterAnimator {
+
+    private final Launcher mLauncher;
+
+    public UnfoldMoveFromCenterHotseatAnimator(Launcher launcher, WindowManager windowManager) {
+        super(windowManager);
+        mLauncher = launcher;
+    }
+
+    @Override
+    protected void onPrepareViewsForAnimation() {
+        Hotseat hotseat = mLauncher.getHotseat();
+
+        ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
+        disableClipping(hotseat);
+
+        for (int i = 0; i < hotseatIcons.getChildCount(); i++) {
+            View child = hotseatIcons.getChildAt(i);
+            registerViewForAnimation(child);
+        }
+    }
+
+    @Override
+    public void onTransitionFinished() {
+        restoreClipping(mLauncher.getHotseat());
+        super.onTransitionFinished();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
index 95403b2..3d72398 100644
--- a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
@@ -16,44 +16,28 @@
 package com.android.quickstep.util;
 
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
 
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
-import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator;
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
-
-import java.util.HashMap;
-import java.util.Map;
 
 /**
  * Animation that moves launcher icons and widgets from center to the sides (final position)
  */
-public class UnfoldMoveFromCenterWorkspaceAnimator
-        implements UnfoldTransitionProgressProvider.TransitionProgressListener {
+public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCenterAnimator {
 
     private final Launcher mLauncher;
-    private final UnfoldMoveFromCenterAnimator mMoveFromCenterAnimation;
-
-    private final Map<ViewGroup, Boolean> mOriginalClipToPadding = new HashMap<>();
-    private final Map<ViewGroup, Boolean> mOriginalClipChildren = new HashMap<>();
 
     public UnfoldMoveFromCenterWorkspaceAnimator(Launcher launcher, WindowManager windowManager) {
+        super(windowManager);
         mLauncher = launcher;
-        mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
-                new LauncherViewsMoveFromCenterTranslationApplier());
     }
 
     @Override
-    public void onTransitionStarted() {
-        mMoveFromCenterAnimation.updateDisplayProperties();
-
+    protected void onPrepareViewsForAnimation() {
         Workspace workspace = mLauncher.getWorkspace();
-        Hotseat hotseat = mLauncher.getHotseat();
 
         // App icons and widgets
         workspace
@@ -65,57 +49,17 @@
 
                     for (int i = 0; i < itemsContainer.getChildCount(); i++) {
                         View child = itemsContainer.getChildAt(i);
-                        mMoveFromCenterAnimation.registerViewForAnimation(child);
+                        registerViewForAnimation(child);
                     }
                 });
 
         disableClipping(workspace);
-
-        // Hotseat icons
-        ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
-        disableClipping(hotseat);
-
-        for (int i = 0; i < hotseatIcons.getChildCount(); i++) {
-            View child = hotseatIcons.getChildAt(i);
-            mMoveFromCenterAnimation.registerViewForAnimation(child);
-        }
-
-        onTransitionProgress(0f);
-    }
-
-    @Override
-    public void onTransitionProgress(float progress) {
-        mMoveFromCenterAnimation.onTransitionProgress(progress);
     }
 
     @Override
     public void onTransitionFinished() {
-        mMoveFromCenterAnimation.onTransitionFinished();
-        mMoveFromCenterAnimation.clearRegisteredViews();
-
         restoreClipping(mLauncher.getWorkspace());
         mLauncher.getWorkspace().forEachVisiblePage(page -> restoreClipping((CellLayout) page));
-        restoreClipping(mLauncher.getHotseat());
-
-        mOriginalClipChildren.clear();
-        mOriginalClipToPadding.clear();
-    }
-
-    private void disableClipping(ViewGroup view) {
-        mOriginalClipToPadding.put(view, view.getClipToPadding());
-        mOriginalClipChildren.put(view, view.getClipChildren());
-        view.setClipToPadding(false);
-        view.setClipChildren(false);
-    }
-
-    private void restoreClipping(ViewGroup view) {
-        final Boolean originalClipToPadding = mOriginalClipToPadding.get(view);
-        if (originalClipToPadding != null) {
-            view.setClipToPadding(originalClipToPadding);
-        }
-        final Boolean originalClipChildren = mOriginalClipChildren.get(view);
-        if (originalClipChildren != null) {
-            view.setClipChildren(originalClipChildren);
-        }
+        super.onTransitionFinished();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index 325ec04..a343e0a 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -129,9 +129,7 @@
     public void update(RectF position, float progress, float windowRadius) {
         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
 
-        float dX = mIsRtl
-                ? position.left - (lp.getMarginStart() - lp.width)
-                : position.left - lp.getMarginStart();
+        float dX = position.left - lp.getMarginStart();
         float dY = position.top - lp.topMargin;
 
         setTranslationX(dX);
@@ -157,16 +155,10 @@
         lp.ignoreInsets = true;
         // Position the floating view exactly on top of the original
         lp.topMargin = Math.round(pos.top);
-        if (mIsRtl) {
-            lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right));
-        } else {
-            lp.setMarginStart(Math.round(pos.left));
-        }
+        lp.setMarginStart(Math.round(pos.left));
         // Set the properties here already to make sure they are available when running the first
         // animation frame.
-        int left = mIsRtl
-                ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
-                : lp.leftMargin;
+        int left = lp.leftMargin;
         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 4771d1e..9311261 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -1,5 +1,6 @@
 package com.android.quickstep.views;
 
+import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
 
@@ -122,6 +123,14 @@
         invalidate();
     }
 
+    public float getSplitRatio() {
+        if (mSplitBoundsConfig != null) {
+            return mSplitBoundsConfig.appsStackedVertically
+                    ? mSplitBoundsConfig.topTaskPercent : mSplitBoundsConfig.leftTaskPercent;
+        }
+        return DEFAULT_SPLIT_RATIO;
+    }
+
     @Override
     public boolean offerTouchToChildren(MotionEvent event) {
         computeAndSetIconTouchDelegate(mIconView2, mIcon2CenterCoords, mIcon2TouchDelegate);
@@ -148,16 +157,27 @@
     @Nullable
     @Override
     public RunnableList launchTaskAnimated() {
-        getRecentsView().getSplitPlaceholder().launchTasks(this /*groupedTaskView*/,
-                null /*callback*/,
+        if (mTask == null || mSecondaryTask == null) {
+            return null;
+        }
+
+        RunnableList endCallback = new RunnableList();
+        RecentsView recentsView = getRecentsView();
+        // Callbacks run from remote animation when recents animation not currently running
+        recentsView.getSplitPlaceholder().launchTasks(this /*groupedTaskView*/,
+                success -> endCallback.executeAllAndDestroy(),
                 false /* freezeTaskList */);
-        return null;
+
+        // Callbacks get run from recentsView for case when recents animation already running
+        recentsView.addSideTaskLaunchCallback(endCallback);
+        return endCallback;
     }
 
     @Override
     public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
         getRecentsView().getSplitPlaceholder().launchTasks(mTask, mSecondaryTask,
-                STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList);
+                STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList,
+                getSplitRatio());
     }
 
     @Override
@@ -238,4 +258,10 @@
         super.updateSnapshotRadius();
         mSnapshotView2.setFullscreenParams(mCurrentFullscreenParams);
     }
+
+    @Override
+    protected void setIconAndDimTransitionProgress(float progress, boolean invert) {
+        super.setIconAndDimTransitionProgress(progress, invert);
+        mIconView2.setAlpha(mIconView.getAlpha());
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 3cba392..3e06f55 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
-import static com.android.quickstep.ViewUtils.postFrameDrawn;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -33,6 +32,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statehandlers.DepthController;
@@ -71,6 +71,7 @@
     @Override
     public void startHome() {
         mActivity.getStateManager().goToState(NORMAL);
+        AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
 
     @Override
@@ -93,10 +94,6 @@
 
     @Override
     public void onStateTransitionStart(LauncherState toState) {
-        if (toState == NORMAL || toState == SPRING_LOADED) {
-            // Clean-up logic that occurs when recents is no longer in use/visible.
-            reset();
-        }
         setOverviewStateEnabled(toState.overviewUi);
         setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
         setOverviewFullscreenEnabled(toState.getOverviewFullscreenProgress() == 1);
@@ -105,13 +102,17 @@
 
     @Override
     public void onStateTransitionComplete(LauncherState finalState) {
+        if (finalState == NORMAL || finalState == SPRING_LOADED) {
+            // Clean-up logic that occurs when recents is no longer in use/visible.
+            reset();
+        }
         boolean isOverlayEnabled = finalState == OVERVIEW || finalState == OVERVIEW_MODAL_TASK;
         setOverlayEnabled(isOverlayEnabled);
         setFreezeViewVisibility(false);
 
         if (isOverlayEnabled) {
-            postFrameDrawn(this, () -> runActionOnRemoteHandles(remoteTargetHandle ->
-                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)));
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4b9b8a4..3020dd9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -107,6 +107,7 @@
 import android.widget.OverScroller;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.core.graphics.ColorUtils;
@@ -181,6 +182,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
@@ -822,7 +824,7 @@
     @Override
     public void onTaskIconChanged(String pkg, UserHandle user) {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = getTaskViewAt(i);
+            TaskView tv = requireTaskViewAt(i);
             Task task = tv.getTask();
             if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
                     && task.key.userId == user.getIdentifier()) {
@@ -1104,7 +1106,7 @@
         }
 
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             int[] taskIds = taskView.getTaskIds();
             if (taskIds[0] == taskId || taskIds[1] == taskId) {
                 return taskView;
@@ -1121,9 +1123,6 @@
             // Reset the running task when leaving overview since it can still have a reference to
             // its thumbnail
             mTmpRunningTasks = null;
-            if (mSplitSelectStateController.isSplitSelectActive()) {
-                cancelSplitSelect(false);
-            }
             // Remove grouped tasks and recycle once we exit overview
             int taskCount = getTaskViewCount();
             for (int i = 0; i < taskCount; i++) {
@@ -1181,7 +1180,7 @@
         if (showAsGrid()) {
             int taskCount = getTaskViewCount();
             for (int i = 0; i < taskCount; i++) {
-                TaskView taskView = getTaskViewAt(i);
+                TaskView taskView = requireTaskViewAt(i);
                 if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
                     // Keep consuming events to pass to delegate
                     return true;
@@ -1418,7 +1417,7 @@
             if (runningTaskId != -1) {
                 targetPage = indexOfChild(newRunningTaskView);
             } else if (getTaskViewCount() > 0) {
-                targetPage = indexOfChild(getTaskViewAt(0));
+                targetPage = indexOfChild(requireTaskViewAt(0));
             }
         } else if (currentTaskId != -1) {
             currentTaskView = getTaskViewByTaskId(currentTaskId);
@@ -1452,7 +1451,7 @@
 
     private void removeTasksViewsAndClearAllButton() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            removeView(getTaskViewAt(i));
+            removeView(requireTaskViewAt(i));
         }
         if (indexOfChild(mClearAllButton) != -1) {
             removeView(mClearAllButton);
@@ -1498,7 +1497,7 @@
 
     public void resetTaskVisuals() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             if (mIgnoreResetTaskId != taskView.getTaskIds()[0]) {
                 taskView.resetViewTransforms();
                 taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
@@ -1508,6 +1507,16 @@
             }
         }
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // resetTaskVisuals is called at the end of dismiss animation which could update
+            // primary and secondary translation of the live tile cut out. We will need to do so
+            // here accordingly.
+            runActionOnRemoteHandles(remoteTargetHandle -> {
+                TaskViewSimulator simulator = remoteTargetHandle.getTaskViewSimulator();
+                simulator.taskPrimaryTranslation.value = 0;
+                simulator.taskSecondaryTranslation.value = 0;
+                simulator.fullScreenProgress.value = 0;
+                simulator.recentsViewScale.value = 1;
+            });
             // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
             // null.
             if (!mRunningTaskShowScreenshot) {
@@ -1529,7 +1538,7 @@
         mFullscreenProgress = fullscreenProgress;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
+            requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
         mClearAllButton.setFullscreenProgress(fullscreenProgress);
 
@@ -1656,7 +1665,7 @@
 
         float accumulatedTranslationX = 0;
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             taskView.updateTaskSize();
             taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX);
             taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f);
@@ -1802,7 +1811,7 @@
 
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             Task task = taskView.getTask();
             int index = indexOfChild(taskView);
             boolean visible;
@@ -1945,7 +1954,7 @@
         }
 
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             if (taskView.getTaskViewId() == taskViewId) {
                 return taskView;
             }
@@ -2043,7 +2052,7 @@
         int runningIndex = getCurrentPage();
         AnimatorSet as = new AnimatorSet();
         for (int i = 0; i < getTaskViewCount(); i++) {
-            View taskView = getTaskViewAt(i);
+            View taskView = requireTaskViewAt(i);
             if (runningIndex == i && taskView.getAlpha() != 0) {
                 continue;
             }
@@ -2054,7 +2063,7 @@
 
     private void updateChildTaskOrientations() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).setOrientationState(mOrientationState);
+            requireTaskViewAt(i).setOrientationState(mOrientationState);
         }
         TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
         if (tv != null) {
@@ -2259,7 +2268,7 @@
             mTaskIconScaledDown = isScaledDown;
             int taskCount = getTaskViewCount();
             for (int i = 0; i < taskCount; i++) {
-                getTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+                requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
             }
         }
     }
@@ -2275,7 +2284,7 @@
         mTaskIconScaledDown = false;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             taskView.setIconScaleAnimStartProgress(0f);
             taskView.animateIconScaleAndDimIntoView();
         }
@@ -2347,7 +2356,7 @@
             mTopRowIdSet.clear();
         }
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
             // which case keep tasks in their respective rows. For the running task, don't join
@@ -2412,7 +2421,7 @@
                         if (j == focusedTaskIndex) {
                             continue;
                         }
-                        widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+                        widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
                     }
 
                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
@@ -2431,7 +2440,7 @@
                         if (j == focusedTaskIndex) {
                             continue;
                         }
-                        widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+                        widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
                     }
 
                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
@@ -2509,7 +2518,7 @@
         }
 
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
                     + snappedTaskNonGridScrollAdjustment);
         }
@@ -2550,7 +2559,7 @@
         mGridProgress = gridProgress;
 
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setGridProgress(gridProgress);
+            requireTaskViewAt(i).setGridProgress(gridProgress);
         }
         mClearAllButton.setGridProgress(gridProgress);
     }
@@ -2713,7 +2722,7 @@
                         mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
                 // Pick the next focused task from the preferred row.
                 for (int i = 0; i < taskCount; i++) {
-                    TaskView taskView = getTaskViewAt(i);
+                    TaskView taskView = requireTaskViewAt(i);
                     if (taskView == dismissedTaskView) {
                         continue;
                     }
@@ -2816,7 +2825,7 @@
                                 + (taskCount - 1) * halfAdditionalDismissTranslationOffset,
                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
                 for (int i = 0; i < taskCount; i++) {
-                    TaskView taskView = getTaskViewAt(i);
+                    TaskView taskView = requireTaskViewAt(i);
                     anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff,
                             clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1));
                     dismissTranslationInterpolationEnd = Utilities.boundToRange(
@@ -3118,14 +3127,10 @@
                         }
                     } else {
                         // Update focus task and its size.
-                        if (finalIsFocusedTaskDismissed) {
-                            if (finalNextFocusedTaskView != null) {
-                                mFocusedTaskViewId = finalNextFocusedTaskView.getTaskViewId();
-                                mTopRowIdSet.remove(mFocusedTaskViewId);
-                                finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
-                            } else {
-                                mFocusedTaskViewId = -1;
-                            }
+                        if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) {
+                            mFocusedTaskViewId = finalNextFocusedTaskView.getTaskViewId();
+                            mTopRowIdSet.remove(mFocusedTaskViewId);
+                            finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
                         }
                         updateTaskSize(/*isTaskDismissal=*/ true);
                         updateChildTaskOrientations();
@@ -3136,7 +3141,7 @@
                             // Rebalance tasks in the grid
                             int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
                             if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
-                                TaskView taskView = getTaskViewAt(highestVisibleTaskIndex);
+                                TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
 
                                 boolean shouldRebalance;
                                 int screenStart = mOrientationHandler.getPrimaryScroll(
@@ -3189,6 +3194,7 @@
                                 }
                             }
                         }
+                        pageBeginTransition();
                         setCurrentPage(pageToSnapTo);
                         // Update various scroll-dependent UI.
                         dispatchScrollChanged();
@@ -3233,7 +3239,7 @@
         IntArray topArray = new IntArray(mTopRowIdSet.size());
         int taskViewCount = getTaskViewCount();
         for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = getTaskViewAt(i).getTaskViewId();
+            int taskViewId = requireTaskViewAt(i).getTaskViewId();
             if (mTopRowIdSet.contains(taskViewId)) {
                 topArray.add(taskViewId);
             }
@@ -3252,7 +3258,7 @@
         IntArray bottomArray = new IntArray(bottomRowIdArraySize);
         int taskViewCount = getTaskViewCount();
         for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = getTaskViewAt(i).getTaskViewId();
+            int taskViewId = requireTaskViewAt(i).getTaskViewId();
             if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
                 bottomArray.add(taskViewId);
             }
@@ -3322,7 +3328,7 @@
 
         int count = getTaskViewCount();
         for (int i = 0; i < count; i++) {
-            addDismissedTaskAnimations(getTaskViewAt(i), duration, anim);
+            addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim);
         }
 
         mPendingAnimation = anim;
@@ -3445,7 +3451,7 @@
         mContentAlpha = alpha;
         int runningTaskId = getTaskIdsForRunningTaskView()[0];
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView child = getTaskViewAt(i);
+            TaskView child = requireTaskViewAt(i);
             int[] childTaskIds = child.getTaskIds();
             if (!mRunningTaskTileHidden ||
                     (childTaskIds[0] != runningTaskId && childTaskIds[1] != runningTaskId)) {
@@ -3546,6 +3552,14 @@
         return child instanceof TaskView ? (TaskView) child : null;
     }
 
+    /**
+     * A version of {@link #getTaskViewAt} when the caller is sure about the input index.
+     */
+    @NonNull
+    private TaskView requireTaskViewAt(int index) {
+        return Objects.requireNonNull(getTaskViewAt(index));
+    }
+
     public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
         mOnEmptyMessageUpdatedListener = listener;
     }
@@ -3762,7 +3776,7 @@
     protected void setTaskViewsResistanceTranslation(float translation) {
         mTaskViewsSecondaryTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView task = getTaskViewAt(i);
+            TaskView task = requireTaskViewAt(i);
             task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
         }
         runActionOnRemoteHandles(
@@ -3772,14 +3786,14 @@
 
     private void updateTaskViewsSnapshotRadius() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).updateSnapshotRadius();
+            requireTaskViewAt(i).updateSnapshotRadius();
         }
     }
 
     protected void setTaskViewsPrimarySplitTranslation(float translation) {
         mTaskViewsPrimarySplitTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView task = getTaskViewAt(i);
+            TaskView task = requireTaskViewAt(i);
             task.getPrimarySplitTranslationProperty().set(task, translation);
         }
     }
@@ -3787,7 +3801,7 @@
     protected void setTaskViewsSecondarySplitTranslation(float translation) {
         mTaskViewsSecondarySplitTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             if (taskView == mSplitHiddenTaskView) {
                 continue;
             }
@@ -3801,7 +3815,7 @@
     public void applySplitPrimaryScrollOffset() {
         if (isSplitPlaceholderFirstInGrid()) {
             for (int i = 0; i < getTaskViewCount(); i++) {
-                getTaskViewAt(i).setSplitScrollOffsetPrimary(mSplitPlaceholderSize);
+                requireTaskViewAt(i).setSplitScrollOffsetPrimary(mSplitPlaceholderSize);
             }
         } else if (isSplitPlaceholderLastInGrid()) {
             mClearAllButton.setSplitSelectScrollOffsetPrimary(-mSplitPlaceholderSize);
@@ -3843,7 +3857,7 @@
      */
     public void resetSplitPrimaryScrollOffset() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).setSplitScrollOffsetPrimary(0);
+            requireTaskViewAt(i).setSplitScrollOffsetPrimary(0);
         }
         mClearAllButton.setSplitSelectScrollOffsetPrimary(0);
     }
@@ -3923,111 +3937,9 @@
         pendingAnimation.buildAnim().start();
     }
 
-    public PendingAnimation cancelSplitSelect(boolean animate) {
-        SplitSelectStateController splitController = mSplitSelectStateController;
-        @StagePosition int stagePosition = splitController.getActiveSplitStagePosition();
-        Rect initialBounds = splitController.getInitialBounds();
-        splitController.resetState();
-        int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
-        PendingAnimation pendingAnim = new PendingAnimation(duration);
-        mSplitToast.cancel();
-        mSplitUnsupportedToast.cancel();
-        if (!animate) {
-            resetFromSplitSelectionState();
-            return pendingAnim;
-        }
-
-        addViewInLayout(mSplitHiddenTaskView, mSplitHiddenTaskViewIndex,
-                mSplitHiddenTaskView.getLayoutParams());
-        mSplitHiddenTaskView.setAlpha(0);
-        int[] oldScroll = new int[getChildCount()];
-        getPageScrolls(oldScroll, false,
-                view -> view.getVisibility() != GONE && view != mSplitHiddenTaskView);
-
-        int[] newScroll = new int[getChildCount()];
-        getPageScrolls(newScroll, false, SIMPLE_SCROLL_LOGIC);
-
-        boolean needsCurveUpdates = false;
-        for (int i = mSplitHiddenTaskViewIndex; i >= 0; i--) {
-            View child = getChildAt(i);
-            if (child == mSplitHiddenTaskView) {
-                TaskView taskView = (TaskView) child;
-
-                int dir = mOrientationHandler.getSplitTaskViewDismissDirection(stagePosition,
-                        mActivity.getDeviceProfile());
-                FloatProperty<TaskView> dismissingTaskViewTranslate;
-                Rect hiddenBounds = new Rect(taskView.getLeft(), taskView.getTop(),
-                        taskView.getRight(), taskView.getBottom());
-                int distanceDelta = 0;
-                if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_SECONDARY_NEGATIVE) {
-                    dismissingTaskViewTranslate = taskView
-                            .getSecondaryDissmissTranslationProperty();
-                    distanceDelta = initialBounds.top - hiddenBounds.top;
-                    taskView.layout(initialBounds.left, hiddenBounds.top, initialBounds.right,
-                            hiddenBounds.bottom);
-                } else {
-                    dismissingTaskViewTranslate = taskView
-                            .getPrimaryDismissTranslationProperty();
-                    distanceDelta = initialBounds.left - hiddenBounds.left;
-                    taskView.layout(hiddenBounds.left, initialBounds.top, hiddenBounds.right,
-                            initialBounds.bottom);
-                    if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_POSITIVE) {
-                        distanceDelta *= -1;
-                    }
-                }
-                pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView,
-                        dismissingTaskViewTranslate,
-                        distanceDelta));
-                pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, ALPHA, 1));
-            } else {
-                // If insertion is on last index (furthest from clear all), we directly add the view
-                // else we translate all views to the right of insertion index further right,
-                // ignore views to left
-                if (showAsGrid()) {
-                    // TODO(b/186800707) handle more elegantly for grid
-                    continue;
-                }
-                int scrollDiff = newScroll[i] - oldScroll[i];
-                if (scrollDiff != 0) {
-                    FloatProperty translationProperty = child instanceof TaskView
-                            ? ((TaskView) child).getPrimaryDismissTranslationProperty()
-                            : mOrientationHandler.getPrimaryViewTranslate();
-
-                    ResourceProvider rp = DynamicResource.provider(mActivity);
-                    SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
-                            .setDampingRatio(
-                                    rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
-                            .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
-                    pendingAnim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
-                            .setDuration(duration), ACCEL, sp);
-                    needsCurveUpdates = true;
-                }
-            }
-        }
-
-        if (needsCurveUpdates) {
-            pendingAnim.addOnFrameCallback(this::updateCurveProperties);
-        }
-
-        pendingAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                // TODO(b/186800707) Figure out how to undo for grid view
-                //  Need to handle cases where dismissed task is
-                //  * Top Row
-                //  * Bottom Row
-                //  * Focused Task
-                updateGridProperties();
-                resetFromSplitSelectionState();
-            }
-        });
-
-        return pendingAnim;
-    }
-
     /** TODO(b/181707736) More gracefully handle exiting split selection state */
     private void resetFromSplitSelectionState() {
-        if (!showAsGrid()) {
+        if (!mActivity.getDeviceProfile().overviewShowAsGrid) {
             int pageToSnapTo = mCurrentPage;
             if (mSplitHiddenTaskViewIndex <= pageToSnapTo) {
                 pageToSnapTo += 1;
@@ -4040,7 +3952,6 @@
         resetTaskVisuals();
         mSplitHiddenTaskViewIndex = -1;
         if (mSplitHiddenTaskView != null) {
-            mSplitHiddenTaskView.setTranslationY(0);
             mSplitHiddenTaskView.setVisibility(VISIBLE);
             mSplitHiddenTaskView = null;
         }
@@ -4070,8 +3981,8 @@
         mTaskViewDeadZoneRect.setEmpty();
         int count = getTaskViewCount();
         if (count > 0) {
-            final View taskView = getTaskViewAt(0);
-            getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
+            final View taskView = requireTaskViewAt(0);
+            requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
             mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
                     taskView.getBottom());
         }
@@ -4501,9 +4412,8 @@
     }
 
     private int getFirstViewIndex() {
-        return mShowAsGridLastOnLayout && mFocusedTaskViewId != -1
-                ? indexOfChild(getFocusedTaskView())
-                : 0;
+        TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
+        return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
     }
 
     private int getLastViewIndex() {
@@ -4553,7 +4463,7 @@
 
         final int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             float scrollDiff = taskView.getScrollAdjustment(showAsFullscreen, showAsGrid);
             int pageScroll = newPageScrolls[i] + (int) scrollDiff;
             if ((mIsRtl && pageScroll < clearAllScroll + clearAllWidth)
@@ -4721,7 +4631,7 @@
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
+            requireTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
         }
     }
 
@@ -4875,7 +4785,7 @@
         mColorTint = tintAmount;
 
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
+            requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
         }
 
         Drawable scrimBg = mActivity.getScrimView().getBackground();
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index d91669a..da92551 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -19,6 +19,7 @@
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
+import static com.android.launcher3.Utilities.comp;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
@@ -290,8 +291,17 @@
             float cornerRadius) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
-                canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
-                canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
+                // TODO(b/189265196): Temporary fix to align the surface with the cutout perfectly.
+                // Round up only when the live tile task is displayed in Overview.
+                float rounding = comp(mFullscreenParams.mFullscreenProgress);
+                float left = x + rounding / 2;
+                float top = y + rounding / 2;
+                float right = width - rounding;
+                float bottom = height - rounding;
+
+                canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
+                        mClearPaint);
+                canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
                         mDimmingPaintAfterClearing);
                 return;
             }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index e8077cf..8dee4e7 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -599,7 +599,9 @@
         if (confirmSecondSplitSelectApp()) {
             return;
         }
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
+        RecentsView recentsView = getRecentsView();
+        RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles;
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask() && remoteTargetHandles != null) {
             if (!mIsClickableAsLiveTile) {
                 return;
             }
@@ -612,9 +614,7 @@
             }
 
             mIsClickableAsLiveTile = false;
-            RecentsView recentsView = getRecentsView();
             RemoteAnimationTargets targets;
-            RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles;
             if (remoteTargetHandles.length == 1) {
                 targets = remoteTargetHandles[0].getTransformParams().getTargetSet();
             } else {
@@ -912,7 +912,7 @@
         return deviceProfile.overviewShowAsGrid && !isFocusedTask();
     }
 
-    private void setIconAndDimTransitionProgress(float progress, boolean invert) {
+    protected void setIconAndDimTransitionProgress(float progress, boolean invert) {
         if (invert) {
             progress = 1 - progress;
         }
@@ -1531,6 +1531,7 @@
         private final float mCornerRadius;
         private final float mWindowCornerRadius;
 
+        public float mFullscreenProgress;
         public RectF mCurrentDrawnInsets = new RectF();
         public float mCurrentDrawnCornerRadius;
         /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
@@ -1548,6 +1549,8 @@
          */
         public void setProgress(float fullscreenProgress, float parentScale, float taskViewScale,
                 int previewWidth, DeviceProfile dp, PreviewPositionHelper pph) {
+            mFullscreenProgress = fullscreenProgress;
+
             RectF insets = pph.getInsetsToDrawInFullscreen(dp);
 
             float currentInsetsLeft = insets.left * fullscreenProgress;
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
new file mode 100644
index 0000000..ba1a60d
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -0,0 +1,159 @@
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
+import static com.android.quickstep.OverviewCommandHelper.TYPE_HOME;
+import static com.android.quickstep.OverviewCommandHelper.TYPE_TOGGLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TouchInteractionService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class TaskbarNavButtonControllerTest {
+
+    private final static int DISPLAY_ID = 2;
+
+    @Mock
+    SystemUiProxy mockSystemUiProxy;
+    @Mock
+    TouchInteractionService mockService;
+    @Mock
+    OverviewCommandHelper mockCommandHelper;
+    @Mock
+    Handler mockHandler;
+
+    private TaskbarNavButtonController mNavButtonController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mockService.getDisplayId()).thenReturn(DISPLAY_ID);
+        when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper);
+        mNavButtonController = new TaskbarNavButtonController(mockService,
+                mockSystemUiProxy, mockHandler);
+    }
+
+    @Test
+    public void testPressBack() {
+        mNavButtonController.onButtonClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(1)).onBackPressed();
+    }
+
+    @Test
+    public void testPressImeSwitcher() {
+        mNavButtonController.onButtonClick(BUTTON_IME_SWITCH);
+        verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed();
+    }
+
+    @Test
+    public void testPressA11yShortClick() {
+        mNavButtonController.onButtonClick(BUTTON_A11Y);
+        verify(mockSystemUiProxy, times(1))
+                .notifyAccessibilityButtonClicked(DISPLAY_ID);
+    }
+
+    @Test
+    public void testPressA11yLongClick() {
+        mNavButtonController.onButtonLongClick(BUTTON_A11Y);
+        verify(mockSystemUiProxy, times(1)).notifyAccessibilityButtonLongClicked();
+    }
+
+    @Test
+    public void testLongPressHome() {
+        mNavButtonController.onButtonLongClick(BUTTON_HOME);
+        verify(mockSystemUiProxy, times(1)).startAssistant(any());
+    }
+
+    @Test
+    public void testPressHome() {
+        mNavButtonController.onButtonClick(BUTTON_HOME);
+        verify(mockCommandHelper, times(1)).addCommand(TYPE_HOME);
+    }
+
+    @Test
+    public void testPressRecents() {
+        mNavButtonController.onButtonClick(BUTTON_RECENTS);
+        verify(mockCommandHelper, times(1)).addCommand(TYPE_TOGGLE);
+    }
+
+    @Test
+    public void testPressRecentsWithScreenPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonClick(BUTTON_RECENTS);
+        verify(mockCommandHelper, times(0)).addCommand(TYPE_TOGGLE);
+    }
+
+    @Test
+    public void testLongPressBackRecentsNotPinned() {
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(0)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressBackRecentsPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(1)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressBackRecentsTooLongPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        try {
+            Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(0)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressBackRecentsMultipleAttemptPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        try {
+            Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(0)).stopScreenPinning();
+
+        // Try again w/in threshold
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(1)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressHomeScreenPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_HOME);
+        verify(mockSystemUiProxy, times(0)).startAssistant(any());
+    }
+}
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 78f3f11..c689942 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -321,6 +321,9 @@
     <!-- Size of the maximum radius for the enforced rounded rectangles. -->
     <dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
 
+<!-- Base Swipe Detector, speed in dp/s -->
+    <dimen name="base_swift_detector_fling_release_velocity">1dp</dimen>
+
 <!-- Overview placeholder to compile in Launcher3 without Quickstep -->
     <dimen name="task_thumbnail_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size">0dp</dimen>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f2836ca..ff19918 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -813,15 +813,23 @@
         Point padding = getTotalWorkspacePadding();
 
         int numColumns = isTwoPanels ? inv.numColumns * 2 : inv.numColumns;
-        int cellLayoutTotalPadding =
-                isTwoPanels ? 4 * cellLayoutPaddingLeftRightPx : 2 * cellLayoutPaddingLeftRightPx;
-        int screenWidthPx = availableWidthPx - padding.x - cellLayoutTotalPadding;
+        int screenWidthPx = getWorkspaceWidth(padding);
         result.x = calculateCellWidth(screenWidthPx, cellLayoutBorderSpacePx.x, numColumns);
         result.y = calculateCellHeight(availableHeightPx - padding.y
                 - cellLayoutBottomPaddingPx, cellLayoutBorderSpacePx.y, inv.numRows);
         return result;
     }
 
+    public int getWorkspaceWidth() {
+        return getWorkspaceWidth(getTotalWorkspacePadding());
+    }
+
+    public int getWorkspaceWidth(Point workspacePadding) {
+        int cellLayoutTotalPadding =
+                isTwoPanels ? 4 * cellLayoutPaddingLeftRightPx : 2 * cellLayoutPaddingLeftRightPx;
+        return availableWidthPx - workspacePadding.x - cellLayoutTotalPadding;
+    }
+
     public Point getTotalWorkspacePadding() {
         updateWorkspacePadding();
         return new Point(workspacePadding.left + workspacePadding.right,
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index d844b87..2ebedf7 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -286,7 +286,11 @@
         mExtraAttrs = closestProfile.extraAttrs;
 
         iconSize = displayOption.iconSizes;
-        iconBitmapSize = ResourceUtils.pxFromDp(iconSize[INDEX_DEFAULT], metrics);
+        float maxIconSize = iconSize[0];
+        for (int i = 1; i < iconSize.length; i++) {
+            maxIconSize = Math.max(maxIconSize, iconSize[i]);
+        }
+        iconBitmapSize = ResourceUtils.pxFromDp(maxIconSize, metrics);
         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
 
         iconTextSize = displayOption.textSizes;
@@ -555,13 +559,10 @@
         }
         out.multiply(1.0f / weights);
 
-        // Since the bitmaps are persisted, ensure that the default bitmap size is same as
+        // Since the bitmaps are persisted, ensure that all bitmap sizes are not larger than
         // predefined size to avoid cache invalidation
-        out.iconSizes[INDEX_DEFAULT] =
-                closestPoint.iconSizes[INDEX_DEFAULT];
-        for (int i = INDEX_DEFAULT + 1; i < COUNT_SIZES; i++) {
-            out.iconSizes[i] = Math.min(out.iconSizes[i],
-                    out.iconSizes[INDEX_DEFAULT]);
+        for (int i = INDEX_DEFAULT; i < COUNT_SIZES; i++) {
+            out.iconSizes[i] = Math.min(out.iconSizes[i], closestPoint.iconSizes[i]);
         }
 
         return out;
diff --git a/src/com/android/launcher3/anim/RevealOutlineAnimation.java b/src/com/android/launcher3/anim/RevealOutlineAnimation.java
index f99dabc..f5a746f 100644
--- a/src/com/android/launcher3/anim/RevealOutlineAnimation.java
+++ b/src/com/android/launcher3/anim/RevealOutlineAnimation.java
@@ -26,9 +26,27 @@
     /** Sets the progress, from 0 to 1, of the reveal animation. */
     abstract void setProgress(float progress);
 
+    /**
+     * @see #createRevealAnimator(View, boolean, float) where startProgress is set to 0.
+     */
     public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) {
-        ValueAnimator va =
-                isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
+        return createRevealAnimator(revealView, isReversed, 0f /* startProgress */);
+    }
+
+    /**
+     * Animates the given View's ViewOutline according to {@link #setProgress(float)}.
+     * @param revealView The View whose outline we are animating.
+     * @param isReversed Whether we are hiding rather than revealing the View.
+     * @param startProgress The progress at which to start the newly created animation. Useful if
+     * the previous reveal animation was cancelled and we want to create a new animation where it
+     * left off. Note that if isReversed=true, we start at 1 - startProgress (and go to 0).
+     * @return The Animator, which the caller must start.
+     */
+    public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed,
+            float startProgress) {
+        ValueAnimator va = isReversed
+                ? ValueAnimator.ofFloat(1f - startProgress, 0f)
+                : ValueAnimator.ofFloat(startProgress, 1f);
         final float elevation = revealView.getElevation();
 
         va.addListener(new AnimatorListenerAdapter() {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 13ad90e..d3351dc 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -31,6 +31,8 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Workspace;
@@ -215,6 +217,19 @@
     }
 
     public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
+        addItem(context, item, newItem, null);
+    }
+
+    public synchronized void addItem(
+            Context context, ItemInfo item, boolean newItem, @Nullable LoaderMemoryLogger logger) {
+        if (logger != null) {
+            logger.addLog(
+                    Log.DEBUG,
+                    TAG,
+                    String.format("Adding item to ID map: %s", item.toString()),
+                    /* stackTrace= */ null);
+        }
+
         itemsIdMap.put(item.id, item);
         switch (item.itemType) {
             case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 47df538..08b38e8 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -383,18 +383,23 @@
         info.cellY = getInt(cellYIndex);
     }
 
+    public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
+        checkAndAddItem(info, dataModel, null);
+    }
+
     /**
      * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item,
      * otherwise marks it for deletion.
      */
-    public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
+    public void checkAndAddItem(
+            ItemInfo info, BgDataModel dataModel, LoaderMemoryLogger logger) {
         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             // Ensure that it is a valid intent. An exception here will
             // cause the item loading to get skipped
             ShortcutKey.fromItemInfo(info);
         }
         if (checkItemPlacement(info)) {
-            dataModel.addItem(mContext, info, false);
+            dataModel.addItem(mContext, info, false, logger);
         } else {
             markDeleted("Item position overlap");
         }
diff --git a/src/com/android/launcher3/model/LoaderMemoryLogger.java b/src/com/android/launcher3/model/LoaderMemoryLogger.java
new file mode 100644
index 0000000..f48efcb
--- /dev/null
+++ b/src/com/android/launcher3/model/LoaderMemoryLogger.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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.model;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+
+/**
+ * Helper logger that collects logs while {@code LoaderTask#run} executes and prints them all iff
+ * an exception is caught in {@code LoaderTask#run}.
+ */
+public class LoaderMemoryLogger {
+
+    private static final String TAG = "LoaderMemoryLogger";
+
+    private final ArrayList<LogEntry> mLogEntries = new ArrayList<>();
+
+    protected LoaderMemoryLogger() {}
+
+    protected void addLog(int logLevel, String tag, String log) {
+        addLog(logLevel, tag, log, null);
+    }
+
+    protected void addLog(
+            int logLevel, String tag, String log, Exception stackTrace) {
+        switch (logLevel) {
+            case Log.ASSERT:
+            case Log.ERROR:
+            case Log.DEBUG:
+            case Log.INFO:
+            case Log.VERBOSE:
+            case Log.WARN:
+                mLogEntries.add(new LogEntry(logLevel, tag, log, stackTrace));
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid log level provided: " + logLevel);
+
+        }
+    }
+
+    protected void clearLogs() {
+        mLogEntries.clear();
+    }
+
+    protected void printLogs() {
+        for (LogEntry logEntry : mLogEntries) {
+            String tag = String.format("%s: %s", TAG, logEntry.mLogTag);
+            String logString = logEntry.mStackStrace == null
+                    ? logEntry.mLogString
+                    : String.format(
+                            "%s\n%s",
+                            logEntry.mLogString,
+                            Log.getStackTraceString(logEntry.mStackStrace));
+
+            Log.println(logEntry.mLogLevel, tag, logString);
+        }
+        clearLogs();
+    }
+
+    private static class LogEntry {
+
+        protected final int mLogLevel;
+        protected final String mLogTag;
+        protected final String mLogString;
+        @Nullable protected final Exception mStackStrace;
+
+        protected LogEntry(
+                int logLevel, String logTag, String logString, @Nullable Exception stackStrace) {
+            mLogLevel = logLevel;
+            mLogTag = logTag;
+            mLogString = logString;
+            mStackStrace = stackStrace;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index a4f6f7a..2a0f9a6 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -52,6 +52,8 @@
 import android.util.LongSparseArray;
 import android.util.TimingLogger;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
@@ -197,11 +199,12 @@
 
         Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
         TimingLogger logger = new TimingLogger(TAG, "run");
+        LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
             Trace.beginSection("LoadWorkspace");
             try {
-                loadWorkspace(allShortcuts);
+                loadWorkspace(allShortcuts, memoryLogger);
             } finally {
                 Trace.endSection();
             }
@@ -311,9 +314,13 @@
 
             mModelDelegate.modelLoadComplete();
             transaction.commit();
+            memoryLogger.clearLogs();
         } catch (CancellationException e) {
             // Loader stopped, ignore
             logASplit(logger, "Cancelled");
+        } catch (Exception e) {
+            memoryLogger.printLogs();
+            throw e;
         } finally {
             logger.dumpToLog();
         }
@@ -325,13 +332,21 @@
         this.notify();
     }
 
-    private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts) {
+    private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, LoaderMemoryLogger logger) {
         loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI,
-                null /* selection */);
+                null /* selection */, logger);
     }
 
-    protected void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, Uri contentUri,
-            String selection) {
+    protected void loadWorkspace(
+            List<ShortcutInfo> allDeepShortcuts, Uri contentUri, String selection) {
+        loadWorkspace(allDeepShortcuts, contentUri, selection, null);
+    }
+
+    protected void loadWorkspace(
+            List<ShortcutInfo> allDeepShortcuts,
+            Uri contentUri,
+            String selection,
+            @Nullable LoaderMemoryLogger logger) {
         final Context context = mApp.getContext();
         final ContentResolver contentResolver = context.getContentResolver();
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
@@ -635,7 +650,7 @@
                                         }
                                 }
 
-                                c.checkAndAddItem(info, mBgDataModel);
+                                c.checkAndAddItem(info, mBgDataModel, logger);
                             } else {
                                 throw new RuntimeException("Unexpected null WorkspaceItemInfo");
                             }
@@ -654,7 +669,7 @@
                             // no special handling required for restored folders
                             c.markRestored();
 
-                            c.checkAndAddItem(folderInfo, mBgDataModel);
+                            c.checkAndAddItem(folderInfo, mBgDataModel, logger);
                             break;
 
                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 1276ece..52c3581 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -17,6 +17,7 @@
 
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
+import android.content.Context;
 import android.graphics.PointF;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -26,6 +27,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.R;
+
 import java.util.LinkedList;
 import java.util.Queue;
 
@@ -44,10 +47,9 @@
     private static final boolean DBG = false;
     private static final String TAG = "BaseSwipeDetector";
     private static final float ANIMATION_DURATION = 1200;
-    /** The minimum release velocity in pixels per millisecond that triggers fling.*/
-    private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
     private static final PointF sTempPoint = new PointF();
 
+    private final float mReleaseVelocity;
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     protected final boolean mIsRtl;
@@ -64,6 +66,7 @@
     private boolean mIsSettingState;
 
     protected boolean mIgnoreSlopWhenSettling;
+    protected Context mContext;
 
     private enum ScrollState {
         IDLE,
@@ -71,10 +74,14 @@
         SETTLING       // onDragEnd
     }
 
-    protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) {
+    protected BaseSwipeDetector(@NonNull Context context, @NonNull ViewConfiguration config,
+            boolean isRtl) {
         mTouchSlop = config.getScaledTouchSlop();
         mMaxVelocity = config.getScaledMaximumFlingVelocity();
         mIsRtl = isRtl;
+        mContext = context;
+        mReleaseVelocity = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.base_swift_detector_fling_release_velocity);
     }
 
     public static long calculateDuration(float velocity, float progressNeeded) {
@@ -120,7 +127,7 @@
     }
 
     public boolean isFling(float velocity) {
-        return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS;
+        return Math.abs(velocity) > mReleaseVelocity;
     }
 
     public boolean onTouchEvent(MotionEvent ev) {
@@ -236,7 +243,7 @@
         } else {
             mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
             mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
-        } 
+        }
     }
 
     protected abstract boolean shouldScrollStart(PointF displacement);
diff --git a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
index 944391e..6e2f0d8 100644
--- a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
@@ -21,7 +21,6 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
 
@@ -43,13 +42,7 @@
     private int mScrollDirections;
 
     public BothAxesSwipeDetector(@NonNull Context context, @NonNull Listener l) {
-        this(ViewConfiguration.get(context), l, Utilities.isRtl(context.getResources()));
-    }
-
-    @VisibleForTesting
-    protected BothAxesSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            boolean isRtl) {
-        super(config, isRtl);
+        super(context, ViewConfiguration.get(context), Utilities.isRtl(context.getResources()));
         mListener = l;
     }
 
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index e127074..c255225 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -36,6 +36,7 @@
 import android.graphics.RectF;
 import android.graphics.drawable.ShapeDrawable;
 import android.util.FloatProperty;
+import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.VelocityTracker;
@@ -462,8 +463,8 @@
     }
 
     @Override
-    public FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
-            DeviceProfile deviceProfile) {
-        return primary;
+    public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
+            FloatProperty secondary, DeviceProfile deviceProfile) {
+        return new Pair<>(primary, secondary);
     }
 }
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index d954552..54b30fb 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -24,6 +24,7 @@
 import android.graphics.RectF;
 import android.graphics.drawable.ShapeDrawable;
 import android.util.FloatProperty;
+import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
@@ -113,8 +114,8 @@
     float getSecondaryValue(float x, float y);
 
     boolean isLayoutNaturalToLauncher();
-    FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
-            DeviceProfile deviceProfile);
+    Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
+            FloatProperty secondary, DeviceProfile deviceProfile);
     int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
     List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
     /**
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index fbc335c..e69944a 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -35,6 +35,7 @@
 import android.graphics.RectF;
 import android.graphics.drawable.ShapeDrawable;
 import android.util.FloatProperty;
+import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.VelocityTracker;
@@ -571,12 +572,12 @@
     }
 
     @Override
-    public FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
-            DeviceProfile dp) {
-        if (dp.isLandscape) { // or seascape
-            return primary;
+    public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
+            FloatProperty secondary, DeviceProfile deviceProfile) {
+        if (deviceProfile.isLandscape) { // or seascape
+            return new Pair<>(primary, secondary);
         } else {
-            return secondary;
+            return new Pair<>(secondary, primary);
         }
     }
 }
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index f751b7d..5c599c0 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -106,13 +106,15 @@
 
     public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l,
             @NonNull Direction dir) {
-        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
+        super(context, ViewConfiguration.get(context),  Utilities.isRtl(context.getResources()));
+        mListener = l;
+        mDir = dir;
     }
 
     @VisibleForTesting
-    protected SingleAxisSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            @NonNull Direction dir, boolean isRtl) {
-        super(config, isRtl);
+    protected SingleAxisSwipeDetector(@NonNull Context context, @NonNull ViewConfiguration config,
+            @NonNull Listener l, @NonNull Direction dir, boolean isRtl) {
+        super(context, config, isRtl);
         mListener = l;
         mDir = dir;
     }
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 53b1c3e..cb714b2 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -68,6 +68,11 @@
     public @interface StageType {}
     ///////////////////////////////////
 
+    /**
+     * Default split ratio for launching app pair from overview.
+     */
+    public static final float DEFAULT_SPLIT_RATIO = 0.5f;
+
     public static class SplitPositionOption {
         public final int iconResId;
         public final int textResId;
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 8b7ad46..27cf134 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -460,7 +460,7 @@
         if (mIconLoadResult != null && mIconLoadResult.isIconLoaded) {
             setVisibility(View.VISIBLE);
         }
-        if (!mIsOpening) {
+        if (!mIsOpening && mOriginalIcon != null) {
             // When closing an app, we want the item on the workspace to be invisible immediately
             setIconAndDotVisible(mOriginalIcon, false);
         }
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
index 49fcd2e..f945819 100644
--- a/src/com/android/launcher3/views/Snackbar.java
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -28,8 +28,9 @@
 import android.view.MotionEvent;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -44,7 +45,7 @@
     private static final long HIDE_DURATION_MS = 180;
     private static final int TIMEOUT_DURATION_MS = 4000;
 
-    private final BaseDraggingActivity mActivity;
+    private final ActivityContext mActivity;
     private Runnable mOnDismissed;
 
     public Snackbar(Context context, AttributeSet attrs) {
@@ -53,12 +54,19 @@
 
     public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mActivity = BaseDraggingActivity.fromContext(context);
+        mActivity = ActivityContext.lookupContext(context);
         inflate(context, R.layout.snackbar, this);
     }
 
-    public static void show(BaseDraggingActivity activity, int labelStringResId,
-            int actionStringResId, Runnable onDismissed, Runnable onActionClicked) {
+    /** Show a snackbar with just a label. */
+    public static <T extends Context & ActivityContext> void show(T activity, int labelStringRedId,
+            Runnable onDismissed) {
+        show(activity, labelStringRedId, NO_ID, onDismissed, null);
+    }
+
+    /** Show a snackbar with a label and action. */
+    public static <T extends Context & ActivityContext> void show(T activity, int labelStringResId,
+            int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked) {
         closeOpenViews(activity, true, TYPE_SNACKBAR);
         Snackbar snackbar = new Snackbar(activity, null);
         // Set some properties here since inflated xml only contains the children.
@@ -87,13 +95,30 @@
         params.setMargins(0, 0, 0, marginBottom + insets.bottom);
 
         TextView labelView = snackbar.findViewById(R.id.label);
-        TextView actionView = snackbar.findViewById(R.id.action);
         String labelText = res.getString(labelStringResId);
-        String actionText = res.getString(actionStringResId);
-        int totalContentWidth = (int) (labelView.getPaint().measureText(labelText)
-                + actionView.getPaint().measureText(actionText))
+        labelView.setText(labelText);
+
+        TextView actionView = snackbar.findViewById(R.id.action);
+        float actionWidth;
+        if (actionStringResId != NO_ID) {
+            String actionText = res.getString(actionStringResId);
+            actionWidth = actionView.getPaint().measureText(actionText)
+                    + actionView.getPaddingRight() + actionView.getPaddingLeft();
+            actionView.setText(actionText);
+            actionView.setOnClickListener(v -> {
+                if (onActionClicked != null) {
+                    onActionClicked.run();
+                }
+                snackbar.mOnDismissed = null;
+                snackbar.close(true);
+            });
+        } else {
+            actionWidth = 0;
+            actionView.setVisibility(GONE);
+        }
+
+        int totalContentWidth = (int) (labelView.getPaint().measureText(labelText) + actionWidth)
                 + labelView.getPaddingRight() + labelView.getPaddingLeft()
-                + actionView.getPaddingRight() + actionView.getPaddingLeft()
                 + padding * 2;
         if (totalContentWidth > params.width) {
             // The text doesn't fit in our standard width so update width to accommodate.
@@ -113,17 +138,8 @@
                 params.width = maxWidth;
             }
         }
-        labelView.setText(labelText);
-        actionView.setText(actionText);
-        actionView.setOnClickListener(v -> {
-            if (onActionClicked != null) {
-                onActionClicked.run();
-            }
-            snackbar.mOnDismissed = null;
-            snackbar.close(true);
-        });
-        snackbar.mOnDismissed = onDismissed;
 
+        snackbar.mOnDismissed = onDismissed;
         snackbar.setAlpha(0);
         snackbar.setScaleX(0.8f);
         snackbar.setScaleY(0.8f);
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 472e1a1..260f556 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -58,6 +59,7 @@
     private TouchEventGenerator mGenerator;
     private SingleAxisSwipeDetector mDetector;
     private int mTouchSlop;
+    Context mContext;
 
     @Mock
     private SingleAxisSwipeDetector.Listener mMockListener;
@@ -69,12 +71,13 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mGenerator = new TouchEventGenerator((ev) -> mDetector.onTouchEvent(ev));
-        ViewConfiguration orgConfig = ViewConfiguration
-                .get(InstrumentationRegistry.getTargetContext());
+        mContext = InstrumentationRegistry.getTargetContext();
+        ViewConfiguration orgConfig = ViewConfiguration.get(mContext);
         doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig)
                 .getScaledMaximumFlingVelocity();
 
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, VERTICAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_BOTH, false);
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
@@ -84,7 +87,8 @@
 
     @Test
     public void testDragStart_verticalPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, VERTICAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 - mTouchSlop);
@@ -94,7 +98,8 @@
 
     @Test
     public void testDragStart_verticalNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, VERTICAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
@@ -112,7 +117,8 @@
 
     @Test
     public void testDragStart_horizontalPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
@@ -123,7 +129,8 @@
 
     @Test
     public void testDragStart_horizontalNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
@@ -134,7 +141,8 @@
 
     @Test
     public void testDragStart_horizontalRtlPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, true);
         mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
@@ -145,7 +153,8 @@
 
     @Test
     public void testDragStart_horizontalRtlNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, true);
         mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index d5479fb..3eb8cf1 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -212,7 +212,7 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get overview actions")) {
             verifyActiveContainer();
-            UiObject2 overviewActions = mLauncher.waitForLauncherObject("action_buttons");
+            UiObject2 overviewActions = mLauncher.waitForOverviewObject("action_buttons");
             return new OverviewActions(overviewActions, mLauncher);
         }
     }
@@ -224,19 +224,16 @@
         return mLauncher.hasLauncherObject(mLauncher.getOverviewObjectSelector("clear_all"));
     }
 
-    /* TODO(b/197630182): Once b/188790554 is fixed, remove instanceof check. Currently, when
-        swiping from app to overview in Fallback Recents, taskbar remains and no action buttons
-        are visible, so we are only testing Overview for now, not BaseOverview. */
     private void verifyActionsViewVisibility() {
-        if (!(this instanceof Overview) || !hasTasks()) {
+        if (!hasTasks()) {
             return;
         }
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {
             if (mLauncher.isTablet() && !isOverviewSnappedToFocusedTaskForTablet()) {
-                mLauncher.waitUntilLauncherObjectGone("action_buttons");
+                mLauncher.waitUntilOverviewObjectGone("action_buttons");
             } else {
-                mLauncher.waitForLauncherObject("action_buttons");
+                mLauncher.waitForOverviewObject("action_buttons");
             }
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 3485dd1..91b1bc7 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1029,6 +1029,10 @@
         waitUntilGoneBySelector(getLauncherObjectSelector(resId));
     }
 
+    void waitUntilOverviewObjectGone(String resId) {
+        waitUntilGoneBySelector(getOverviewObjectSelector(resId));
+    }
+
     void waitUntilLauncherObjectGone(BySelector selector) {
         waitUntilGoneBySelector(makeLauncherSelector(selector));
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 6e7264a..0bac2ca 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -77,7 +77,8 @@
             mLauncher.scroll(
                     widgetsContainer,
                     Direction.UP,
-                    new Rect(0, 0, mLauncher.getVisibleBounds(widgetsContainer).width(), 0),
+                    new Rect(0, 0, mLauncher.getRightGestureMarginInContainer(widgetsContainer) + 1,
+                            0),
                     FLING_STEPS, false);
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("flung back")) {
                 verifyActiveContainer();