Merge "Converting ModelTask to an interface instead of an abstract class" into main
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 5ac5761..9eabb55 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.views.DesktopAppSelectView;
@@ -229,6 +230,7 @@
         for (DesktopVisibilityListener listener : mDesktopVisibilityListeners) {
             listener.onDesktopVisibilityChanged(areDesktopTasksVisible);
         }
+        DisplayController.handleInfoChangeForDesktopMode(mLauncher);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
deleted file mode 100644
index 3635827..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.taskbar;
-
-import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_NOTIFICATIONS;
-import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_QUICK_SETTINGS;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo.Config;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
-
-/**
- * Controller for managing buttons and status icons in taskbar in a desktop environment.
- */
-public class DesktopNavbarButtonsViewController extends NavbarButtonsViewController {
-
-    private final TaskbarActivityContext mContext;
-    private final FrameLayout mNavButtonsView;
-    private final ViewGroup mNavButtonContainer;
-    private final ViewGroup mStartContextualContainer;
-    private final View mAllAppsButton;
-
-    private TaskbarControllers mControllers;
-
-    public DesktopNavbarButtonsViewController(TaskbarActivityContext context,
-            @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) {
-        super(context, navigationBarPanelContext, navButtonsView);
-        mContext = context;
-        mNavButtonsView = navButtonsView;
-        mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
-        mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons);
-        mAllAppsButton = LayoutInflater.from(context)
-                .inflate(R.layout.taskbar_all_apps_button, mStartContextualContainer, false);
-        mAllAppsButton.setOnClickListener(v -> mControllers.taskbarAllAppsController.toggle());
-    }
-
-    /**
-     * Initializes the controller
-     */
-    @Override
-    public void init(TaskbarControllers controllers) {
-        mControllers = controllers;
-        super.init(controllers);
-    }
-
-    @Override
-    protected void setupController() {
-        mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarHeight;
-
-        // Quick settings and notifications buttons
-        addButton(R.drawable.ic_sysbar_quick_settings, BUTTON_QUICK_SETTINGS,
-                mNavButtonContainer, mControllers.navButtonController,
-                R.id.quick_settings_button);
-        addButton(R.drawable.ic_sysbar_notifications, BUTTON_NOTIFICATIONS,
-                mNavButtonContainer, mControllers.navButtonController,
-                R.id.notifications_button);
-        // All apps button
-        mStartContextualContainer.addView(mAllAppsButton);
-    }
-
-    /** Cleans up on destroy */
-    @Override
-    public void onDestroy() { }
-
-    @Override
-    public void onConfigurationChanged(@Config int configChanges) { }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRecentAppsController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRecentAppsController.java
deleted file mode 100644
index acfbea3..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRecentAppsController.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.taskbar;
-
-import android.app.ActivityManager;
-import android.content.ComponentName;
-import android.util.SparseArray;
-
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.quickstep.RecentsModel;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Provides recent apps functionality specifically in a desktop environment.
- */
-public class DesktopTaskbarRecentAppsController extends TaskbarRecentAppsController {
-
-    private final TaskbarActivityContext mContext;
-    private ArrayList<ItemInfo> mRunningApps = new ArrayList<>();
-    private AppInfo[] mApps;
-
-    public DesktopTaskbarRecentAppsController(TaskbarActivityContext context) {
-        mContext = context;
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        mApps = null;
-    }
-
-    @Override
-    protected void setApps(AppInfo[] apps) {
-        mApps = apps;
-    }
-
-    @Override
-    protected boolean isEnabled() {
-        return true;
-    }
-
-    /**
-     * Set mRunningApps to hold currently running applications using the list of currently running
-     * tasks. Filtering is also done to ignore applications that are already on the taskbar in the
-     * original hotseat.
-     */
-    @Override
-    protected void updateRunningApps(SparseArray<ItemInfo> hotseatItems) {
-        ArrayList<AppInfo> runningApps = getRunningAppsFromTasks();
-        ArrayList<ItemInfo> filteredRunningApps = new ArrayList<>();
-        for (AppInfo runningApp : runningApps) {
-            boolean shouldAddOnTaskbar = true;
-            for (int i = 0; i < hotseatItems.size(); i++) {
-                if (hotseatItems.keyAt(i) >= mControllers.taskbarActivityContext.getDeviceProfile()
-                        .numShownHotseatIcons) {
-                    break;
-                }
-                if (hotseatItems.valueAt(i).getTargetPackage()
-                        .equals(runningApp.getTargetPackage())) {
-                    shouldAddOnTaskbar = false;
-                    break;
-                }
-            }
-            if (shouldAddOnTaskbar) {
-                filteredRunningApps.add(new WorkspaceItemInfo(runningApp));
-            }
-        }
-        mRunningApps = filteredRunningApps;
-        mControllers.taskbarViewController.commitRunningAppsToUI();
-    }
-
-    /**
-     * Returns a copy of hotseatItems with the addition of currently running applications.
-     */
-    @Override
-    protected ItemInfo[] updateHotseatItemInfos(ItemInfo[] hotseatItemInfos) {
-        // hotseatItemInfos.length would be 0 if deviceProfile.numShownHotseatIcons is 0, so we
-        // don't want to show anything in the hotseat
-        if (hotseatItemInfos.length == 0) return hotseatItemInfos;
-
-        int runningAppsIndex = 0;
-        ItemInfo[] newHotseatItemsInfo = Arrays.copyOf(
-                hotseatItemInfos, hotseatItemInfos.length + mRunningApps.size());
-        for (int i = hotseatItemInfos.length; i < newHotseatItemsInfo.length; i++) {
-            newHotseatItemsInfo[i] = mRunningApps.get(runningAppsIndex);
-            runningAppsIndex++;
-        }
-        return newHotseatItemsInfo;
-    }
-
-
-    /**
-     * Returns a list of running applications from the list of currently running tasks.
-     */
-    private ArrayList<AppInfo> getRunningAppsFromTasks() {
-        ArrayList<ActivityManager.RunningTaskInfo> tasks =
-                RecentsModel.INSTANCE.get(mContext).getRunningTasks();
-        ArrayList<AppInfo> runningApps = new ArrayList<>();
-        // early return if apps is empty, since we would have no AppInfo to compare
-        if (mApps == null)  {
-            return runningApps;
-        }
-
-        Set<String> seenPackages = new HashSet<>();
-        for (ActivityManager.RunningTaskInfo taskInfo : tasks) {
-            if (taskInfo.realActivity == null) continue;
-
-            // If a different task for the same package has already been handled, skip this one
-            String taskPackage = taskInfo.realActivity.getPackageName();
-            if (seenPackages.contains(taskPackage)) continue;
-
-            // Otherwise, get the corresponding AppInfo and add it to the list
-            seenPackages.add(taskPackage);
-            AppInfo app = getAppInfo(taskInfo.realActivity);
-            if (app == null) continue;
-            runningApps.add(app);
-        }
-        return runningApps;
-    }
-
-    /**
-     * Retrieves the corresponding AppInfo for the activity.
-     */
-    private AppInfo getAppInfo(ComponentName activity) {
-        String packageName = activity.getPackageName();
-        for (AppInfo app : mApps) {
-            if (!packageName.equals(app.getTargetPackage())) {
-                continue;
-            }
-            return app;
-        }
-        return null;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
index 06d25a2..3649c4e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsController.kt
@@ -40,12 +40,16 @@
  */
 class DesktopTaskbarRunningAppsController(
     private val recentsModel: RecentsModel,
-    private val desktopVisibilityController: DesktopVisibilityController?,
+    // Pass a provider here instead of the actual DesktopVisibilityController instance since that
+    // instance might not be available when this constructor is called.
+    private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?,
 ) : TaskbarRecentAppsController() {
 
     private var apps: Array<AppInfo>? = null
     private var allRunningDesktopAppInfos: List<AppInfo>? = null
-    private var runningDesktopAppInfosExceptHotseatItems: List<ItemInfo>? = null
+
+    private val desktopVisibilityController: DesktopVisibilityController?
+        get() = desktopVisibilityControllerProvider()
 
     private val isInDesktopMode: Boolean
         get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
@@ -63,20 +67,24 @@
     override fun isEnabled() = true
 
     @VisibleForTesting
-    public override fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo>?): Array<ItemInfo>? {
-        val actualHotseatItems = hotseatItems ?: return super.updateHotseatItemInfos(null)
+    public override fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
         if (!isInDesktopMode) {
             Log.d(TAG, "updateHotseatItemInfos: not in Desktop Mode")
             return hotseatItems
         }
         val newHotseatItemInfos =
-            actualHotseatItems
+            hotseatItems
+                .filterNotNull()
                 // Ignore predicted apps - we show running apps instead
                 .filter { itemInfo -> !itemInfo.isPredictedItem }
                 .toMutableList()
         val runningDesktopAppInfos =
-            runningDesktopAppInfosExceptHotseatItems ?: return newHotseatItemInfos.toTypedArray()
-        newHotseatItemInfos.addAll(runningDesktopAppInfos)
+            allRunningDesktopAppInfos?.let {
+                getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList())
+            }
+        if (runningDesktopAppInfos != null) {
+            newHotseatItemInfos.addAll(runningDesktopAppInfos)
+        }
         return newHotseatItemInfos.toTypedArray()
     }
 
@@ -88,19 +96,13 @@
     }
 
     @VisibleForTesting
-    public override fun updateRunningApps(hotseatItems: SparseArray<ItemInfo>?) {
+    public override fun updateRunningApps() {
         if (!isInDesktopMode) {
             Log.d(TAG, "updateRunningApps: not in Desktop Mode")
             mControllers.taskbarViewController.commitRunningAppsToUI()
             return
         }
-        val allRunningDesktopAppInfos = getRunningDesktopAppInfos()
-        this.allRunningDesktopAppInfos = allRunningDesktopAppInfos
-        runningDesktopAppInfosExceptHotseatItems =
-            hotseatItems?.let {
-                getRunningDesktopAppInfosExceptHotseatApps(allRunningDesktopAppInfos, it.toList())
-            }
-
+        allRunningDesktopAppInfos = getRunningDesktopAppInfos()
         mControllers.taskbarViewController.commitRunningAppsToUI()
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarUIController.java
deleted file mode 100644
index 2dd610c4..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopTaskbarUIController.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.taskbar;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.quickstep.util.TISBindHelper;
-
-/**
- * A data source which integrates with a Launcher instance, used specifically for a
- * desktop environment.
- */
-public class DesktopTaskbarUIController extends TaskbarUIController {
-
-    private final QuickstepLauncher mLauncher;
-
-    public DesktopTaskbarUIController(QuickstepLauncher launcher) {
-        mLauncher = launcher;
-    }
-
-    @SuppressWarnings("MissingSuperCall") // TODO: Fix me
-    @Override
-    protected void init(TaskbarControllers taskbarControllers) {
-        super.init(taskbarControllers);
-        mLauncher.getHotseat().setIconsAlpha(0f);
-        mControllers.taskbarViewController.updateRunningApps();
-    }
-
-    @SuppressWarnings("MissingSuperCall") // TODO: Fix me
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        mLauncher.getHotseat().setIconsAlpha(1f);
-    }
-
-    /** Disable taskbar stashing in desktop environment. */
-    @Override
-    public boolean supportsVisualStashing() {
-        return false;
-    }
-
-    @Nullable
-    @Override
-    protected TISBindHelper getTISBindHelper() {
-        return mLauncher.getTISBindHelper();
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 2c2311a..2ce6a41 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -24,10 +24,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.TaskTransitionSpec;
-import android.view.WindowManagerGlobal;
 import android.window.RemoteTransition;
 
 import androidx.annotation.NonNull;
@@ -37,7 +33,6 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.QuickstepTransitionManager;
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.logging.InstanceId;
@@ -139,7 +134,6 @@
         mLauncher.setTaskbarUIController(null);
         mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
         mHomeState.removeListener(mVisibilityChangeListener);
-        updateTaskTransitionSpec(true);
     }
 
     private void onInAppDisplayProgressChanged() {
@@ -278,26 +272,6 @@
     private void onStashedInAppChanged(DeviceProfile deviceProfile) {
         boolean taskbarStashedInApps = mControllers.taskbarStashController.isStashedInApp();
         deviceProfile.isTaskbarPresentInApps = !taskbarStashedInApps;
-        updateTaskTransitionSpec(taskbarStashedInApps);
-    }
-
-    private void updateTaskTransitionSpec(boolean taskbarIsHidden) {
-        try {
-            if (taskbarIsHidden) {
-                // Clear custom task transition settings when the taskbar is stashed
-                WindowManagerGlobal.getWindowManagerService().clearTaskTransitionSpec();
-            } else {
-                // Adjust task transition spec to account for taskbar being visible
-                WindowManagerGlobal.getWindowManagerService().setTaskTransitionSpec(
-                        new TaskTransitionSpec(
-                                mLauncher.getColor(R.color.taskbar_background)));
-            }
-        } catch (RemoteException e) {
-            // This shouldn't happen but if it does task animations won't look good until the
-            // taskbar stashing state is changed.
-            Log.e(TAG, "Failed to update task transition spec to account for new taskbar state",
-                    e);
-        }
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 2c5aeb3..d5306fb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -317,38 +317,28 @@
             mPropertyHolders.add(new StatePropertyHolder(
                     mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
                     flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
-
-            // Rotation button
-            RotationButton rotationButton = new RotationButtonImpl(
-                    addButton(mEndContextualContainer, R.id.rotate_suggestion,
-                            R.layout.taskbar_contextual_button));
-            rotationButton.hide();
-            mControllers.rotationButtonController.setRotationButton(rotationButton, null);
-        } else {
-            mFloatingRotationButton = new FloatingRotationButton(
-                    ENABLE_TASKBAR_NAVBAR_UNIFICATION ? mNavigationBarPanelContext : mContext,
-                    R.string.accessibility_rotate_button,
-                    R.layout.rotate_suggestion,
-                    R.id.rotate_suggestion,
-                    R.dimen.floating_rotation_button_min_margin,
-                    R.dimen.rounded_corner_content_padding,
-                    R.dimen.floating_rotation_button_taskbar_left_margin,
-                    R.dimen.floating_rotation_button_taskbar_bottom_margin,
-                    R.dimen.floating_rotation_button_diameter,
-                    R.dimen.key_button_ripple_max_width,
-                    R.bool.floating_rotation_button_position_left);
-            mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
-                    mRotationButtonListener);
-
-            if (!mIsImeRenderingNavButtons) {
-                View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
-                        mStartContextualContainer, mControllers.navButtonController, R.id.back);
-                imeDownButton.setRotation(Utilities.isRtl(resources) ? 90 : -90);
-                // Only show when IME is visible.
-                mPropertyHolders.add(new StatePropertyHolder(imeDownButton,
-                        flags -> (flags & FLAG_IME_VISIBLE) != 0));
-            }
+        } else if (!mIsImeRenderingNavButtons) {
+            View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
+                    mStartContextualContainer, mControllers.navButtonController, R.id.back);
+            imeDownButton.setRotation(Utilities.isRtl(resources) ? 90 : -90);
+            // Only show when IME is visible.
+            mPropertyHolders.add(new StatePropertyHolder(imeDownButton,
+                    flags -> (flags & FLAG_IME_VISIBLE) != 0));
         }
+        mFloatingRotationButton = new FloatingRotationButton(
+                ENABLE_TASKBAR_NAVBAR_UNIFICATION ? mNavigationBarPanelContext : mContext,
+                R.string.accessibility_rotate_button,
+                R.layout.rotate_suggestion,
+                R.id.rotate_suggestion,
+                R.dimen.floating_rotation_button_min_margin,
+                R.dimen.rounded_corner_content_padding,
+                R.dimen.floating_rotation_button_taskbar_left_margin,
+                R.dimen.floating_rotation_button_taskbar_bottom_margin,
+                R.dimen.floating_rotation_button_diameter,
+                R.dimen.key_button_ripple_max_width,
+                R.bool.floating_rotation_button_position_left);
+        mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
+                mRotationButtonListener);
 
         applyState();
         mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
@@ -791,7 +781,6 @@
             NavButtonLayoutter navButtonLayoutter =
                     NavButtonLayoutFactory.Companion.getUiLayoutter(
                             dp, mNavButtonsView, mImeSwitcherButton,
-                            mControllers.rotationButtonController.getRotationButton(),
                             mA11yButton, mSpace, res, isInKidsMode, isInSetup, isThreeButtonNav,
                             mContext.isPhoneMode(), mWindowManagerProxy.getRotation(mContext));
             navButtonLayoutter.layoutButtons(mContext, isA11yButtonPersistent());
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 1a94424..8845813 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.taskbar;
 
-import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.os.Trace.TRACE_TAG_APP;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@@ -248,8 +247,6 @@
 
         mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this);
 
-        final boolean isPcMode = getPackageManager().hasSystemFeature(FEATURE_PC);
-
         // If Bubble bar is present, TaskbarControllers depends on it so build it first.
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
         BubbleBarController.onTaskbarRecreated();
@@ -280,11 +277,7 @@
         mControllers = new TaskbarControllers(this,
                 new TaskbarDragController(this),
                 buttonController,
-                isPcMode
-                        ? new DesktopNavbarButtonsViewController(this, mNavigationBarPanelContext,
-                                navButtonsView)
-                        : new NavbarButtonsViewController(this, mNavigationBarPanelContext,
-                                navButtonsView),
+                new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView),
                 rotationButtonController,
                 new TaskbarDragLayerController(this, mDragLayer),
                 new TaskbarViewController(this, taskbarView),
@@ -305,25 +298,24 @@
                 new VoiceInteractionWindowController(this),
                 new TaskbarTranslationController(this),
                 new TaskbarSpringOnStashController(this),
-                createTaskbarRecentAppsController(isPcMode),
+                createTaskbarRecentAppsController(),
                 TaskbarEduTooltipController.newInstance(this),
                 new KeyboardQuickSwitchController(),
-                new TaskbarPinningController(this),
+                new TaskbarPinningController(this, () ->
+                        DisplayController.INSTANCE.get(this).getInfo().isInDesktopMode()),
                 bubbleControllersOptional);
 
         mLauncherPrefs = LauncherPrefs.get(this);
     }
 
-    private TaskbarRecentAppsController createTaskbarRecentAppsController(boolean isPcMode) {
-        if (isPcMode) return new DesktopTaskbarRecentAppsController(this);
+    private TaskbarRecentAppsController createTaskbarRecentAppsController() {
         // TODO(b/335401172): unify DesktopMode checks in Launcher
-        final boolean showRunningAppsInDesktopMode = enableDesktopWindowingMode()
-                && enableDesktopWindowingTaskbarRunningApps();
-        return showRunningAppsInDesktopMode
-                        ? new DesktopTaskbarRunningAppsController(
-                                RecentsModel.INSTANCE.get(this),
-                                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController())
-                        : TaskbarRecentAppsController.DEFAULT;
+        if (enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()) {
+            return new DesktopTaskbarRunningAppsController(
+                    RecentsModel.INSTANCE.get(this),
+                    LauncherActivityInterface.INSTANCE::getDesktopVisibilityController);
+        }
+        return TaskbarRecentAppsController.DEFAULT;
     }
 
     /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 95c4e25..4a8ed87 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -155,13 +155,16 @@
             )
 
             // if there's an animating bubble add it to the touch region so that it's clickable
-            val animatingBubbleBounds =
+            val isAnimatingNewBubble =
                 controllers.bubbleControllers
                     .getOrNull()
                     ?.bubbleBarViewController
-                    ?.animatingBubbleBounds
-            if (animatingBubbleBounds != null) {
-                defaultTouchableRegion.op(animatingBubbleBounds, Region.Op.UNION)
+                    ?.isAnimatingNewBubble
+                    ?: false
+            if (isAnimatingNewBubble) {
+                val iconBounds =
+                    controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
+                defaultTouchableRegion.op(iconBounds, Region.Op.UNION)
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 3196bfb..42e6edb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.taskbar;
 
 import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
@@ -26,6 +25,7 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
 import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG;
@@ -143,7 +143,7 @@
     private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
         @Override
         public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
-            if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE
+            if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE
                     | CHANGE_TASKBAR_PINNING)) != 0) {
                 recreateTaskbar();
             }
@@ -426,9 +426,6 @@
      */
     private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) {
         if (activity instanceof QuickstepLauncher) {
-            if (mTaskbarActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) {
-                return new DesktopTaskbarUIController((QuickstepLauncher) activity);
-            }
             return new LauncherTaskbarUIController((QuickstepLauncher) activity);
         }
         if (activity instanceof RecentsActivity) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index be87cfd..9f24d38 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -279,7 +279,7 @@
 
     /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */
     public void updateRunningApps() {
-        mControllers.taskbarRecentAppsController.updateRunningApps(mHotseatItems);
+        mControllers.taskbarRecentAppsController.updateRunningApps();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index 2f2d636..6c9cc64 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -23,6 +23,7 @@
 import com.android.app.animation.Interpolators
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_PINNED
@@ -31,8 +32,10 @@
 import java.io.PrintWriter
 
 /** Controls taskbar pinning through a popup view. */
-class TaskbarPinningController(private val context: TaskbarActivityContext) :
-    TaskbarControllers.LoggableTaskbarController {
+class TaskbarPinningController(
+    private val context: TaskbarActivityContext,
+    private val isInDesktopModeProvider: () -> Boolean,
+) : TaskbarControllers.LoggableTaskbarController {
 
     private lateinit var controllers: TaskbarControllers
     private lateinit var taskbarSharedState: TaskbarSharedState
@@ -54,14 +57,22 @@
                 if (!didPreferenceChange) {
                     return
                 }
+                val shouldPinTaskbar =
+                    if (isInDesktopModeProvider()) {
+                        !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
+                    } else {
+                        !launcherPrefs.get(TASKBAR_PINNING)
+                    }
+
                 val animateToValue =
-                    if (!launcherPrefs.get(TASKBAR_PINNING)) {
+                    if (shouldPinTaskbar) {
                         statsLogManager.logger().log(LAUNCHER_TASKBAR_PINNED)
                         PINNING_PERSISTENT
                     } else {
                         statsLogManager.logger().log(LAUNCHER_TASKBAR_UNPINNED)
                         PINNING_TRANSIENT
                     }
+
                 taskbarSharedState.taskbarWasPinned = animateToValue == PINNING_TRANSIENT
                 animateTaskbarPinning(animateToValue)
             }
@@ -123,13 +134,24 @@
     @VisibleForTesting
     fun recreateTaskbarAndUpdatePinningValue() {
         updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false)
-        launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING))
+        if (isInDesktopModeProvider()) {
+            launcherPrefs.put(
+                TASKBAR_PINNING_IN_DESKTOP_MODE,
+                !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
+            )
+        } else {
+            launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING))
+        }
     }
 
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
         pw.println(prefix + "TaskbarPinningController:")
         pw.println("$prefix\tisAnimatingTaskbarPinning=$isAnimatingTaskbarPinning")
         pw.println("$prefix\tTASKBAR_PINNING shared pref =" + launcherPrefs.get(TASKBAR_PINNING))
+        pw.println(
+            "$prefix\tTASKBAR_PINNING_IN_DESKTOP_MODE shared pref in desktop mode =" +
+                launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
+        )
     }
 
     companion object {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java
index 9b84f1b..a29c74b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.java
@@ -17,9 +17,8 @@
 
 import static java.util.Collections.emptySet;
 
-import android.util.SparseArray;
-
 import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
 
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -59,13 +58,12 @@
     }
 
     /** Called to update hotseatItems, no-op except in desktop environment. */
-    protected ItemInfo[] updateHotseatItemInfos(ItemInfo[] hotseatItems) {
+    protected ItemInfo[] updateHotseatItemInfos(@NonNull ItemInfo[] hotseatItems) {
         return hotseatItems;
     }
 
     /** Called to update the list of currently running apps, no-op except in desktop environment. */
-    protected void updateRunningApps(SparseArray<ItemInfo> hotseatItems) {
-    }
+    protected void updateRunningApps() {}
 
     /** Returns the currently running apps, or an empty Set if outside of Desktop environment. */
     public Set<String> getRunningApps() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 2162a73..182ff7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -319,8 +319,7 @@
         updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isStashedInAppAuto);
         updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup);
         updateStateForFlag(FLAG_IN_SETUP, isInSetup);
-        updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneMode()
-                && !mActivity.isThreeButtonNav());
+        updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneGestureNavMode());
         // For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
         // us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
         updateStateForFlag(FLAG_IN_APP, true);
@@ -356,6 +355,7 @@
         boolean hideTaskbar = isVisible || !mActivity.isUserSetupComplete();
         updateStateForFlag(FLAG_IN_SETUP, hideTaskbar);
         updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, hideTaskbar);
+        updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneGestureNavMode());
         applyState(hideTaskbar ? 0 : getStashDuration());
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index effef3c..77f8a8a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.taskbar;
 
-import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 
 import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
@@ -157,23 +156,21 @@
         // Needed to draw folder leave-behind when opening one.
         setWillNotDraw(false);
 
-        if (!mActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) {
-            mAllAppsButton = (IconButtonView) LayoutInflater.from(context)
-                    .inflate(R.layout.taskbar_all_apps_button, this, false);
-            mAllAppsButton.setIconDrawable(resources.getDrawable(
-                    getAllAppsButton(isTransientTaskbar)));
-            mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-            mAllAppsButton.setForegroundTint(
-                    mActivityContext.getColor(R.color.all_apps_button_color));
+        mAllAppsButton = (IconButtonView) LayoutInflater.from(context)
+                .inflate(R.layout.taskbar_all_apps_button, this, false);
+        mAllAppsButton.setIconDrawable(resources.getDrawable(
+                getAllAppsButton(isTransientTaskbar)));
+        mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+        mAllAppsButton.setForegroundTint(
+                mActivityContext.getColor(R.color.all_apps_button_color));
 
-            if (enableTaskbarPinning()) {
-                mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
-                        R.layout.taskbar_divider,
-                        this, false);
-                mTaskbarDivider.setIconDrawable(
-                        resources.getDrawable(R.drawable.taskbar_divider_button));
-                mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-            }
+        if (enableTaskbarPinning()) {
+            mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
+                    R.layout.taskbar_divider,
+                    this, false);
+            mTaskbarDivider.setIconDrawable(
+                    resources.getDrawable(R.drawable.taskbar_divider_button));
+            mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
         }
 
         // TODO: Disable touch events on QSB otherwise it can crash.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 981c9f9..66e5302 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -419,9 +419,7 @@
         }
         if (update.bubbleBarLocation != null) {
             if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
-                // Animate when receiving updates. Skip it if we received the initial state.
-                boolean animate = !update.initialState;
-                updateBubbleBarLocationInternal(update.bubbleBarLocation, animate);
+                updateBubbleBarLocationInternal(update.bubbleBarLocation);
             }
         }
     }
@@ -483,15 +481,21 @@
      * Updates the value locally in Launcher and in WMShell.
      */
     public void updateBubbleBarLocation(BubbleBarLocation location) {
-        updateBubbleBarLocationInternal(location, false /* animate */);
+        updateBubbleBarLocationInternal(location);
         mSystemUiProxy.setBubbleBarLocation(location);
     }
 
-    private void updateBubbleBarLocationInternal(BubbleBarLocation location, boolean animate) {
-        mBubbleBarViewController.setBubbleBarLocation(location, animate);
+    private void updateBubbleBarLocationInternal(BubbleBarLocation location) {
+        mBubbleBarViewController.setBubbleBarLocation(location);
         mBubbleStashController.setBubbleBarLocation(location);
     }
 
+    @Override
+    public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+        mMainExecutor.execute(
+                () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
+    }
+
     //
     // Loading data for the bubbles
     //
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
index 8ed9949..9e5ffc9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt
@@ -19,7 +19,6 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.Point
-import android.graphics.RectF
 import android.view.Gravity.BOTTOM
 import android.view.Gravity.LEFT
 import android.view.Gravity.RIGHT
@@ -37,44 +36,29 @@
 class BubbleBarPinController(
     private val context: Context,
     private val container: FrameLayout,
-    private val screenSizeProvider: () -> Point
-) : BaseBubblePinController() {
+    screenSizeProvider: () -> Point
+) : BaseBubblePinController(screenSizeProvider) {
 
     private lateinit var bubbleBarViewController: BubbleBarViewController
     private lateinit var bubbleStashController: BubbleStashController
+    private var exclRectWidth: Float = 0f
+    private var exclRectHeight: Float = 0f
+
     private var dropTargetView: View? = null
 
     fun init(bubbleControllers: BubbleControllers) {
         bubbleBarViewController = bubbleControllers.bubbleBarViewController
         bubbleStashController = bubbleControllers.bubbleStashController
+        exclRectWidth = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width)
+        exclRectHeight = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_height)
     }
 
-    override fun getScreenCenterX(): Int {
-        return screenSizeProvider.invoke().x / 2
+    override fun getExclusionRectWidth(): Float {
+        return exclRectWidth
     }
 
-    override fun getExclusionRect(): RectF {
-        val rect =
-            RectF(
-                0f,
-                0f,
-                context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width),
-                context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_height)
-            )
-        val screenSize = screenSizeProvider.invoke()
-        val middleX = screenSize.x / 2
-        // Center it around the bottom center of the screen
-        rect.offsetTo(middleX - rect.width() / 2, screenSize.y - rect.height())
-        return rect
-    }
-
-    override fun createDropTargetView(): View {
-        return LayoutInflater.from(context)
-            .inflate(R.layout.bubble_bar_drop_target, container, false)
-            .also { view ->
-                dropTargetView = view
-                container.addView(view)
-            }
+    override fun getExclusionRectHeight(): Float {
+        return exclRectHeight
     }
 
     override fun getDropTargetView(): View? {
@@ -86,6 +70,15 @@
         dropTargetView = null
     }
 
+    override fun createDropTargetView(): View {
+        return LayoutInflater.from(context)
+            .inflate(R.layout.bubble_bar_drop_target, container, false)
+            .also { view ->
+                dropTargetView = view
+                container.addView(view)
+            }
+    }
+
     @SuppressLint("RtlHardcoded")
     override fun updateLocation(location: BubbleBarLocation) {
         val onLeft = location.isOnLeft(container.isLayoutRtl)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 5234936..60e8abe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -104,8 +104,6 @@
      * updates the bounds and accounts for translation.
      */
     private final Rect mBubbleBarBounds = new Rect();
-    /** The bounds of the animating bubble in the coordinate space of the BubbleBarView. */
-    private final Rect mAnimatingBubbleBounds = new Rect();
     // The amount the bubbles overlap when they are stacked in the bubble bar
     private final float mIconOverlapAmount;
     // The spacing between the bubbles when bubble bar is expanded
@@ -155,8 +153,6 @@
 
     private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED;
 
-    private boolean mLocationChangePending;
-
     public BubbleBarView(Context context) {
         this(context, null);
     }
@@ -185,12 +181,12 @@
 
         setClipToPadding(false);
 
-        mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarHeight());
+        mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight());
         setBackgroundDrawable(mBubbleBarBackground);
 
         mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
         mWidthAnimator.addUpdateListener(animation -> {
-            updateChildrenRenderNodeProperties();
+            updateChildrenRenderNodeProperties(mBubbleBarLocation);
             invalidate();
         });
         mWidthAnimator.addListener(new Animator.AnimatorListener() {
@@ -246,7 +242,7 @@
             params.width = (int) mIconSize;
             childView.setLayoutParams(params);
         }
-        mBubbleBarBackground.setHeight(getBubbleBarHeight());
+        mBubbleBarBackground.setHeight(getBubbleBarExpandedHeight());
         updateLayoutParams();
     }
 
@@ -264,7 +260,7 @@
         setPivotY(mRelativePivotY * getHeight());
 
         // Position the views
-        updateChildrenRenderNodeProperties();
+        updateChildrenRenderNodeProperties(mBubbleBarLocation);
     }
 
     @Override
@@ -280,7 +276,6 @@
 
     @SuppressLint("RtlHardcoded")
     private void onBubbleBarLocationChanged() {
-        mLocationChangePending = false;
         final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
         mBubbleBarBackground.setAnchorLeft(onLeft);
         mRelativePivotX = onLeft ? 0f : 1f;
@@ -301,11 +296,18 @@
     /**
      * Update {@link BubbleBarLocation}
      */
-    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, boolean animate) {
-        if (animate) {
-            animateToBubbleBarLocation(bubbleBarLocation);
-        } else {
-            setBubbleBarLocationInternal(bubbleBarLocation);
+    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+        if (mBubbleBarLocationAnimator != null) {
+            mBubbleBarLocationAnimator.removeAllListeners();
+            mBubbleBarLocationAnimator.cancel();
+            mBubbleBarLocationAnimator = null;
+        }
+        setTranslationX(0f);
+        setAlpha(1f);
+        if (bubbleBarLocation != mBubbleBarLocation) {
+            mBubbleBarLocation = bubbleBarLocation;
+            onBubbleBarLocationChanged();
+            invalidate();
         }
     }
 
@@ -318,51 +320,37 @@
         }
         mDragging = dragging;
         setElevation(dragging ? mDragElevation : mBubbleElevation);
-        if (!dragging && mLocationChangePending) {
-            // During drag finish animation we may update the translation x value to shift the
-            // bubble to the new drop target. Clear the translation here.
-            setTranslationX(0f);
-            onBubbleBarLocationChanged();
-        }
     }
 
     /**
-     * Adjust resting position for the bubble bar while it is being dragged.
-     * <p>
-     * Bubble bar is laid out on left or right side of the screen. When it is being dragged to
-     * the opposite side, the resting position should be on that side. Calculate any additional
-     * translation that may be required to move the bubble bar to the new side.
+     * Get translation for bubble bar when drag is released and it needs to animate back to the
+     * resting position.
+     * Resting position is based on the supplied location. If the supplied location is different
+     * from the internal location that was used to lay out the bubble bar, translation values are
+     * calculated to position the bar at the desired location.
      *
-     * @param restingPosition relative resting position of the bubble bar from the laid out position
+     * @param initialTranslation initial bubble bar translation at the start of drag
+     * @param location           desired location of the bubble bar when drag is released
+     * @return point with x and y values representing translation on x and y-axis
      */
-    @SuppressLint("RtlHardcoded")
-    void adjustRelativeRestingPosition(PointF restingPosition) {
-        final boolean locationOnLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
-        // Bubble bar is placed left or right with gravity. Check where it is currently.
-        final int absoluteGravity = Gravity.getAbsoluteGravity(
-                ((LayoutParams) getLayoutParams()).gravity, getLayoutDirection());
-        final boolean gravityOnLeft =
-                (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT;
-
-        // Bubble bar is pinned to the same side per gravity and the desired location.
-        // Resting translation does not need to be adjusted.
-        if (locationOnLeft == gravityOnLeft) {
-            return;
-        }
-
+    public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation,
+            BubbleBarLocation location) {
+        // Start with the initial translation. Value on y-axis can be reused.
+        final PointF dragEndTranslation = new PointF(initialTranslation);
         // Bubble bar is laid out on left or right side of the screen. And the desired new
-        // location is on the other side. Calculate x translation value required to shift the
+        // location is on the other side. Calculate x translation value required to shift
         // bubble bar from one side to the other.
-        float x = getDistanceFromOtherSide();
-        if (locationOnLeft) {
+        final float shift = getDistanceFromOtherSide();
+        if (location.isOnLeft(isLayoutRtl())) {
             // New location is on the left, shift left
             // before -> |......ooo.| after -> |.ooo......|
-            restingPosition.x = -x;
+            dragEndTranslation.x = -shift;
         } else {
             // New location is on the right, shift right
             // before -> |.ooo......| after -> |......ooo.|
-            restingPosition.x = x;
+            dragEndTranslation.x = shift;
         }
+        return dragEndTranslation;
     }
 
     private float getDistanceFromOtherSide() {
@@ -376,49 +364,40 @@
         return (float) (displayWidth - getWidth() - margin);
     }
 
-    private void setBubbleBarLocationInternal(BubbleBarLocation bubbleBarLocation) {
-        if (bubbleBarLocation != mBubbleBarLocation) {
-            mBubbleBarLocation = bubbleBarLocation;
-            if (mDragging) {
-                mLocationChangePending = true;
-            } else {
-                onBubbleBarLocationChanged();
-                invalidate();
-            }
-        }
-    }
-
-    private void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
-        if (bubbleBarLocation == mBubbleBarLocation) {
-            // nothing to do, already at expected location
-            return;
-        }
+    /**
+     * Animate bubble bar to the given location transiently. Does not modify the layout or the value
+     * returned by {@link #getBubbleBarLocation()}.
+     */
+    public void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
         if (mBubbleBarLocationAnimator != null && mBubbleBarLocationAnimator.isRunning()) {
+            mBubbleBarLocationAnimator.removeAllListeners();
             mBubbleBarLocationAnimator.cancel();
         }
 
         // Location animation uses two separate animators.
         // First animator hides the bar.
-        // After it completes, location update is sent to layout the bar in the new location.
+        // After it completes, bubble positions in the bar and arrow position is updated.
         // Second animator is started to show the bar.
-        mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator();
+        mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator(bubbleBarLocation);
         mBubbleBarLocationAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                // Bubble bar is not visible, update the location
-                setBubbleBarLocationInternal(bubbleBarLocation);
+                updateChildrenRenderNodeProperties(bubbleBarLocation);
+                mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl()));
+
                 // Animate it in
-                mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator();
+                mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator(bubbleBarLocation);
                 mBubbleBarLocationAnimator.start();
             }
         });
         mBubbleBarLocationAnimator.start();
     }
 
-    private AnimatorSet getLocationUpdateFadeOutAnimator() {
+    private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation bubbleBarLocation) {
         final float shift =
                 getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT;
-        final float tx = mBubbleBarLocation.isOnLeft(isLayoutRtl()) ? shift : -shift;
+        final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
+        final float tx = getTranslationX() + (onLeft ? shift : -shift);
 
         ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X, tx)
                 .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS);
@@ -433,14 +412,31 @@
         return animatorSet;
     }
 
-    private Animator getLocationUpdateFadeInAnimator() {
+    private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation animatedLocation) {
         final float shift =
                 getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT;
-        final float startTx = mBubbleBarLocation.isOnLeft(isLayoutRtl()) ? shift : -shift;
+
+        final boolean onLeft = animatedLocation.isOnLeft(isLayoutRtl());
+        final float startTx;
+        final float finalTx;
+        if (animatedLocation == mBubbleBarLocation) {
+            // Animated location matches layout location.
+            finalTx = 0;
+        } else {
+            // We are animating in to a transient location, need to move the bar accordingly.
+            finalTx = getDistanceFromOtherSide() * (onLeft ? -1 : 1);
+        }
+        if (onLeft) {
+            // Bar will be shown on the left side. Start point is shifted right.
+            startTx = finalTx + shift;
+        } else {
+            // Bar will be shown on the right side. Start point is shifted left.
+            startTx = finalTx - shift;
+        }
 
         ValueAnimator positionAnim = new SpringAnimationBuilder(getContext())
                 .setStartValue(startTx)
-                .setEndValue(0)
+                .setEndValue(finalTx)
                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
                 .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
                 .build(this, VIEW_TRANSLATE_X);
@@ -462,30 +458,6 @@
         return mBubbleBarBounds;
     }
 
-    /** Returns the bounds of the animating bubble, or {@code null} if no bubble is animating. */
-    @Nullable
-    public Rect getAnimatingBubbleBounds() {
-        if (mIsAnimatingNewBubble) {
-            return mAnimatingBubbleBounds;
-        }
-        return null;
-    }
-
-    /**
-     * Updates the animating bubble bounds. This should be called when the bubble is fully animated
-     * in so that we can include it in taskbar touchable region.
-     *
-     * <p>The bounds are adjusted to the coordinate space of BubbleBarView so that it can be used
-     * by taskbar.
-     */
-    public void updateAnimatingBubbleBounds(int left, int top, int width, int height) {
-        Rect bubbleBarBounds = getBubbleBarBounds();
-        mAnimatingBubbleBounds.left = bubbleBarBounds.left + left;
-        mAnimatingBubbleBounds.top = bubbleBarBounds.top + top;
-        mAnimatingBubbleBounds.right = mAnimatingBubbleBounds.left + width;
-        mAnimatingBubbleBounds.bottom = mAnimatingBubbleBounds.top + height;
-    }
-
     /**
      * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
      * respectively. If the value is not in range of 0 to 1 it will be normalized.
@@ -498,6 +470,11 @@
         requestLayout();
     }
 
+    /** Like {@link #setRelativePivot(float, float)} but only updates pivot y. */
+    public void setRelativePivotY(float y) {
+        setRelativePivot(mRelativePivotX, y);
+    }
+
     /**
      * Get current relative pivot for X axis
      */
@@ -512,38 +489,14 @@
         return mRelativePivotY;
     }
 
-    /** Prepares for animating a bubble while being stashed. */
-    public void prepareForAnimatingBubbleWhileStashed(String bubbleKey) {
+    /** Notifies the bubble bar that a new bubble animation is starting. */
+    public void onAnimatingBubbleStarted() {
         mIsAnimatingNewBubble = true;
-        // we're about to animate the new bubble in. the new bubble has already been added to this
-        // view, but we're currently stashed, so before we can start the animation we need make
-        // everything else in the bubble bar invisible, except for the bubble that's being animated.
-        setBackground(null);
-        for (int i = 0; i < getChildCount(); i++) {
-            final BubbleView view = (BubbleView) getChildAt(i);
-            final String key = view.getBubble().getKey();
-            if (!bubbleKey.equals(key)) {
-                view.setVisibility(INVISIBLE);
-            }
-        }
-        setVisibility(VISIBLE);
-        setAlpha(1);
-        setTranslationY(0);
-        setScaleX(1);
-        setScaleY(1);
     }
 
-    /** Resets the state after the bubble animation completed. */
+    /** Notifies the bubble bar that a new bubble animation is complete. */
     public void onAnimatingBubbleCompleted() {
         mIsAnimatingNewBubble = false;
-        // setting the background triggers relayout so no need to explicitly invalidate after the
-        // animation
-        setBackground(mBubbleBarBackground);
-        for (int i = 0; i < getChildCount(); i++) {
-            final BubbleView view = (BubbleView) getChildAt(i);
-            view.setVisibility(VISIBLE);
-            view.setAlpha(1f);
-        }
     }
 
     // TODO: (b/280605790) animate it
@@ -577,7 +530,7 @@
 
     private void updateLayoutParams() {
         LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
-        lp.height = getBubbleBarHeight();
+        lp.height = (int) getBubbleBarExpandedHeight();
         lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
         setLayoutParams(lp);
     }
@@ -592,13 +545,7 @@
      * Updates the z order, positions, and badge visibility of the bubble views in the bar based
      * on the expanded state.
      */
-    private void updateChildrenRenderNodeProperties() {
-        if (mIsAnimatingNewBubble) {
-            // don't update bubbles if a new bubble animation is playing.
-            // the bubble bar will redraw itself via onLayout after the animation.
-            return;
-        }
-
+    private void updateChildrenRenderNodeProperties(BubbleBarLocation bubbleBarLocation) {
         final float widthState = (float) mWidthAnimator.getAnimatedValue();
         final float currentWidth = getWidth();
         final float expandedWidth = expandedWidth();
@@ -606,7 +553,7 @@
         int bubbleCount = getChildCount();
         final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
         final boolean animate = getVisibility() == VISIBLE;
-        final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
+        final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
         // elevation state is opposite to widthState - when expanded all icons are flat
         float elevationState = (1 - widthState);
         for (int i = 0; i < bubbleCount; i++) {
@@ -664,8 +611,9 @@
         }
 
         // update the arrow position
-        final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed();
-        final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded();
+        final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed(
+                bubbleBarLocation);
+        final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(bubbleBarLocation);
         final float interpolatedWidth =
                 widthState * (expandedWidth - collapsedWidth) + collapsedWidth;
         final float arrowPosition;
@@ -712,7 +660,7 @@
                     addViewInLayout(child, i, child.getLayoutParams());
                 }
             }
-            updateChildrenRenderNodeProperties();
+            updateChildrenRenderNodeProperties(mBubbleBarLocation);
         }
     }
 
@@ -753,7 +701,7 @@
             return;
         }
         // Find the center of the bubble when it's expanded, set the arrow position to it.
-        final float tx = arrowPositionForSelectedWhenExpanded();
+        final float tx = arrowPositionForSelectedWhenExpanded(mBubbleBarLocation);
         final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX();
         if (tx == currentArrowPosition) {
             // arrow position remains unchanged
@@ -778,10 +726,10 @@
         }
     }
 
-    private float arrowPositionForSelectedWhenExpanded() {
+    private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) {
         final int index = indexOfChild(mSelectedBubbleView);
         final int bubblePosition;
-        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+        if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
             // Bubble positions are reversed. First bubble is on the right.
             bubblePosition = getChildCount() - index - 1;
         } else {
@@ -791,10 +739,10 @@
                 + mIconSize / 2f;
     }
 
-    private float arrowPositionForSelectedWhenCollapsed() {
+    private float arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation) {
         final int index = indexOfChild(mSelectedBubbleView);
         final int bubblePosition;
-        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+        if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
             // Bubble positions are reversed. First bubble may be shifted, if there are more
             // bubbles than the current bubble and overflow.
             bubblePosition = index == 0 && getChildCount() > 2 ? 1 : 0;
@@ -864,8 +812,13 @@
                 : mIconSize + horizontalPadding;
     }
 
-    private int getBubbleBarHeight() {
-        return (int) (mIconSize + mBubbleBarPadding * 2 + mPointerSize);
+    private float getBubbleBarExpandedHeight() {
+        return getBubbleBarCollapsedHeight() + mPointerSize;
+    }
+
+    float getBubbleBarCollapsedHeight() {
+        // the pointer is invisible when collapsed
+        return mIconSize + mBubbleBarPadding * 2;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 3c46f32..dc48a66 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -189,6 +189,10 @@
         return mBubbleBarTranslationY;
     }
 
+    float getBubbleBarCollapsedHeight() {
+        return mBarView.getBubbleBarCollapsedHeight();
+    }
+
     /**
      * Whether the bubble bar is visible or not.
      */
@@ -211,8 +215,17 @@
     /**
      * Update bar {@link BubbleBarLocation}
      */
-    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, boolean animate) {
-        mBarView.setBubbleBarLocation(bubbleBarLocation, animate);
+    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+        mBarView.setBubbleBarLocation(bubbleBarLocation);
+    }
+
+    /**
+     * Animate bubble bar to the given location. The location change is transient. It does not
+     * update the state of the bubble bar.
+     * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
+     */
+    public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+        mBarView.animateToBubbleBarLocation(bubbleBarLocation);
     }
 
     /**
@@ -222,10 +235,9 @@
         return mBarView.getBubbleBarBounds();
     }
 
-    /** The bounds of the animating bubble, or {@code null} if no bubble is animating. */
-    @Nullable
-    public Rect getAnimatingBubbleBounds() {
-        return mBarView.getAnimatingBubbleBounds();
+    /** Whether a new bubble is animating. */
+    public boolean isAnimatingNewBubble() {
+        return mBarView.isAnimatingNewBubble();
     }
 
     /** The horizontal margin of the bubble bar from the edge of the screen. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
index 8b811d9..49f114a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -110,25 +110,25 @@
     /**
      * Animates the dragged bubble movement back to the initial position.
      *
-     * @param initialPosition the position to animate to
+     * @param restingPosition the position to animate to
      * @param velocity        the initial velocity to use for the spring animation
      * @param endActions      gets called when the animation completes or gets cancelled
      */
-    public void animateToInitialState(@NonNull PointF initialPosition, @NonNull PointF velocity,
+    public void animateToRestingState(@NonNull PointF restingPosition, @NonNull PointF velocity,
             @Nullable Runnable endActions) {
         mBubbleAnimator.cancel();
         mBubbleAnimator
                 .spring(DynamicAnimation.SCALE_X, 1f)
                 .spring(DynamicAnimation.SCALE_Y, 1f)
-                .spring(DynamicAnimation.TRANSLATION_X, initialPosition.x, velocity.x,
+                .spring(DynamicAnimation.TRANSLATION_X, restingPosition.x, velocity.x,
                         mTranslationConfig)
-                .spring(DynamicAnimation.TRANSLATION_Y, initialPosition.y, velocity.y,
+                .spring(DynamicAnimation.TRANSLATION_Y, restingPosition.y, velocity.y,
                         mTranslationConfig)
                 .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
                         boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
                         boolean allRelevantPropertyAnimationsEnded) -> {
                     if (canceled || allRelevantPropertyAnimationsEnded) {
-                        resetAnimatedViews(initialPosition);
+                        resetAnimatedViews(restingPosition);
                         if (endActions != null) {
                             endActions.run();
                         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 5ffc6d8..d1c9da7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -26,6 +26,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 
 /**
  * Controls bubble bar drag interactions.
@@ -37,6 +39,7 @@
  */
 public class BubbleDragController {
     private final TaskbarActivityContext mActivity;
+    private BubbleBarController mBubbleBarController;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleDismissController mBubbleDismissController;
     private BubbleBarPinController mBubbleBarPinController;
@@ -51,11 +54,10 @@
      * controllers may still be waiting for init().
      */
     public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarController = bubbleControllers.bubbleBarController;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleDismissController = bubbleControllers.bubbleDismissController;
         mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
-        mBubbleBarPinController.setListener(
-                bubbleControllers.bubbleBarController::updateBubbleBarLocation);
         mBubbleDismissController.setListener(
                 stuck -> mBubbleBarPinController.setDropTargetHidden(stuck));
     }
@@ -96,6 +98,17 @@
         PointF initialRelativePivot = new PointF();
         bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
 
+            @Nullable
+            private BubbleBarLocation mReleasedLocation;
+
+            private final LocationChangeListener mLocationChangeListener =
+                    new LocationChangeListener() {
+                        @Override
+                        public void onRelease(@NonNull BubbleBarLocation location) {
+                            mReleasedLocation = location;
+                        }
+                    };
+
             @Override
             protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
                 if (bubbleBarView.isExpanded()) return false;
@@ -104,6 +117,7 @@
 
             @Override
             void onDragStart() {
+                mBubbleBarPinController.setListener(mLocationChangeListener);
                 initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
                         bubbleBarView.getRelativePivotY());
                 // By default the bubble bar view pivot is in bottom right corner, while dragging
@@ -134,13 +148,17 @@
                 // Restoring the initial pivot for the bubble bar view
                 bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
                 bubbleBarView.setIsDragging(false);
+                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
             }
 
             @Override
             protected PointF getRestingPosition() {
-                PointF restingPosition = super.getRestingPosition();
-                bubbleBarView.adjustRelativeRestingPosition(restingPosition);
-                return restingPosition;
+                if (mReleasedLocation == null
+                        || mReleasedLocation == bubbleBarView.getBubbleBarLocation()) {
+                    return getInitialPosition();
+                }
+                return bubbleBarView.getBubbleBarDragReleaseTranslation(getInitialPosition(),
+                        mReleasedLocation);
             }
         });
     }
@@ -229,6 +247,13 @@
         }
 
         /**
+         * Get the initial position of the view when drag started
+         */
+        protected PointF getInitialPosition() {
+            return mViewInitialPosition;
+        }
+
+        /**
          * Get the resting position of the view when drag is released
          */
         protected PointF getRestingPosition() {
@@ -362,7 +387,7 @@
                 mAnimator.animateDismiss(mViewInitialPosition, onComplete);
             } else {
                 onDragRelease();
-                mAnimator.animateToInitialState(getRestingPosition(), getCurrentVelocity(),
+                mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(),
                         onComplete);
             }
             mBubbleDismissController.hideDismissView();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index bea0af8..f689a05 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -25,6 +25,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.StashedHandleViewController;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -77,11 +78,16 @@
     private boolean mBubblesShowingOnOverview;
     private boolean mIsSysuiLocked;
 
+    private final float mHandleCenterFromScreenBottom;
+
     @Nullable
     private AnimatorSet mAnimator;
 
     public BubbleStashController(TaskbarActivityContext activity) {
         mActivity = activity;
+        // the handle is centered within the stashed taskbar area
+        mHandleCenterFromScreenBottom =
+                mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f;
     }
 
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
@@ -266,7 +272,6 @@
      */
     private AnimatorSet createStashAnimator(boolean isStashed, long duration) {
         AnimatorSet animatorSet = new AnimatorSet();
-        final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f;
 
         AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
         // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
@@ -280,7 +285,8 @@
             firstHalfDurationScale = 0.75f;
             secondHalfDurationScale = 0.5f;
 
-            fullLengthAnimatorSet.play(mIconTranslationYForStash.animateToValue(stashTranslation));
+            fullLengthAnimatorSet.play(
+                    mIconTranslationYForStash.animateToValue(getStashTranslation()));
 
             firstHalfAnimatorSet.playTogether(
                     mIconAlphaForStash.animateToValue(0),
@@ -329,6 +335,10 @@
         return animatorSet;
     }
 
+    private float getStashTranslation() {
+        return (mUnstashedHeight - mStashedHeight) / 2f;
+    }
+
     private void onIsStashedChanged() {
         mControllers.runAfterInit(() -> {
             mHandleViewController.onIsStashedChanged();
@@ -336,7 +346,7 @@
         });
     }
 
-    private float getBubbleBarTranslationYForTaskbar() {
+    public float getBubbleBarTranslationYForTaskbar() {
         return -mActivity.getDeviceProfile().taskbarBottomMargin;
     }
 
@@ -355,6 +365,25 @@
                 : getBubbleBarTranslationYForTaskbar();
     }
 
+    /**
+     * The difference on the Y axis between the center of the handle and the center of the bubble
+     * bar.
+     */
+    public float getDiffBetweenHandleAndBarCenters() {
+        // the difference between the centers of the handle and the bubble bar is the difference
+        // between their distance from the bottom of the screen.
+
+        float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f;
+        return mHandleCenterFromScreenBottom - barCenter;
+    }
+
+    /** The distance the handle moves as part of the new bubble animation. */
+    public float getStashedHandleTranslationForNewBubbleAnimation() {
+        // the should move up to the top of the stashed taskbar area. it is centered within it so
+        // it should move the same distance as it is away from the bottom.
+        return -mHandleCenterFromScreenBottom;
+    }
+
     /** Checks whether the motion event is over the stash handle. */
     public boolean isEventOverStashHandle(MotionEvent ev) {
         return mHandleViewController.isEventOverHandle(ev);
@@ -365,11 +394,6 @@
         mHandleViewController.setBubbleBarLocation(bubbleBarLocation);
     }
 
-    /** Returns the x position of the center of the stashed handle. */
-    public float getStashedHandleCenterX() {
-        return mHandleViewController.getStashedHandleCenterX();
-    }
-
     /** Returns the [PhysicsAnimator] for the stashed handle view. */
     public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
         return mHandleViewController.getPhysicsAnimator();
@@ -389,4 +413,14 @@
         mIsStashed = false;
         onIsStashedChanged();
     }
+
+    /** Stashes the bubble bar immediately without animation. */
+    public void stashBubbleBarImmediate() {
+        mHandleViewController.setTranslationYForSwipe(0);
+        mIconAlphaForStash.setValue(0);
+        mIconTranslationYForStash.updateValue(getStashTranslation());
+        mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
+        mIsStashed = true;
+        onIsStashedChanged();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 6f1a093..91103d7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -251,11 +251,6 @@
         return mStashedHandleAlpha;
     }
 
-    /** Returns the x position of the center of the stashed handle. */
-    public float getStashedHandleCenterX() {
-        return mStashedHandleBounds.exactCenterX();
-    }
-
     /**
      * Creates and returns an Animator that updates the stashed handle  shape and size.
      * When stashed, the shape is a thin rounded pill. When unstashed, the shape morphs into
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index da36944..a6d0ff8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -40,20 +40,8 @@
     private companion object {
         /** The time to show the flyout. */
         const val FLYOUT_DELAY_MS: Long = 2500
-        /** The translation Y the new bubble will animate to. */
-        const val BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y = -50f
-        /** The initial translation Y value the new bubble is set to before the animation starts. */
-        // TODO(liranb): get rid of this and calculate this based on the y-distance between the
-        // bubble and the stash handle.
-        const val BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET = 50f
         /** The initial scale Y value that the new bubble is set to before the animation starts. */
         const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
-        /**
-         * The distance the stashed handle will travel as it gets hidden as part of the new bubble
-         * animation.
-         */
-        // TODO(liranb): calculate this based on the position of the views
-        const val BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y = -20f
     }
 
     /** Wrapper around the animating bubble with its show and hide animations. */
@@ -105,166 +93,156 @@
         if (animator.isRunning()) animator.cancel()
         // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
         // and the second part hides it after a delay.
-        val showAnimation = buildShowAnimation(bubbleView, b.key)
-        val hideAnimation = buildHideAnimation(bubbleView)
+        val showAnimation = buildShowAnimation()
+        val hideAnimation = buildHideAnimation()
         animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
     /**
-     * Returns a lambda that starts the animation that shows the new bubble.
+     * Returns a [Runnable] that starts the animation that shows the new or updated bubble.
      *
      * Visually, the animation is divided into 2 parts. The stash handle starts animating up and
-     * fading out and then the bubble starts animating up and fading in.
+     * fading out and then the bubble bar starts animating up and fading in.
      *
-     * To make the transition from the handle to the bubble smooth, the positions and movement of
-     * the 2 views must be synchronized. To do that we use a single spring path along the Y axis,
-     * starting from the handle's position to the eventual bubble's position. The path is split into
-     * 3 parts.
+     * To make the transition from the handle to the bar smooth, the positions and movement of the 2
+     * views must be synchronized. To do that we use a single spring path along the Y axis, starting
+     * from the handle's position to the eventual bar's position. The path is split into 3 parts.
      * 1. In the first part, we only animate the handle.
-     * 1. In the second part the handle is fully hidden, and the bubble is animating in.
-     * 1. The third part is the overshoot of the spring animation, where we make the bubble fully
+     * 2. In the second part the handle is fully hidden, and the bubble bar is animating in.
+     * 3. The third part is the overshoot of the spring animation, where we make the bubble fully
      *    visible which helps avoiding further updates when we re-enter the second part.
      */
-    private fun buildShowAnimation(
-        bubbleView: BubbleView,
-        key: String,
-    ) = Runnable {
-        bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
-        // calculate the initial translation x the bubble should have in order to align it with the
-        // stash handle.
-        val initialTranslationX =
-            bubbleStashController.stashedHandleCenterX - bubbleView.centerXOnScreen
-        // prepare the bubble for the animation
-        bubbleView.alpha = 0f
-        bubbleView.translationX = initialTranslationX
-        bubbleView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
-        bubbleView.visibility = VISIBLE
+    private fun buildShowAnimation() = Runnable {
+        // prepare the bubble bar for the animation
+        bubbleBarView.onAnimatingBubbleStarted()
+        bubbleBarView.visibility = VISIBLE
+        bubbleBarView.alpha = 0f
+        bubbleBarView.translationY = 0f
+        bubbleBarView.scaleX = 1f
+        bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
+        bubbleBarView.relativePivotY = 0.5f
+
+        // this is the offset between the center of the bubble bar and the center of the stash
+        // handle. when the handle becomes invisible and we start animating in the bubble bar,
+        // the translation y is offset by this value to make the transition from the handle to the
+        // bar smooth.
+        val offset = bubbleStashController.diffBetweenHandleAndBarCenters
+        val stashedHandleTranslationY =
+            bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
 
         // this is the total distance that both the stashed handle and the bubble will be traveling
-        val totalTranslationY =
-            BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y + BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
+        // at the end of the animation the bubble bar will be positioned in the same place when it
+        // shows while we're in an app.
+        val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
         val animator = bubbleStashController.stashedHandlePhysicsAnimator
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
-        animator.addUpdateListener { target, values ->
+        animator.addUpdateListener { handle, values ->
             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
             when {
-                ty >= BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y -> {
+                ty >= stashedHandleTranslationY -> {
                     // we're in the first leg of the animation. only animate the handle. the bubble
-                    // remains hidden during this part of the animation
+                    // bar remains hidden during this part of the animation
 
-                    // map the path [0, BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y] to [0,1]
-                    val fraction = ty / BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
-                    target.alpha = 1 - fraction
+                    // map the path [0, stashedHandleTranslationY] to [0,1]
+                    val fraction = ty / stashedHandleTranslationY
+                    handle.alpha = 1 - fraction
                 }
                 ty >= totalTranslationY -> {
                     // this is the second leg of the animation. the handle should be completely
-                    // hidden and the bubble should start animating in.
+                    // hidden and the bubble bar should start animating in.
                     // it's possible that we're re-entering this leg because this is a spring
-                    // animation, so only set the alpha and scale for the bubble if we didn't
+                    // animation, so only set the alpha and scale for the bubble bar if we didn't
                     // already fully animate in.
-                    target.alpha = 0f
-                    bubbleView.translationY = ty + BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET
-                    if (bubbleView.alpha != 1f) {
-                        // map the path
-                        // [BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y, totalTranslationY]
-                        // to [0, 1]
+                    handle.alpha = 0f
+                    bubbleBarView.translationY = ty - offset
+                    if (bubbleBarView.alpha != 1f) {
+                        // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
                         val fraction =
-                            (ty - BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y) /
-                                BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y
-                        bubbleView.alpha = fraction
-                        bubbleView.scaleY =
+                            (ty - stashedHandleTranslationY) /
+                                (totalTranslationY - stashedHandleTranslationY)
+                        bubbleBarView.alpha = fraction
+                        bubbleBarView.scaleY =
                             BUBBLE_ANIMATION_INITIAL_SCALE_Y +
                                 (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
                     }
                 }
                 else -> {
                     // we're past the target animated value, set the alpha and scale for the bubble
-                    // so that it's fully visible and no longer changing, but keep moving it along
-                    // the animation path
-                    bubbleView.alpha = 1f
-                    bubbleView.scaleY = 1f
-                    bubbleView.translationY = ty + BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET
+                    // bar so that it's fully visible and no longer changing, but keep moving it
+                    // along the animation path
+                    bubbleBarView.alpha = 1f
+                    bubbleBarView.scaleY = 1f
+                    bubbleBarView.translationY = ty - offset
                 }
             }
         }
         animator.addEndListener { _, _, _, _, _, _, _ ->
-            // the bubble is now fully settled in. make it touchable
-            bubbleBarView.updateAnimatingBubbleBounds(
-                bubbleView.left,
-                bubbleView.top,
-                bubbleView.width,
-                bubbleView.height
-            )
+            // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
         animator.start()
     }
 
     /**
-     * Returns a lambda that starts the animation that hides the new bubble.
+     * Returns a [Runnable] that starts the animation that hides the bubble bar.
      *
      * Similarly to the show animation, this is visually divided into 2 parts. We first animate the
-     * bubble out, and then animate the stash handle in. At the end of the animation we reset the
-     * values of the bubble.
+     * bubble bar out, and then animate the stash handle in. At the end of the animation we reset
+     * values of the bubble bar.
      *
      * This is a spring animation that goes along the same path of the show animation in the
      * opposite order, and is split into 3 parts:
      * 1. In the first part the bubble animates out.
-     * 1. In the second part the bubble is fully hidden and the handle animates in.
-     * 1. The third part is the overshoot. The handle is made fully visible.
+     * 2. In the second part the bubble bar is fully hidden and the handle animates in.
+     * 3. The third part is the overshoot. The handle is made fully visible.
      */
-    private fun buildHideAnimation(bubbleView: BubbleView) = Runnable {
-        // this is the total distance that both the stashed handle and the bubble will be traveling
-        val totalTranslationY =
-            BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y + BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
+    private fun buildHideAnimation() = Runnable {
+        val offset = bubbleStashController.diffBetweenHandleAndBarCenters
+        val stashedHandleTranslationY =
+            bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
+        // this is the total distance that both the stashed handle and the bar will be traveling
+        val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
         val animator = bubbleStashController.stashedHandlePhysicsAnimator
         animator.setDefaultSpringConfig(springConfig)
         animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
-        animator.addUpdateListener { target, values ->
+        animator.addUpdateListener { handle, values ->
             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
             when {
-                ty <= BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y -> {
-                    // this is the first leg of the animation. only animate the bubble. the handle
-                    // is hidden during this part
-                    bubbleView.translationY = ty + BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET
-                    // map the path
-                    // [totalTranslationY, BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y]
-                    // to [0, 1]
-                    val fraction = (totalTranslationY - ty) / BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y
-                    bubbleView.alpha = 1 - fraction / 2
-                    bubbleView.scaleY = 1 - (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
+                ty <= stashedHandleTranslationY -> {
+                    // this is the first leg of the animation. only animate the bubble bar. the
+                    // handle is hidden during this part
+                    bubbleBarView.translationY = ty - offset
+                    // map the path [totalTranslationY, stashedHandleTranslationY] to [0, 1]
+                    val fraction =
+                        (totalTranslationY - ty) / (totalTranslationY - stashedHandleTranslationY)
+                    bubbleBarView.alpha = 1 - fraction
+                    bubbleBarView.scaleY = 1 - (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
                 }
                 ty <= 0 -> {
-                    // this is the second part of the animation. make the bubble invisible and
+                    // this is the second part of the animation. make the bubble bar invisible and
                     // start fading in the handle, but don't update the alpha if it's already fully
                     // visible
-                    bubbleView.alpha = 0f
-                    if (target.alpha != 1f) {
-                        // map the path [BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y, 0] to [0, 1]
-                        val fraction =
-                            (BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y - ty) /
-                                BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
-                        target.alpha = fraction
+                    bubbleBarView.alpha = 0f
+                    if (handle.alpha != 1f) {
+                        // map the path [stashedHandleTranslationY, 0] to [0, 1]
+                        val fraction = (stashedHandleTranslationY - ty) / stashedHandleTranslationY
+                        handle.alpha = fraction
                     }
                 }
                 else -> {
                     // we reached the target value. set the alpha of the handle to 1
-                    target.alpha = 1f
+                    handle.alpha = 1f
                 }
             }
         }
         animator.addEndListener { _, _, _, _, _, _, _ ->
             animatingBubble = null
-            bubbleView.alpha = 0f
-            bubbleView.translationY = 0f
-            bubbleView.scaleY = 1f
-            if (bubbleStashController.isStashed) {
-                bubbleBarView.alpha = 0f
-            }
+            bubbleStashController.stashBubbleBarImmediate()
             bubbleBarView.onAnimatingBubbleCompleted()
+            bubbleBarView.relativePivotY = 1f
             bubbleStashController.updateTaskbarTouchRegion()
         }
         animator.start()
@@ -275,14 +253,7 @@
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
         bubbleBarView.onAnimatingBubbleCompleted()
+        bubbleBarView.relativePivotY = 1f
         animatingBubble = null
     }
 }
-
-/** The X position in screen coordinates of the center of the bubble. */
-private val BubbleView.centerXOnScreen: Float
-    get() {
-        val screenCoordinates = IntArray(2)
-        getLocationOnScreen(screenCoordinates)
-        return screenCoordinates[0] + width / 2f
-    }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
index fe91362..8ad2493 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
@@ -27,7 +27,6 @@
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
-import com.android.systemui.shared.rotation.RotationButton
 
 /**
  * Meant to be a simple container for data subclasses will need
@@ -41,14 +40,13 @@
  * @property startContextualContainer ViewGroup that holds the start contextual button (ex, A11y).
  */
 abstract class AbstractNavButtonLayoutter(
-        val resources: Resources,
-        val navButtonContainer: LinearLayout,
-        protected val endContextualContainer: ViewGroup,
-        protected val startContextualContainer: ViewGroup,
-        protected val imeSwitcher: ImageView?,
-        protected val rotationButton: RotationButton?,
-        protected val a11yButton: ImageView?,
-        protected val space: Space?
+    val resources: Resources,
+    val navButtonContainer: LinearLayout,
+    protected val endContextualContainer: ViewGroup,
+    protected val startContextualContainer: ViewGroup,
+    protected val imeSwitcher: ImageView?,
+    protected val a11yButton: ImageView?,
+    protected val space: Space?
 ) : NavButtonLayoutter {
     protected val homeButton: ImageView? = navButtonContainer.findViewById(R.id.home)
     protected val recentsButton: ImageView? = navButtonContainer.findViewById(R.id.recent_apps)
@@ -66,17 +64,24 @@
     }
 
     fun getParamsToCenterView(): FrameLayout.LayoutParams {
-        val params = FrameLayout.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        val params =
+            FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT
+            )
         params.gravity = Gravity.CENTER
-        return params;
+        return params
     }
 
-    open fun repositionContextualContainer(contextualContainer: ViewGroup, buttonSize: Int,
-                                           barAxisMarginStart: Int, barAxisMarginEnd: Int,
-                                           gravity: Int) {
-        val contextualContainerParams = FrameLayout.LayoutParams(
-                buttonSize, ViewGroup.LayoutParams.MATCH_PARENT)
+    open fun repositionContextualContainer(
+        contextualContainer: ViewGroup,
+        buttonSize: Int,
+        barAxisMarginStart: Int,
+        barAxisMarginEnd: Int,
+        gravity: Int
+    ) {
+        val contextualContainerParams =
+            FrameLayout.LayoutParams(buttonSize, ViewGroup.LayoutParams.MATCH_PARENT)
         contextualContainerParams.apply {
             marginStart = barAxisMarginStart
             marginEnd = barAxisMarginEnd
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
index 4368b95..aa8f5a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
@@ -29,27 +29,24 @@
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.*
-import com.android.systemui.shared.rotation.RotationButton
 
 class KidsNavLayoutter(
-        resources: Resources,
-        navBarContainer: LinearLayout,
-        endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup,
-        imeSwitcher: ImageView?,
-        rotationButton: RotationButton?,
-        a11yButton: ImageView?,
-        space: Space?
+    resources: Resources,
+    navBarContainer: LinearLayout,
+    endContextualContainer: ViewGroup,
+    startContextualContainer: ViewGroup,
+    imeSwitcher: ImageView?,
+    a11yButton: ImageView?,
+    space: Space?
 ) :
     AbstractNavButtonLayoutter(
-            resources,
-            navBarContainer,
-            endContextualContainer,
-            startContextualContainer,
-            imeSwitcher,
-            rotationButton,
-            a11yButton,
-            space
+        resources,
+        navBarContainer,
+        endContextualContainer,
+        startContextualContainer,
+        imeSwitcher,
+        a11yButton,
+        space
     ) {
 
     override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
@@ -105,11 +102,16 @@
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        val contextualMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_contextual_button_padding)
+        val contextualMargin =
+            resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_padding)
         repositionContextualContainer(endContextualContainer, WRAP_CONTENT, 0, 0, Gravity.END)
-        repositionContextualContainer(startContextualContainer, WRAP_CONTENT, contextualMargin,
-                contextualMargin, Gravity.START)
+        repositionContextualContainer(
+            startContextualContainer,
+            WRAP_CONTENT,
+            contextualMargin,
+            contextualMargin,
+            Gravity.START
+        )
 
         if (imeSwitcher != null) {
             startContextualContainer.addView(imeSwitcher)
@@ -119,9 +121,5 @@
             endContextualContainer.addView(a11yButton)
             a11yButton.layoutParams = getParamsToCenterView()
         }
-        if (rotationButton != null) {
-            endContextualContainer.addView(rotationButton.currentView)
-            rotationButton.currentView.layoutParams = getParamsToCenterView()
-        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 5bfdce9..1e9f09b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -25,10 +25,11 @@
 import android.widget.Space
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.*
+import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_CONTEXTUAL_BUTTONS
+import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_NAV_BUTTONS
+import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_START_CONTEXTUAL_BUTTONS
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.Companion
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
-import com.android.systemui.shared.rotation.RotationButton
 
 /**
  * Select the correct layout for nav buttons
@@ -58,7 +59,6 @@
             deviceProfile: DeviceProfile,
             navButtonsView: NearestTouchFrame,
             imeSwitcher: ImageView?,
-            rotationButton: RotationButton?,
             a11yButton: ImageView?,
             space: Space?,
             resources: Resources,
@@ -86,7 +86,6 @@
                             endContextualContainer,
                             startContextualContainer,
                             imeSwitcher,
-                            rotationButton,
                             a11yButton,
                             space
                         )
@@ -98,7 +97,6 @@
                             endContextualContainer,
                             startContextualContainer,
                             imeSwitcher,
-                            rotationButton,
                             a11yButton,
                             space
                         )
@@ -110,7 +108,6 @@
                             endContextualContainer,
                             startContextualContainer,
                             imeSwitcher,
-                            rotationButton,
                             a11yButton,
                             space
                         )
@@ -123,7 +120,6 @@
                         endContextualContainer,
                         startContextualContainer,
                         imeSwitcher,
-                        rotationButton,
                         a11yButton,
                         space
                     )
@@ -138,7 +134,6 @@
                                 endContextualContainer,
                                 startContextualContainer,
                                 imeSwitcher,
-                                rotationButton,
                                 a11yButton,
                                 space
                             )
@@ -150,7 +145,6 @@
                                 endContextualContainer,
                                 startContextualContainer,
                                 imeSwitcher,
-                                rotationButton,
                                 a11yButton,
                                 space
                             )
@@ -162,7 +156,6 @@
                                 endContextualContainer,
                                 startContextualContainer,
                                 imeSwitcher,
-                                rotationButton,
                                 a11yButton,
                                 space
                             )
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
index bf820c0..8d91f2c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -22,29 +22,26 @@
 import android.widget.LinearLayout
 import android.widget.Space
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.systemui.shared.rotation.RotationButton
 
 /** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
 class PhoneGestureLayoutter(
-        resources: Resources,
-        navBarContainer: LinearLayout,
-        endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup,
-        imeSwitcher: ImageView?,
-        rotationButton: RotationButton?,
-        a11yButton: ImageView?,
-        space: Space?
+    resources: Resources,
+    navBarContainer: LinearLayout,
+    endContextualContainer: ViewGroup,
+    startContextualContainer: ViewGroup,
+    imeSwitcher: ImageView?,
+    a11yButton: ImageView?,
+    space: Space?
 ) :
-        AbstractNavButtonLayoutter(
-                resources,
-                navBarContainer,
-                endContextualContainer,
-                startContextualContainer,
-                imeSwitcher,
-                rotationButton,
-                a11yButton,
-                space
-        ) {
+    AbstractNavButtonLayoutter(
+        resources,
+        navBarContainer,
+        endContextualContainer,
+        startContextualContainer,
+        imeSwitcher,
+        a11yButton,
+        space
+    ) {
 
     override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         endContextualContainer.removeAllViews()
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 6a935f1..9f7f07e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -26,35 +26,32 @@
 import android.widget.Space
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.systemui.shared.rotation.RotationButton
 
 open class PhoneLandscapeNavLayoutter(
-        resources: Resources,
-        navBarContainer: LinearLayout,
-        endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup,
-        imeSwitcher: ImageView?,
-        rotationButton: RotationButton?,
-        a11yButton: ImageView?,
-        space: Space?
+    resources: Resources,
+    navBarContainer: LinearLayout,
+    endContextualContainer: ViewGroup,
+    startContextualContainer: ViewGroup,
+    imeSwitcher: ImageView?,
+    a11yButton: ImageView?,
+    space: Space?
 ) :
     AbstractNavButtonLayoutter(
-            resources,
-            navBarContainer,
-            endContextualContainer,
-            startContextualContainer,
-            imeSwitcher,
-            rotationButton,
-            a11yButton,
-            space
+        resources,
+        navBarContainer,
+        endContextualContainer,
+        startContextualContainer,
+        imeSwitcher,
+        a11yButton,
+        space
     ) {
 
     override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         val totalHeight = context.deviceProfile.heightPx
-        val homeButtonHeight = resources.getDimensionPixelSize(
-                R.dimen.taskbar_phone_home_button_size)
-        val roundedCornerContentMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val homeButtonHeight =
+            resources.getDimensionPixelSize(R.dimen.taskbar_phone_home_button_size)
+        val roundedCornerContentMargin =
+            resources.getDimensionPixelSize(R.dimen.taskbar_phone_rounded_corner_content_margin)
         val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
         val contentWidth = totalHeight - roundedCornerContentMargin * 2 - contentPadding * 2
 
@@ -63,13 +60,13 @@
         val sideButtonHeight = contextualButtonHeight * 2
         val navButtonContainerHeight = contentWidth - contextualButtonHeight * 2
 
-        val navContainerParams = FrameLayout.LayoutParams(
-                MATCH_PARENT, navButtonContainerHeight.toInt())
+        val navContainerParams =
+            FrameLayout.LayoutParams(MATCH_PARENT, navButtonContainerHeight.toInt())
         navContainerParams.apply {
             topMargin =
-                    (contextualButtonHeight + contentPadding + roundedCornerContentMargin).toInt()
+                (contextualButtonHeight + contentPadding + roundedCornerContentMargin).toInt()
             bottomMargin =
-                    (contextualButtonHeight + contentPadding + roundedCornerContentMargin).toInt()
+                (contextualButtonHeight + contentPadding + roundedCornerContentMargin).toInt()
             marginEnd = 0
             marginStart = 0
         }
@@ -84,8 +81,8 @@
         navButtonContainer.gravity = Gravity.CENTER
 
         // Add the spaces in between the nav buttons
-        val spaceInBetween = (navButtonContainerHeight - homeButtonHeight -
-                sideButtonHeight * 2) / 2.0f
+        val spaceInBetween =
+            (navButtonContainerHeight - homeButtonHeight - sideButtonHeight * 2) / 2.0f
         for (i in 0 until navButtonContainer.childCount) {
             val navButton = navButtonContainer.getChildAt(i)
             val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams
@@ -124,13 +121,23 @@
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        val roundedCornerContentMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val roundedCornerContentMargin =
+            resources.getDimensionPixelSize(R.dimen.taskbar_phone_rounded_corner_content_margin)
         val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
-        repositionContextualContainer(startContextualContainer, buttonSize,
-                roundedCornerContentMargin + contentPadding, 0, Gravity.TOP)
-        repositionContextualContainer(endContextualContainer, buttonSize,
-                0, roundedCornerContentMargin + contentPadding, Gravity.BOTTOM)
+        repositionContextualContainer(
+            startContextualContainer,
+            buttonSize,
+            roundedCornerContentMargin + contentPadding,
+            0,
+            Gravity.TOP
+        )
+        repositionContextualContainer(
+            endContextualContainer,
+            buttonSize,
+            0,
+            roundedCornerContentMargin + contentPadding,
+            Gravity.BOTTOM
+        )
 
         if (imeSwitcher != null) {
             startContextualContainer.addView(imeSwitcher)
@@ -140,16 +147,16 @@
             startContextualContainer.addView(a11yButton)
             a11yButton.layoutParams = getParamsToCenterView()
         }
-        if (rotationButton != null) {
-            startContextualContainer.addView(rotationButton.currentView)
-            rotationButton.currentView.layoutParams = getParamsToCenterView()
-        }
         endContextualContainer.addView(space, MATCH_PARENT, MATCH_PARENT)
     }
 
-    override fun repositionContextualContainer(contextualContainer: ViewGroup, buttonSize: Int,
-                                               barAxisMarginTop: Int, barAxisMarginBottom: Int,
-                                               gravity: Int) {
+    override fun repositionContextualContainer(
+        contextualContainer: ViewGroup,
+        buttonSize: Int,
+        barAxisMarginTop: Int,
+        barAxisMarginBottom: Int,
+        gravity: Int
+    ) {
         val contextualContainerParams = FrameLayout.LayoutParams(MATCH_PARENT, buttonSize)
         contextualContainerParams.apply {
             marginStart = 0
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index 0672270..5b24ebf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -26,34 +26,32 @@
 import android.widget.Space
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.systemui.shared.rotation.RotationButton
 
 class PhonePortraitNavLayoutter(
-        resources: Resources,
-        navBarContainer: LinearLayout,
-        endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup,
-        imeSwitcher: ImageView?,
-        rotationButton: RotationButton?,
-        a11yButton: ImageView?,
-        space: Space?
+    resources: Resources,
+    navBarContainer: LinearLayout,
+    endContextualContainer: ViewGroup,
+    startContextualContainer: ViewGroup,
+    imeSwitcher: ImageView?,
+    a11yButton: ImageView?,
+    space: Space?
 ) :
     AbstractNavButtonLayoutter(
-            resources,
-            navBarContainer,
-            endContextualContainer,
-            startContextualContainer,
-            imeSwitcher,
-            rotationButton,
-            a11yButton,
-            space
+        resources,
+        navBarContainer,
+        endContextualContainer,
+        startContextualContainer,
+        imeSwitcher,
+        a11yButton,
+        space
     ) {
 
     override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         val totalWidth = context.deviceProfile.widthPx
-        val homeButtonWidth = resources.getDimensionPixelSize(R.dimen.taskbar_phone_home_button_size)
-        val roundedCornerContentMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val homeButtonWidth =
+            resources.getDimensionPixelSize(R.dimen.taskbar_phone_home_button_size)
+        val roundedCornerContentMargin =
+            resources.getDimensionPixelSize(R.dimen.taskbar_phone_rounded_corner_content_margin)
         val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
         val contentWidth = totalWidth - roundedCornerContentMargin * 2 - contentPadding * 2
 
@@ -62,15 +60,18 @@
         val sideButtonWidth = contextualButtonWidth * 2
         val navButtonContainerWidth = contentWidth - contextualButtonWidth * 2
 
-        val navContainerParams = FrameLayout.LayoutParams(navButtonContainerWidth.toInt(),
-                ViewGroup.LayoutParams.MATCH_PARENT)
+        val navContainerParams =
+            FrameLayout.LayoutParams(
+                navButtonContainerWidth.toInt(),
+                ViewGroup.LayoutParams.MATCH_PARENT
+            )
         navContainerParams.apply {
             topMargin = 0
             bottomMargin = 0
             marginEnd =
-                    (contextualButtonWidth + contentPadding + roundedCornerContentMargin).toInt()
+                (contextualButtonWidth + contentPadding + roundedCornerContentMargin).toInt()
             marginStart =
-                    (contextualButtonWidth + contentPadding + roundedCornerContentMargin).toInt()
+                (contextualButtonWidth + contentPadding + roundedCornerContentMargin).toInt()
         }
 
         // Ensure order of buttons is correct
@@ -85,8 +86,8 @@
         navButtonContainer.gravity = Gravity.CENTER
 
         // Add the spaces in between the nav buttons
-        val spaceInBetween = (navButtonContainerWidth - homeButtonWidth -
-                sideButtonWidth * 2) / 2.0f
+        val spaceInBetween =
+            (navButtonContainerWidth - homeButtonWidth - sideButtonWidth * 2) / 2.0f
         for (i in 0 until navButtonContainer.childCount) {
             val navButton = navButtonContainer.getChildAt(i)
             val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams
@@ -114,10 +115,20 @@
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        repositionContextualContainer(startContextualContainer, contextualButtonWidth.toInt(),
-                roundedCornerContentMargin + contentPadding, 0, Gravity.START)
-        repositionContextualContainer(endContextualContainer, contextualButtonWidth.toInt(), 0,
-                roundedCornerContentMargin + contentPadding, Gravity.END)
+        repositionContextualContainer(
+            startContextualContainer,
+            contextualButtonWidth.toInt(),
+            roundedCornerContentMargin + contentPadding,
+            0,
+            Gravity.START
+        )
+        repositionContextualContainer(
+            endContextualContainer,
+            contextualButtonWidth.toInt(),
+            0,
+            roundedCornerContentMargin + contentPadding,
+            Gravity.END
+        )
 
         startContextualContainer.addView(space, MATCH_PARENT, MATCH_PARENT)
         if (imeSwitcher != null) {
@@ -128,9 +139,5 @@
             endContextualContainer.addView(a11yButton)
             a11yButton.layoutParams = getParamsToCenterView()
         }
-        if (rotationButton != null) {
-            endContextualContainer.addView(rotationButton.currentView)
-            rotationButton.currentView.layoutParams = getParamsToCenterView()
-        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
index 869cc43..f0b47f4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
@@ -1,18 +1,18 @@
 /*
-* Copyright (C) 2023 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-*      http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License
-*/
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
 
 package com.android.launcher3.taskbar.navbutton
 
@@ -24,28 +24,25 @@
 import android.widget.LinearLayout
 import android.widget.Space
 import com.android.launcher3.R
-import com.android.systemui.shared.rotation.RotationButton
 
 class PhoneSeascapeNavLayoutter(
-        resources: Resources,
-        navBarContainer: LinearLayout,
-        endContextualContainer: ViewGroup,
-        startContextualContainer: ViewGroup,
-        imeSwitcher: ImageView?,
-        rotationButton: RotationButton?,
-        a11yButton: ImageView?,
-        space: Space?
+    resources: Resources,
+    navBarContainer: LinearLayout,
+    endContextualContainer: ViewGroup,
+    startContextualContainer: ViewGroup,
+    imeSwitcher: ImageView?,
+    a11yButton: ImageView?,
+    space: Space?
 ) :
-        PhoneLandscapeNavLayoutter(
-                resources,
-                navBarContainer,
-                endContextualContainer,
-                startContextualContainer,
-                imeSwitcher,
-                rotationButton,
-                a11yButton,
-                space
-        ) {
+    PhoneLandscapeNavLayoutter(
+        resources,
+        navBarContainer,
+        endContextualContainer,
+        startContextualContainer,
+        imeSwitcher,
+        a11yButton,
+        space
+    ) {
 
     override fun addThreeButtons() {
         // Flip ordering of back and recents buttons
@@ -58,13 +55,23 @@
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        val roundedCornerContentMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val roundedCornerContentMargin =
+            resources.getDimensionPixelSize(R.dimen.taskbar_phone_rounded_corner_content_margin)
         val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
-        repositionContextualContainer(startContextualContainer, buttonSize,
-                roundedCornerContentMargin + contentPadding, 0, Gravity.TOP)
-        repositionContextualContainer(endContextualContainer, buttonSize, 0,
-                roundedCornerContentMargin + contentPadding,  Gravity.BOTTOM)
+        repositionContextualContainer(
+            startContextualContainer,
+            buttonSize,
+            roundedCornerContentMargin + contentPadding,
+            0,
+            Gravity.TOP
+        )
+        repositionContextualContainer(
+            endContextualContainer,
+            buttonSize,
+            0,
+            roundedCornerContentMargin + contentPadding,
+            Gravity.BOTTOM
+        )
 
         startContextualContainer.addView(space, MATCH_PARENT, MATCH_PARENT)
         if (imeSwitcher != null) {
@@ -75,9 +82,5 @@
             endContextualContainer.addView(a11yButton)
             a11yButton.layoutParams = getParamsToCenterView()
         }
-        if (rotationButton != null) {
-            endContextualContainer.addView(rotationButton.currentView)
-            rotationButton.currentView.layoutParams = getParamsToCenterView()
-        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
index 8eff95c..91042c3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
@@ -27,7 +27,6 @@
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.systemui.shared.rotation.RotationButton
 
 const val SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95
 const val SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05
@@ -39,7 +38,6 @@
     endContextualContainer: ViewGroup,
     startContextualContainer: ViewGroup,
     imeSwitcher: ImageView?,
-    rotationButton: RotationButton?,
     a11yButton: ImageView?,
     space: Space?
 ) :
@@ -49,7 +47,6 @@
         endContextualContainer,
         startContextualContainer,
         imeSwitcher,
-        rotationButton,
         a11yButton,
         space
     ) {
@@ -116,9 +113,5 @@
             endContextualContainer.addView(a11yButton)
             a11yButton.layoutParams = getParamsToCenterView()
         }
-        if (rotationButton != null) {
-            endContextualContainer.addView(rotationButton.currentView)
-            rotationButton.currentView.layoutParams = getParamsToCenterView()
-        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
index 34d3fad..a59e8a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
@@ -26,7 +26,6 @@
 import android.widget.Space
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.systemui.shared.rotation.RotationButton
 
 /** Layoutter for rendering task bar in large screen, both in 3-button and gesture nav mode. */
 class TaskbarNavLayoutter(
@@ -35,7 +34,6 @@
     endContextualContainer: ViewGroup,
     startContextualContainer: ViewGroup,
     imeSwitcher: ImageView?,
-    rotationButton: RotationButton?,
     a11yButton: ImageView?,
     space: Space?
 ) :
@@ -45,7 +43,6 @@
         endContextualContainer,
         startContextualContainer,
         imeSwitcher,
-        rotationButton,
         a11yButton,
         space
     ) {
@@ -137,10 +134,6 @@
                 endContextualContainer.addView(a11yButton)
                 a11yButton.layoutParams = getParamsToCenterView()
             }
-            if (rotationButton != null) {
-                endContextualContainer.addView(rotationButton.currentView)
-                rotationButton.currentView.layoutParams = getParamsToCenterView()
-            }
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4acddee..4184ab2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -166,6 +166,8 @@
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
@@ -264,6 +266,10 @@
                         getDepthController(), getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         () -> onStateBack());
+        RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(asContext());
+        // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
+        OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
+                asContext(), deviceState);
         if (enableDesktopWindowingMode()) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
                     getStateManager(), systemUiProxy, getIApplicationThread(),
@@ -272,7 +278,7 @@
         overviewPanel.init(mActionsView, mSplitSelectStateController,
                 mDesktopRecentsTransitionController);
         mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
-                mSplitSelectStateController);
+                mSplitSelectStateController, overviewComponentObserver, deviceState);
         mSplitToWorkspaceController = new SplitToWorkspaceController(this,
                 mSplitSelectStateController);
         mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
@@ -287,7 +293,8 @@
         if (enableDesktopWindowingMode()) {
             mDesktopVisibilityController = new DesktopVisibilityController(this);
             mDesktopVisibilityController.registerSystemUiListener();
-            mSplitSelectStateController.initSplitFromDesktopController(this);
+            mSplitSelectStateController.initSplitFromDesktopController(this,
+                    overviewComponentObserver);
         }
         mHotseatPredictionController = new HotseatPredictionController(this);
 
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index c8a91df..81c9d4a 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -186,6 +186,7 @@
     private final long mSwipeUpStartTimeMs = SystemClock.uptimeMillis();
 
     private boolean mHandlingAtomicEvent;
+    private boolean mIsInExtendedSlopRegion;
 
     public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
         mHomeIntent = componentObserver.getHomeIntent();
@@ -494,6 +495,25 @@
     }
 
     /**
+     * Set whether it's in long press nav handle (LPNH)'s extended touch slop region, e.g., second
+     * stage region in order to continue respect LPNH and ignore other touch slop logic.
+     * This will only be set to true when flag ENABLE_LPNH_TWO_STAGES is turned on.
+     */
+    public void setIsInExtendedSlopRegion(boolean isInExtendedSlopRegion) {
+        if (DeviceConfigWrapper.get().getEnableLpnhTwoStages()) {
+            mIsInExtendedSlopRegion = isInExtendedSlopRegion;
+        }
+    }
+
+    /**
+     * Returns whether it's in LPNH's extended touch slop region. This is only valid when flag
+     * ENABLE_LPNH_TWO_STAGES is turned on.
+     */
+    public boolean isInExtendedSlopRegion() {
+        return mIsInExtendedSlopRegion;
+    }
+
+    /**
      * Returns and clears the canceled animation thumbnail data. This call only returns a value
      * while STATE_RECENTS_ANIMATION_CANCELED state is being set, and the caller is responsible for
      * calling {@link RecentsAnimationController#cleanupScreenshot()}.
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 0a02e99..a71e314 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -109,6 +109,8 @@
      * Sets a listener for changes in {@link #isHomeAndOverviewSame()}
      */
     public void setOverviewChangeListener(Consumer<Boolean> overviewChangeListener) {
+        // TODO(b/337861962): This method should be able to support multiple listeners instead of
+        // one so that we can reuse the same instance of this class across multiple places
         mOverviewChangeListener = overviewChangeListener;
     }
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index b0b2589..fcf5ffc 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -16,7 +16,6 @@
 package com.android.quickstep;
 
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
-import static android.content.pm.PackageManager.FEATURE_PC;
 
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -1385,8 +1384,7 @@
 
     private boolean shouldEnableRunningTasksForDesktopMode() {
         // TODO(b/335401172): unify DesktopMode checks in Launcher
-        return (enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps())
-                || mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
+        return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
     }
 
     private boolean handleMessageAsync(Message msg) {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 832f4e1..f94a29c 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -1018,7 +1018,7 @@
                             .append("TaskbarActivityContext != null, ")
                             .append("using TaskbarUnstashInputConsumer");
                     base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
-                            mOverviewCommandHelper);
+                            mOverviewCommandHelper, mGestureState);
                 }
             }
             if (enableBubblesLongPressNavHandle()) {
@@ -1046,7 +1046,7 @@
                 }
                 reasonString.append("using NavHandleLongPressInputConsumer");
                 base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat,
-                        mDeviceState, navHandle);
+                        mDeviceState, navHandle, mGestureState);
             }
 
             if (!enableBubblesLongPressNavHandle()) {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d881a1f..b79586b 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -53,6 +53,7 @@
 import com.android.systemui.shared.recents.model.Task;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 public class FallbackRecentsView extends RecentsView<RecentsActivity, RecentsState>
         implements StateListener<RecentsState> {
@@ -144,8 +145,9 @@
     @Override
     public void setCurrentTask(int runningTaskViewId) {
         super.setCurrentTask(runningTaskViewId);
-        int runningTaskId = getTaskIdsForRunningTaskView()[0];
-        if (mHomeTask != null && mHomeTask.key.id != runningTaskId) {
+        int[] runningTaskIds = getTaskIdsForRunningTaskView();
+        if (mHomeTask != null
+                && Arrays.stream(runningTaskIds).noneMatch(taskId -> taskId == mHomeTask.key.id)) {
             mHomeTask = null;
             setRunningTaskHidden(false);
         }
@@ -182,13 +184,14 @@
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
         // track the index of the next task appropriately, as if we are switching on any other app.
         // TODO(b/195607777) Confirm home task info is front-most task and not mixed in with others
-        int runningTaskId = getTaskIdsForRunningTaskView()[0];
-        if (mHomeTask != null && mHomeTask.key.id == runningTaskId
+        int[] runningTaskIds = getTaskIdsForRunningTaskView();
+        if (mHomeTask != null
+                && Arrays.stream(runningTaskIds).allMatch(taskId -> taskId == mHomeTask.key.id)
                 && !taskGroups.isEmpty()) {
             // Check if the task list has running task
             boolean found = false;
             for (GroupTask group : taskGroups) {
-                if (group.containsTask(runningTaskId)) {
+                if (Arrays.stream(runningTaskIds).allMatch(group::containsTask)) {
                     found = true;
                     break;
                 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index 075e539..848a43a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.NavHandle;
 import com.android.quickstep.RecentsAnimationDeviceState;
@@ -61,13 +62,14 @@
     private final NavHandle mNavHandle;
     private final StatsLogManager mStatsLogManager;
     private final TopTaskTracker mTopTaskTracker;
+    private final GestureState mGestureState;
 
     private MotionEvent mCurrentDownEvent;
     private boolean mDeepPressLogged;  // Whether deep press has been logged for the current touch.
 
     public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState,
-            NavHandle navHandle) {
+            NavHandle navHandle, GestureState gestureState) {
         super(delegate, inputMonitor);
         mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
         mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress();
@@ -82,6 +84,8 @@
         mTouchSlopSquaredOriginal = deviceState.getSquaredTouchSlop();
         mTouchSlopSquared = mTouchSlopSquaredOriginal;
         mOuterTouchSlopSquared = mTouchSlopSquared * (twoStageMultiplier * twoStageMultiplier);
+        mGestureState = gestureState;
+        mGestureState.setIsInExtendedSlopRegion(false);
         if (DEBUG_NAV_HANDLE) {
             Log.d(TAG, "mLongPressTimeout=" + mLongPressTimeout);
             Log.d(TAG, "mOuterLongPressTimeout=" + mOuterLongPressTimeout);
@@ -126,6 +130,7 @@
                 }
                 mCurrentDownEvent = MotionEvent.obtain(ev);
                 mTouchSlopSquared = mTouchSlopSquaredOriginal;
+                mGestureState.setIsInExtendedSlopRegion(false);
                 mDeepPressLogged = false;
                 if (isInNavBarHorizontalArea(ev.getRawX())) {
                     mNavHandleLongPressHandler.onTouchStarted(mNavHandle);
@@ -154,6 +159,7 @@
                                 - (int) (ev.getEventTime() - ev.getDownTime());
                         MAIN_EXECUTOR.getHandler().postDelayed(mTriggerLongPress, delay);
                         mTouchSlopSquared = mOuterTouchSlopSquared;
+                        mGestureState.setIsInExtendedSlopRegion(true);
                         if (DEBUG_NAV_HANDLE) {
                             Log.d(TAG, "Touch in middle region!");
                         }
@@ -219,6 +225,7 @@
         if (DEBUG_NAV_HANDLE) {
             Log.d(TAG, "cancelLongPress");
         }
+        mGestureState.setIsInExtendedSlopRegion(false);
         MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
         mNavHandleLongPressHandler.onTouchFinished(mNavHandle, reason);
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 9f39476..0d450c6 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -284,8 +284,9 @@
 
                 float horizontalDist = Math.abs(displacementX);
                 float upDist = -displacement;
-                boolean passedSlop = mGestureState.isTrackpadGesture() || squaredHypot(
-                        displacementX, displacementY) >= mSquaredTouchSlop;
+                boolean passedSlop = mGestureState.isTrackpadGesture()
+                        || (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop
+                            && !mGestureState.isInExtendedSlopRegion());
 
                 if (!mPassedSlopOnThisGesture && passedSlop) {
                     mPassedSlopOnThisGesture = true;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index bb8d1d7..c61f71d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -48,6 +48,7 @@
     private final BaseContainerInterface<?, T> mContainerInterface;
     private final BaseDragLayer mTarget;
     private final InputMonitorCompat mInputMonitor;
+    private final GestureState mGestureState;
 
     private final int[] mLocationOnScreen = new int[2];
 
@@ -62,6 +63,7 @@
         mInputMonitor = inputMonitor;
         mStartingInActivityBounds = startingInActivityBounds;
         mContainerInterface = gestureState.getContainerInterface();
+        mGestureState = gestureState;
 
         mTarget = container.getDragLayer();
         mTarget.getLocationOnScreen(mLocationOnScreen);
@@ -84,7 +86,10 @@
             ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
         }
         ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
-        boolean handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
+        boolean handled = false;
+        if (mGestureState == null || !mGestureState.isInExtendedSlopRegion()) {
+            handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
+        }
         ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
         ev.setEdgeFlags(flags);
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index cd180ba..6b3e6e9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -76,10 +77,11 @@
     private final int mStashedTaskbarBottomEdge;
 
     private final @Nullable TransitionCallback mTransitionCallback;
+    private final GestureState mGestureState;
 
     public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext,
-            OverviewCommandHelper overviewCommandHelper) {
+            OverviewCommandHelper overviewCommandHelper, GestureState gestureState) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
         mOverviewCommandHelper = overviewCommandHelper;
@@ -103,6 +105,7 @@
         mTransitionCallback = mIsTransientTaskbar
                 ? taskbarActivityContext.getTranslationCallbacks()
                 : null;
+        mGestureState = gestureState;
     }
 
     @Override
@@ -111,6 +114,11 @@
     }
 
     @Override
+    public boolean allowInterceptByParent() {
+        return super.allowInterceptByParent() && !mHasPassedTaskbarNavThreshold;
+    }
+
+    @Override
     public void onMotionEvent(MotionEvent ev) {
         if (mState != STATE_ACTIVE) {
             boolean isStashedTaskbarHovered = isMouseEvent(ev)
@@ -173,7 +181,8 @@
                             boolean passedTaskbarNavThreshold = dY < 0
                                     && Math.abs(dY) >= mTaskbarNavThreshold;
 
-                            if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
+                            if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold
+                                    && !mGestureState.isInExtendedSlopRegion()) {
                                 mHasPassedTaskbarNavThreshold = true;
                                 if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) {
                                     mTaskbarActivityContext.onSwipeToOpenBubblebar();
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index b3f5d82..07f2d68 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -56,4 +56,10 @@
     public DesktopTask copy() {
         return new DesktopTask(tasks);
     }
+
+    @Override
+    public String toString() {
+        return "type=" + taskViewType + " tasks=" + tasks;
+    }
+
 }
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 9c49647..7dd6afc 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -70,4 +70,10 @@
                 task2 != null ? new Task(task2) : null,
                 mSplitBounds);
     }
+
+    @Override
+    public String toString() {
+        return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
+    }
+
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index f430d79..ee2c2e1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -23,6 +23,7 @@
 import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.content.Context
 import android.graphics.Bitmap
@@ -50,6 +51,7 @@
 import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.logging.StatsLogManager.EventEnum
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -69,6 +71,7 @@
 import com.android.quickstep.views.TaskView
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
 import com.android.quickstep.views.TaskViewIcon
+import com.android.wm.shell.shared.TransitionUtil
 import java.util.Optional
 import java.util.function.Supplier
 
@@ -553,8 +556,14 @@
             check(info != null && t != null) {
                 "trying to launch an app pair icon, but encountered an unexpected null"
             }
-
-            composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+            val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
+            if (appPairLaunchingAppIndex == -1) {
+                // Launch split app pair animation
+                composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+            } else {
+                composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t,
+                        finishCallback, appPairLaunchingAppIndex)
+            }
         } else {
             // Fallback case: simple fade-in animation
             check(info != null && t != null) {
@@ -619,6 +628,39 @@
     }
 
     /**
+     * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise
+     *         the integer index corresponding to [launchingIconView]'s contents for the single app
+     *         to be animated
+     */
+    fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon,
+                                          transitionInfo: TransitionInfo) : Int {
+        val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
+        val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
+        var launchFullscreenAppIndex = -1
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+            if (TransitionUtil.isOpeningType(change.mode) &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+                val baseIntent = taskInfo.baseIntent.component?.packageName
+                if (baseIntent == intent1) {
+                    if (launchFullscreenAppIndex > -1) {
+                        launchFullscreenAppIndex = -1
+                        break
+                    }
+                    launchFullscreenAppIndex = 0
+                } else if (baseIntent == intent2) {
+                    if (launchFullscreenAppIndex > -1) {
+                        launchFullscreenAppIndex = -1
+                        break
+                    }
+                    launchFullscreenAppIndex = 1
+                }
+            }
+        }
+        return launchFullscreenAppIndex
+    }
+
+    /**
      * When the user taps an app pair icon to launch split, this will play the tasks' launch
      * animation from the position of the icon.
      *
@@ -653,7 +695,8 @@
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
         if (launchingIconView.context is TaskbarActivityContext) {
-            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback)
+            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
+                    WINDOWING_MODE_MULTI_WINDOW)
             return
         }
 
@@ -663,11 +706,6 @@
 
         // Create an AnimatorSet that will run both shell and launcher transitions together
         val launchAnimation = AnimatorSet()
-        val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
-        val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
-        progressUpdater.setDuration(timings.getDuration().toLong())
-        progressUpdater.interpolator = Interpolators.LINEAR
-
         var rootCandidate: Change? = null
 
         for (change in transitionInfo.changes) {
@@ -711,27 +749,13 @@
         // Make sure nothing weird happened, like getChange() returning null.
         check(rootCandidate != null) { "Failed to find a root leash" }
 
-        // Shell animation: the apps are revealed toward end of the launch animation
-        progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
-            val progress =
-                Interpolators.clampToProgress(
-                    Interpolators.LINEAR,
-                    valueAnimator.animatedFraction,
-                    timings.appRevealStartOffset,
-                    timings.appRevealEndOffset
-                )
-
-            // Set the alpha of the shell layer (2 apps + divider)
-            t.setAlpha(rootCandidate.leash, progress)
-            t.apply()
-        }
-
         // Create a new floating view in Launcher, positioned above the launching icon
         val drawableArea = launchingIconView.iconDrawableArea
         val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context)
         val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context)
         appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
         appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+
         val floatingView =
             FloatingAppPairView.getFloatingAppPairView(
                 launcher,
@@ -742,84 +766,189 @@
             )
         floatingView.bringToFront()
 
-        // Launcher animation: animate the floating view, expanding to fill the display surface
-        progressUpdater.addUpdateListener(
-            object : MultiValueUpdateListener() {
-                var mDx =
-                    FloatProp(
-                        floatingView.startingPosition.left,
-                        dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
-                        Interpolators.clampToProgress(
-                            timings.getStagedRectXInterpolator(),
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-                var mDy =
-                    FloatProp(
-                        floatingView.startingPosition.top,
-                        dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
-                        Interpolators.clampToProgress(
-                            Interpolators.EMPHASIZED,
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-                var mScaleX =
-                    FloatProp(
-                        1f /* start */,
-                        dp.widthPx / floatingView.startingPosition.width(),
-                        Interpolators.clampToProgress(
-                            Interpolators.EMPHASIZED,
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-                var mScaleY =
-                    FloatProp(
-                        1f /* start */,
-                        dp.heightPx / floatingView.startingPosition.height(),
-                        Interpolators.clampToProgress(
-                            Interpolators.EMPHASIZED,
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-
-                override fun onUpdate(percent: Float, initOnly: Boolean) {
-                    floatingView.progress = percent
-                    floatingView.x = mDx.value
-                    floatingView.y = mDy.value
-                    floatingView.scaleX = mScaleX.value
-                    floatingView.scaleY = mScaleY.value
-                    floatingView.invalidate()
-                }
-            }
-        )
-
-        // When animation ends, remove the floating view and run finishCallback
-        progressUpdater.addListener(
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    safeRemoveViewFromDragLayer(launcher, floatingView)
-                    finishCallback.run()
-                }
-            }
-        )
-
-        launchAnimation.play(progressUpdater)
+        launchAnimation.play(
+                getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
+                        rootCandidate))
         launchAnimation.start()
     }
 
     /**
+     * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate
+     * a single fullscreen icon + background instead of for a pair
+     */
+    @VisibleForTesting
+    fun composeFullscreenIconSplitLaunchAnimator(
+            launchingIconView: AppPairIcon,
+            transitionInfo: TransitionInfo,
+            t: Transaction,
+            finishCallback: Runnable,
+            launchFullscreenIndex: Int
+    ) {
+        // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
+        // use the scale-up animation
+        if (launchingIconView.context is TaskbarActivityContext) {
+            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
+                    WINDOWING_MODE_FULLSCREEN)
+            return
+        }
+
+        // Else we are in Launcher and can launch with the full icon stretch-and-split animation.
+        val launcher = QuickstepLauncher.getLauncher(launchingIconView.context)
+        val dp = launcher.deviceProfile
+
+        // Create an AnimatorSet that will run both shell and launcher transitions together
+        val launchAnimation = AnimatorSet()
+
+        val appInfo = launchingIconView.info
+                .getContents()[launchFullscreenIndex] as WorkspaceItemInfo
+        val intentToLaunch = appInfo.intent.component?.packageName
+        var rootCandidate: Change? = null
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+            val baseIntent = taskInfo.baseIntent.component?.packageName
+            if (TransitionUtil.isOpeningType(change.mode) &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN &&
+                    baseIntent == intentToLaunch) {
+                rootCandidate = change
+            }
+        }
+
+        // If we could not find a proper root candidate, something went wrong.
+        check(rootCandidate != null) { "Could not find a split root candidate" }
+
+        // Recurse up the tree until parent is null, then we've found our root.
+        var parentToken: WindowContainerToken? = rootCandidate.parent
+        while (parentToken != null) {
+            rootCandidate = transitionInfo.getChange(parentToken) ?: break
+            parentToken = rootCandidate.parent
+        }
+
+        // Make sure nothing weird happened, like getChange() returning null.
+        check(rootCandidate != null) { "Failed to find a root leash" }
+
+        // Create a new floating view in Launcher, positioned above the launching icon
+        val drawableArea = launchingIconView.iconDrawableArea
+        val appIcon = appInfo.newIcon(launchingIconView.context)
+        appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+
+        val floatingView =
+                FloatingAppPairView.getFloatingAppPairView(
+                        launcher,
+                        drawableArea,
+                        appIcon,
+                        null /*appIcon2*/,
+                        0 /*dividerPos*/
+                )
+        floatingView.bringToFront()
+        launchAnimation.play(
+                getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
+                        rootCandidate))
+        launchAnimation.start()
+    }
+
+    private fun getIconLaunchValueAnimator(t: Transaction,
+                                           dp: com.android.launcher3.DeviceProfile,
+                                           finishCallback: Runnable,
+                                           launcher: QuickstepLauncher,
+                                           floatingView: FloatingAppPairView,
+                                           rootCandidate: Change) : ValueAnimator {
+        val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+        val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
+        progressUpdater.setDuration(timings.getDuration().toLong())
+        progressUpdater.interpolator = Interpolators.LINEAR
+
+        // Shell animation: the apps are revealed toward end of the launch animation
+        progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+            val progress =
+                    Interpolators.clampToProgress(
+                            Interpolators.LINEAR,
+                            valueAnimator.animatedFraction,
+                            timings.appRevealStartOffset,
+                            timings.appRevealEndOffset
+                    )
+
+            // Set the alpha of the shell layer (2 apps + divider)
+            t.setAlpha(rootCandidate.leash, progress)
+            t.apply()
+        }
+
+        progressUpdater.addUpdateListener(
+                object : MultiValueUpdateListener() {
+                    var mDx =
+                            FloatProp(
+                                    floatingView.startingPosition.left,
+                                    dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
+                                    Interpolators.clampToProgress(
+                                            timings.getStagedRectXInterpolator(),
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+                    var mDy =
+                            FloatProp(
+                                    floatingView.startingPosition.top,
+                                    dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
+                                    Interpolators.clampToProgress(
+                                            Interpolators.EMPHASIZED,
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+                    var mScaleX =
+                            FloatProp(
+                                    1f /* start */,
+                                    dp.widthPx / floatingView.startingPosition.width(),
+                                    Interpolators.clampToProgress(
+                                            Interpolators.EMPHASIZED,
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+                    var mScaleY =
+                            FloatProp(
+                                    1f /* start */,
+                                    dp.heightPx / floatingView.startingPosition.height(),
+                                    Interpolators.clampToProgress(
+                                            Interpolators.EMPHASIZED,
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+
+                    override fun onUpdate(percent: Float, initOnly: Boolean) {
+                        floatingView.progress = percent
+                        floatingView.x = mDx.value
+                        floatingView.y = mDy.value
+                        floatingView.scaleX = mScaleX.value
+                        floatingView.scaleY = mScaleY.value
+                        floatingView.invalidate()
+                    }
+                }
+        )
+        progressUpdater.addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        safeRemoveViewFromDragLayer(launcher, floatingView)
+                        finishCallback.run()
+                    }
+                }
+        )
+
+        return progressUpdater
+    }
+
+    /**
      * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
      * there is no visible associated tile to expand from.
+     * [windowingMode] helps determine whether we are looking for a split or a single fullscreen
+     * [Change]
      */
     @VisibleForTesting
     fun composeScaleUpLaunchAnimation(
         transitionInfo: TransitionInfo,
         t: Transaction,
-        finishCallback: Runnable
+        finishCallback: Runnable,
+        windowingMode: Int
     ) {
         val launchAnimation = AnimatorSet()
         val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
@@ -833,9 +962,8 @@
 
             // TODO (b/316490565): Replace this logic when SplitBounds is available to
             //  startAnimation() and we can know the precise taskIds of launching tasks.
-            // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
             if (
-                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+                taskInfo.windowingMode == windowingMode &&
                     (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
             ) {
                 // Found one!
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index c257be6..df1879e 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -90,7 +90,6 @@
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SplitSelectionListener;
@@ -646,8 +645,13 @@
         }
     }
 
-    public void initSplitFromDesktopController(QuickstepLauncher launcher) {
-        initSplitFromDesktopController(new SplitFromDesktopController(launcher));
+    /**
+     * Init {@code SplitFromDesktopController}
+     */
+    public void initSplitFromDesktopController(QuickstepLauncher launcher,
+            OverviewComponentObserver overviewComponentObserver) {
+        initSplitFromDesktopController(
+                new SplitFromDesktopController(launcher, overviewComponentObserver));
     }
 
     @VisibleForTesting
@@ -956,12 +960,10 @@
         private ISplitSelectListener mSplitSelectListener;
         private Drawable mAppIcon;
 
-        public SplitFromDesktopController(QuickstepLauncher launcher) {
+        public SplitFromDesktopController(QuickstepLauncher launcher,
+                OverviewComponentObserver overviewComponentObserver) {
             mLauncher = launcher;
-            RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
-                    launcher.getApplicationContext());
-            mOverviewComponentObserver =
-                    new OverviewComponentObserver(launcher.getApplicationContext(), deviceState);
+            mOverviewComponentObserver = overviewComponentObserver;
             mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
                     R.dimen.split_placeholder_size);
             mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 555bf21..85d4f4b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -62,12 +62,13 @@
     private final int mSplitPlaceholderInset;
 
     public SplitWithKeyboardShortcutController(QuickstepLauncher launcher,
-            SplitSelectStateController controller) {
+            SplitSelectStateController controller,
+            OverviewComponentObserver overviewComponentObserver,
+            RecentsAnimationDeviceState deviceState) {
         mLauncher = launcher;
         mController = controller;
-        mDeviceState = new RecentsAnimationDeviceState(launcher.getApplicationContext());
-        mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(),
-                mDeviceState);
+        mDeviceState = deviceState;
+        mOverviewComponentObserver = overviewComponentObserver;
 
         mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
                 R.dimen.split_placeholder_size);
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 9268511..304b8f4 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -26,9 +26,11 @@
 import android.view.WindowMetrics;
 
 import com.android.internal.policy.SystemBarUtils;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
+import com.android.quickstep.LauncherActivityInterface;
 
 import java.util.List;
 import java.util.Set;
@@ -49,6 +51,13 @@
     }
 
     @Override
+    public boolean isInDesktopMode() {
+        DesktopVisibilityController desktopController =
+                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+        return desktopController != null && desktopController.areDesktopTasksVisible();
+    }
+
+    @Override
     public int getRotation(Context displayInfoContext) {
         return displayInfoContext.getResources().getConfiguration().windowConfiguration
                 .getRotation();
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index b8afd9d..c3efc3c 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -34,7 +34,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
-import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.BorderAnimator;
 
@@ -136,6 +135,10 @@
      * Enable or disable showing border on focus change
      */
     public void setBorderEnabled(boolean enabled) {
+        if (mBorderEnabled == enabled) {
+            return;
+        }
+
         mBorderEnabled = enabled;
         if (mFocusBorderAnimator != null) {
             mFocusBorderAnimator.setBorderVisibility(/* visible= */
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index a0ec525..e797819 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -204,18 +204,14 @@
     }
 
     private void updateTaskIdContainer() {
-        // TODO(b/249371338): TaskView expects the array to have at least 2 elements.
-        // At least 2 elements in the array
-        mTaskIdContainer = new int[Math.max(mTasks.size(), 2)];
+        mTaskIdContainer = new int[mTasks.size()];
         for (int i = 0; i < mTasks.size(); i++) {
             mTaskIdContainer[i] = mTasks.get(i).key.id;
         }
     }
 
     private void updateTaskIdAttributeContainer() {
-        // TODO(b/249371338): TaskView expects the array to have at least 2 elements.
-        // At least 2 elements in the array
-        mTaskIdAttributeContainer = new TaskIdAttributeContainer[Math.max(mTasks.size(), 2)];
+        mTaskIdAttributeContainer = new TaskIdAttributeContainer[mTasks.size()];
         for (int i = 0; i < mTasks.size(); i++) {
             Task task = mTasks.get(i);
             TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.get(task.key.id);
@@ -248,12 +244,6 @@
     }
 
     @Override
-    public boolean containsTaskId(int taskId) {
-        // Thumbnail map contains taskId -> thumbnail map. Use the keys for contains
-        return mSnapshotViewMap.contains(taskId);
-    }
-
-    @Override
     public void onTaskListVisibilityChanged(boolean visible, int changes) {
         cancelPendingLoadTasks();
         if (visible) {
@@ -311,9 +301,13 @@
         DesktopRecentsTransitionController recentsController =
                 recentsView.getDesktopRecentsController();
         if (recentsController != null) {
-            recentsController.launchDesktopFromRecents(this, success -> {
-                endCallback.executeAllAndDestroy();
-            });
+            recentsController.launchDesktopFromRecents(this,
+                    success -> endCallback.executeAllAndDestroy());
+            Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: " + Arrays.toString(
+                    getTaskIds()));
+        } else {
+            Log.d(TAG, "launchTaskAnimated - recentsController is null: " + Arrays.toString(
+                    getTaskIds()));
         }
 
         // Callbacks get run from recentsView for case when recents animation already running
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index 0d49309..e024995 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -36,17 +36,18 @@
  * animation. Consists of a rectangular background that splits into two, and two app icons that
  * increase in size during the animation.
  */
-class FloatingAppPairBackground(
-    context: Context,
-    private val floatingView: FloatingAppPairView, // the view that we will draw this background on
-    private val appIcon1: Drawable,
-    private val appIcon2: Drawable,
-    dividerPos: Int
+open class FloatingAppPairBackground(
+        context: Context,
+        // the view that we will draw this background on
+        protected val floatingView: FloatingAppPairView,
+        private val appIcon1: Drawable,
+        private val appIcon2: Drawable?,
+        dividerPos: Int
 ) : Drawable() {
     companion object {
         // Design specs -- app icons start small and expand during the animation
-        private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
-        private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
+        internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
+        internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
 
         // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other
         // API for drawing rectangles with 4 different corner radii.
@@ -58,13 +59,13 @@
     private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     // Animation interpolators
-    private val expandXInterpolator: Interpolator
-    private val expandYInterpolator: Interpolator
+    protected val expandXInterpolator: Interpolator
+    protected val expandYInterpolator: Interpolator
     private val cellSplitInterpolator: Interpolator
-    private val iconFadeInterpolator: Interpolator
+    protected val iconFadeInterpolator: Interpolator
 
     // Device-specific measurements
-    private val deviceCornerRadius: Float
+    protected val deviceCornerRadius: Float
     private val deviceHalfDividerSize: Float
     private val desiredSplitRatio: Float
 
@@ -214,7 +215,7 @@
         canvas.save()
         canvas.translate(changingIcon2Left, changingIconTop)
         canvas.scale(changingIconScaleX, changingIconScaleY)
-        appIcon2.alpha = changingIconAlpha
+        appIcon2!!.alpha = changingIconAlpha
         appIcon2.draw(canvas)
         canvas.restore()
     }
@@ -312,7 +313,7 @@
         canvas.save()
         canvas.translate(changingIconLeft, changingIcon2Top)
         canvas.scale(changingIconScaleX, changingIconScaleY)
-        appIcon2.alpha = changingIconAlpha
+        appIcon2!!.alpha = changingIconAlpha
         appIcon2.draw(canvas)
         canvas.restore()
     }
@@ -325,7 +326,7 @@
      * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top
      *   right y, bottom right x, and so on.
      */
-    private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
+    protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
             // Canvas.drawDoubleRoundRect is supported from Q onward
             c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint)
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
index e90aa13..e8d1cc1 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
@@ -40,8 +40,8 @@
         fun getFloatingAppPairView(
             launcher: StatefulActivity<*>,
             originalView: View,
-            appIcon1: Drawable,
-            appIcon2: Drawable,
+            appIcon1: Drawable?,
+            appIcon2: Drawable?,
             dividerPos: Int
         ): FloatingAppPairView {
             val dragLayer: ViewGroup = launcher.getDragLayer()
@@ -64,8 +64,8 @@
     fun init(
         launcher: StatefulActivity<*>,
         originalView: View,
-        appIcon1: Drawable,
-        appIcon2: Drawable,
+        appIcon1: Drawable?,
+        appIcon2: Drawable?,
         dividerPos: Int
     ) {
         val viewBounds = Rect(0, 0, originalView.width, originalView.height)
@@ -92,7 +92,14 @@
         layoutParams = lp
 
         // Prepare to draw app pair icon background
-        background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+        background = if (appIcon1 == null || appIcon2 == null) {
+            val iconToAnimate = appIcon1 ?: appIcon2
+            checkNotNull(iconToAnimate)
+            FloatingFullscreenAppPairBackground(context, this, iconToAnimate,
+                    dividerPos)
+        } else {
+            FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+        }
         background.setBounds(0, 0, lp.width, lp.height)
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt
new file mode 100644
index 0000000..8cd997f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+
+class FloatingFullscreenAppPairBackground(
+        context: Context,
+        floatingView: FloatingAppPairView,
+        private val iconToLaunch: Drawable,
+        dividerPos: Int) :
+        FloatingAppPairBackground(
+                context,
+                floatingView,
+                iconToLaunch,
+                null /*appIcon2*/,
+                dividerPos
+) {
+
+    /** Animates the background as if launching a fullscreen task. */
+    override fun draw(canvas: Canvas) {
+        val progress = floatingView.progress
+
+        // Since the entire floating app pair surface is scaling up during this animation, we
+        // scale down most of these drawn elements so that they appear the proper size on-screen.
+        val scaleFactorX = floatingView.scaleX
+        val scaleFactorY = floatingView.scaleY
+
+        // Get the bounds where we will draw the background image
+        val width = bounds.width().toFloat()
+        val height = bounds.height().toFloat()
+
+        // Get device-specific measurements
+        val cornerRadiusX = deviceCornerRadius / scaleFactorX
+        val cornerRadiusY = deviceCornerRadius / scaleFactorY
+
+        // Draw background
+        drawCustomRoundedRect(
+                canvas,
+                RectF(0f, 0f, width, height),
+                floatArrayOf(
+                        cornerRadiusX,
+                        cornerRadiusY,
+                        cornerRadiusX,
+                        cornerRadiusY,
+                        cornerRadiusX,
+                        cornerRadiusY,
+                        cornerRadiusX,
+                        cornerRadiusY,
+                )
+        )
+
+        // Calculate changing measurements for icon.
+        val changingIconSizeX =
+                (STARTING_ICON_SIZE_PX +
+                        ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                                expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+        val changingIconSizeY =
+                (STARTING_ICON_SIZE_PX +
+                        ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                                expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+        val changingIcon1Left = (width / 2f) - (changingIconSizeX / 2f)
+        val changingIconTop = (height / 2f) - (changingIconSizeY / 2f)
+        val changingIconScaleX = changingIconSizeX / iconToLaunch.bounds.width()
+        val changingIconScaleY = changingIconSizeY / iconToLaunch.bounds.height()
+        val changingIconAlpha =
+                (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt()
+
+        // Draw icon
+        canvas.save()
+        canvas.translate(changingIcon1Left, changingIconTop)
+        canvas.scale(changingIconScaleX, changingIconScaleY)
+        iconToLaunch.alpha = changingIconAlpha
+        iconToLaunch.draw(canvas)
+        canvas.restore()
+    }
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index a593712..c7a4203 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -6,6 +6,7 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell;
 
+import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -46,6 +47,7 @@
 
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Optional;
 import java.util.function.Consumer;
 
 /**
@@ -60,7 +62,7 @@
  */
 public class GroupedTaskView extends TaskView {
 
-    private static final String TAG = TaskView.class.getSimpleName();
+    private static final String TAG = GroupedTaskView.class.getSimpleName();
     @Nullable
     private Task mSecondaryTask;
     // TODO(b/336612373): Support new TTV for GroupedTaskView
@@ -129,9 +131,11 @@
             @Nullable SplitBounds splitBoundsConfig) {
         super.bind(primary, orientedState);
         mSecondaryTask = secondary;
-        mTaskIdContainer[1] = secondary.key.id;
-        mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2,
-                mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT);
+        mTaskIdContainer = new int[]{mTaskIdContainer[0], secondary.key.id};
+        mTaskIdAttributeContainer = new TaskIdAttributeContainer[]{
+                mTaskIdAttributeContainer[0],
+                new TaskIdAttributeContainer(secondary, mSnapshotView2,
+                        mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT)};
         mTaskIdAttributeContainer[0].setStagePosition(
                 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT);
         mSnapshotView2.bind(secondary);
@@ -154,6 +158,9 @@
     public void setUpShowAllInstancesListener() {
         // sets up the listener for the left/top task
         super.setUpShowAllInstancesListener();
+        if (mTaskIdAttributeContainer.length < 2) {
+            return;
+        }
 
         // right/bottom task's base package name
         String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName();
@@ -308,16 +315,20 @@
     }
 
     @Override
-    public boolean containsTaskId(int taskId) {
-        return (mTask != null && mTask.key.id == taskId)
-                || (mSecondaryTask != null && mSecondaryTask.key.id == taskId);
-    }
-
-    @Override
     public TaskThumbnailViewDeprecated[] getThumbnails() {
         return new TaskThumbnailViewDeprecated[]{mTaskThumbnailViewDeprecated, mSnapshotView2};
     }
 
+    /**
+     * Returns taskId that split selection was initiated with,
+     * {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of
+     * split selection
+     */
+    protected int getThisTaskCurrentlyInSplitSelection() {
+        int initialTaskId = getRecentsView().getSplitSelectController().getInitialTaskId();
+        return containsTaskId(initialTaskId) ? initialTaskId : INVALID_TASK_ID;
+    }
+
     @Override
     protected int getLastSelectedChildTaskIndex() {
         SplitSelectStateController splitSelectController =
@@ -382,13 +393,15 @@
         } else {
             // Currently being split with this taskView, let the non-split selected thumbnail
             // take up full thumbnail area
-            TaskIdAttributeContainer container =
-                    mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0];
-            container.getThumbnailView().measure(widthMeasureSpec,
-                    View.MeasureSpec.makeMeasureSpec(
-                            heightSize -
-                                    mContainer.getDeviceProfile().overviewTaskThumbnailTopMarginPx,
-                            MeasureSpec.EXACTLY));
+            Optional<TaskIdAttributeContainer> nonSplitContainer = Arrays.stream(
+                    mTaskIdAttributeContainer).filter(
+                            container -> container.getTask().key.id != initSplitTaskId).findAny();
+            nonSplitContainer.ifPresent(
+                    taskIdAttributeContainer -> taskIdAttributeContainer.getThumbnailView().measure(
+                            widthMeasureSpec, MeasureSpec.makeMeasureSpec(
+                                    heightSize - mContainer.getDeviceProfile()
+                                            .overviewTaskThumbnailTopMarginPx,
+                                    MeasureSpec.EXACTLY)));
         }
         if (!enableOverviewIconMenu()) {
             updateIconPlacement();
@@ -529,7 +542,7 @@
             mDigitalWellBeingToast.setBannerVisibility(visibility);
             mSnapshotView2.setVisibility(visibility);
             mDigitalWellBeingToast2.setBannerVisibility(visibility);
-        } else if (taskId == getTaskIds()[0]) {
+        } else if (mTaskIdContainer.length > 0 && mTaskIdContainer[0] == taskId) {
             mTaskThumbnailViewDeprecated.setVisibility(visibility);
             mDigitalWellBeingToast.setBannerVisibility(visibility);
         } else {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5daafcf..62fa6c8 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -626,7 +626,6 @@
      */
     protected int mRunningTaskViewId = -1;
     private int mTaskViewIdCount;
-    private final int[] INVALID_TASK_IDS = new int[]{-1, -1};
     protected boolean mRunningTaskTileHidden;
     @Nullable
     private Task[] mTmpRunningTasks;
@@ -1413,7 +1412,7 @@
      */
     @Nullable
     public TaskView getTaskViewByTaskIds(int[] taskIds) {
-        if (!hasAnyValidTaskIds(taskIds)) {
+        if (!hasAllValidTaskIds(taskIds)) {
             return null;
         }
 
@@ -1432,9 +1431,11 @@
         return null;
     }
 
-    /** Returns false if {@code taskIds} is null or contains invalid values, true otherwise */
-    private boolean hasAnyValidTaskIds(int[] taskIds) {
-        return taskIds != null && !Arrays.equals(taskIds, INVALID_TASK_IDS);
+    /** Returns false if {@code taskIds} is null or contains any invalid values, true otherwise */
+    private boolean hasAllValidTaskIds(int[] taskIds) {
+        return taskIds != null
+                && taskIds.length > 0
+                && Arrays.stream(taskIds).noneMatch(taskId -> taskId == INVALID_TASK_ID);
     }
 
     public void setOverviewStateEnabled(boolean enabled) {
@@ -1707,6 +1708,12 @@
             return;
         }
 
+        if (taskGroups == null) {
+            Log.d(TAG, "applyLoadPlan - taskGroups is null");
+        } else {
+            Log.d(TAG, "applyLoadPlan - taskGroups: " + taskGroups.stream().map(
+                    GroupTask::toString).toList());
+        }
         mLoadPlanEverApplied = true;
         if (taskGroups == null || taskGroups.isEmpty()) {
             removeTasksViewsAndClearAllButton();
@@ -1720,10 +1727,12 @@
             return;
         }
 
-        int[] currentTaskId = INVALID_TASK_IDS;
+        int[] currentTaskIds;
         TaskView currentTaskView = getTaskViewAt(mCurrentPage);
         if (currentTaskView != null && currentTaskView.getTask() != null) {
-            currentTaskId = currentTaskView.getTaskIds();
+            currentTaskIds = currentTaskView.getTaskIds();
+        } else {
+            currentTaskIds = new int[0];
         }
 
         // Unload existing visible task data
@@ -1735,8 +1744,8 @@
 
         // Save running task ID if it exists before rebinding all taskViews, otherwise the task from
         // the runningTaskView currently bound could get assigned to another TaskView
-        int[] runningTaskId = getTaskIdsForTaskViewId(mRunningTaskViewId);
-        int[] focusedTaskId = getTaskIdsForTaskViewId(mFocusedTaskViewId);
+        int[] runningTaskIds = getTaskIdsForTaskViewId(mRunningTaskViewId);
+        int[] focusedTaskIds = getTaskIdsForTaskViewId(mFocusedTaskViewId);
 
         // Reset the focused task to avoiding initializing TaskViews layout as focused task during
         // binding. The focused task view will be updated after all the TaskViews are bound.
@@ -1819,7 +1828,7 @@
         }
 
         // Keep same previous focused task
-        TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskId);
+        TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
         // If the list changed, maybe the focused task doesn't exist anymore
         if (newFocusedTaskView == null && getTaskViewCount() > 0) {
             newFocusedTaskView = getTaskViewAt(0);
@@ -1830,10 +1839,10 @@
         updateChildTaskOrientations();
 
         TaskView newRunningTaskView = null;
-        if (hasAnyValidTaskIds(runningTaskId)) {
+        if (hasAllValidTaskIds(runningTaskIds)) {
             // Update mRunningTaskViewId to be the new TaskView that was assigned by binding
             // the full list of tasks to taskViews
-            newRunningTaskView = getTaskViewByTaskIds(runningTaskId);
+            newRunningTaskView = getTaskViewByTaskIds(runningTaskIds);
             if (newRunningTaskView != null) {
                 setRunningTaskViewId(newRunningTaskView.getTaskViewId());
             } else {
@@ -1853,8 +1862,8 @@
         if (mNextPage != INVALID_PAGE) {
             // Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll.
             mCurrentPage = previousCurrentPage;
-            if (hasAnyValidTaskIds(currentTaskId)) {
-                currentTaskView = getTaskViewByTaskIds(currentTaskId);
+            if (hasAllValidTaskIds(currentTaskIds)) {
+                currentTaskView = getTaskViewByTaskIds(currentTaskIds);
                 if (currentTaskView != null) {
                     targetPage = indexOfChild(currentTaskView);
                 }
@@ -1863,7 +1872,7 @@
             targetPage = previousFocusedPage;
         } else {
             // Set the current page to the running task, but not if settling on new task.
-            if (hasAnyValidTaskIds(runningTaskId)) {
+            if (hasAllValidTaskIds(runningTaskIds)) {
                 targetPage = indexOfChild(newRunningTaskView);
             } else if (getTaskViewCount() > 0) {
                 targetPage = indexOfChild(requireTaskViewAt(0));
@@ -1963,7 +1972,8 @@
     public void resetTaskVisuals() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             TaskView taskView = requireTaskViewAt(i);
-            if (mIgnoreResetTaskId != taskView.getTaskIds()[0]) {
+            if (Arrays.stream(taskView.getTaskIds()).noneMatch(
+                    taskId -> taskId == mIgnoreResetTaskId)) {
                 taskView.resetViewTransforms();
                 taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
                 taskView.setStableAlpha(mContentAlpha);
@@ -2343,7 +2353,7 @@
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
             TaskIdAttributeContainer[] containers = taskView.getTaskIdAttributeContainers();
-            if (containers[0] == null && containers[1] == null) {
+            if (containers.length == 0) {
                 continue;
             }
             int index = indexOfChild(taskView);
@@ -2498,7 +2508,7 @@
         // For now 2 distinct task IDs is max for split screen
         TaskView runningTaskView = getTaskViewFromTaskViewId(taskViewId);
         if (runningTaskView == null) {
-            return INVALID_TASK_IDS;
+            return new int[0];
         }
 
         return runningTaskView.getTaskIds();
@@ -2587,7 +2597,7 @@
      */
     public void onGestureAnimationStart(
             Task[] runningTasks, RotationTouchHelper rotationTouchHelper) {
-        Log.d(TAG, "onGestureAnimationStart");
+        Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
         mActiveGestureRunningTasks = runningTasks;
         // This needs to be called before the other states are set since it can create the task view
         if (mOrientationState.setGestureActive(true)) {
@@ -2736,22 +2746,19 @@
      * Returns true if we should add a stub taskView for the running task id
      */
     protected boolean shouldAddStubTaskView(Task[] runningTasks) {
-        TaskView taskView = getTaskViewByTaskId(runningTasks[0].key.id);
-        if (taskView == null) {
-            // No TaskView found, add a stub task.
-            return true;
-        }
-
-        if (runningTasks.length > 1) {
-            // Ensure all taskIds matches the TaskView, otherwise add a stub task.
-            return Arrays.stream(runningTasks).anyMatch(
-                    runningTask -> !taskView.containsTaskId(runningTask.key.id));
+        int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
+        TaskView matchingTaskView = null;
+        if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
+            // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single
+            // taskId, therefore we match any DesktopTaskView that contains the runningTaskId.
+            TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]);
+            if (taskview instanceof DesktopTaskView) {
+                matchingTaskView = taskview;
+            }
         } else {
-            // Ensure the TaskView only contains a single taskId, or is a DesktopTask,
-            // otherwise add a stub task.
-            // TODO(b/249371338): Figure out why DesktopTask only have a single runningTask.
-            return taskView.containsMultipleTasks() && !taskView.isDesktopTask();
+            matchingTaskView = getTaskViewByTaskIds(runningTaskIds);
         }
+        return matchingTaskView == null;
     }
 
     /**
@@ -2761,6 +2768,7 @@
      * is called.  Also scrolls the view to this task.
      */
     private void showCurrentTask(Task[] runningTasks) {
+        Log.d(TAG, "showCurrentTask - runningTasks: " + Arrays.toString(runningTasks));
         if (runningTasks.length == 0) {
             return;
         }
@@ -4273,13 +4281,10 @@
         alpha = Utilities.boundToRange(alpha, 0, 1);
         mContentAlpha = alpha;
 
-        int runningTaskId = getTaskIdsForRunningTaskView()[0];
+        TaskView runningTaskView = getRunningTaskView();
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             TaskView child = requireTaskViewAt(i);
-            int[] childTaskIds = child.getTaskIds();
-            if (runningTaskId != INVALID_TASK_ID
-                    && mRunningTaskTileHidden
-                    && (childTaskIds[0] == runningTaskId || childTaskIds[1] == runningTaskId)) {
+            if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) {
                 continue;
             }
             child.setStableAlpha(alpha);
@@ -4753,7 +4758,7 @@
 
         // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
         mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
-                && mSplitHiddenTaskView.containsMultipleTasks());
+                && mSplitHiddenTaskView instanceof GroupedTaskView);
         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
                 splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
                 splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
@@ -4774,14 +4779,14 @@
                 mSplitSelectStateController.isAnimateCurrentTaskDismissal();
         boolean isInitiatingTaskViewSplitPair =
                 mSplitSelectStateController.isDismissingFromSplitPair();
-        if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) {
+        if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair
+                && mSplitHiddenTaskView instanceof GroupedTaskView) {
             // Splitting from Overview for split pair task
             createInitialSplitSelectAnimation(builder);
 
             // Animate pair thumbnail into full thumbnail
-            boolean primaryTaskSelected =
-                    mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id ==
-                            mSplitSelectStateController.getInitialTaskId();
+            boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIds()[0]
+                    == mSplitSelectStateController.getInitialTaskId();
             TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView
                     .getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0];
             TaskThumbnailViewDeprecated thumbnail = taskIdAttributeContainer.getThumbnailView();
@@ -5232,7 +5237,8 @@
         mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
         mPendingAnimation.addEndListener(isSuccess -> {
             if (isSuccess) {
-                if (tv.getTaskIds()[1] != -1 && mRemoteTargetHandles != null) {
+                if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds())
+                        && mRemoteTargetHandles != null) {
                     // TODO(b/194414938): make this part of the animations instead.
                     TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
                             mRemoteTargetHandles[0].getTransformParams().getTargetSet().nonApps,
@@ -5457,8 +5463,9 @@
      * Called when a running recents animation has finished or canceled.
      */
     public void onRecentsAnimationComplete() {
-        Log.d(TAG, "onRecentsAnimationComplete - mRecentsAnimationController: "
-                + mRecentsAnimationController);
+        Log.d(TAG, "onRecentsAnimationComplete "
+                + "- mRecentsAnimationController: " + mRecentsAnimationController
+                + ", mSideTaskLaunchCallback: " + mSideTaskLaunchCallback);
         // At this point, the recents animation is not running and if the animation was canceled
         // by a display rotation then reset this state to show the screenshot
         setRunningTaskViewShowScreenshot(true);
@@ -5879,8 +5886,7 @@
         }
 
         taskView.setShowScreenshot(true);
-        for (TaskIdAttributeContainer container :
-                taskView.getTaskIdAttributeContainers()) {
+        for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) {
             if (container == null) {
                 continue;
             }
@@ -6012,7 +6018,8 @@
     }
 
     public void cleanupRemoteTargets() {
-        Log.d(TAG, "cleanupRemoteTargets");
+        Log.d(TAG, "cleanupRemoteTargets - mRemoteTargetHandles: " + Arrays.toString(
+                mRemoteTargetHandles));
         mRemoteTargetHandles = null;
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 8fd99de..1e2a259 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.views;
 
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.widget.Toast.LENGTH_SHORT;
 
@@ -48,7 +47,6 @@
 import android.animation.ObjectAnimator;
 import android.annotation.IdRes;
 import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.TypedArray;
@@ -127,6 +125,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
@@ -150,7 +149,8 @@
      */
     @Retention(SOURCE)
     @IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS})
-    public @interface TaskDataChanges {}
+    public @interface TaskDataChanges {
+    }
 
     /**
      * Type of task view
@@ -371,12 +371,9 @@
     private float mStableAlpha = 1;
 
     private int mTaskViewId = -1;
-    /**
-     * Index 0 will contain taskID of left/top task, index 1 will contain taskId of bottom/right
-     */
-    protected int[] mTaskIdContainer = new int[]{-1, -1};
+    protected int[] mTaskIdContainer = new int[0];
     protected TaskIdAttributeContainer[] mTaskIdAttributeContainer =
-            new TaskIdAttributeContainer[2];
+            new TaskIdAttributeContainer[0];
 
     private boolean mShowScreenshot;
     private boolean mBorderEnabled;
@@ -395,9 +392,11 @@
 
     private boolean mIsClickableAsLiveTile = true;
 
-    @Nullable private final BorderAnimator mFocusBorderAnimator;
+    @Nullable
+    private final BorderAnimator mFocusBorderAnimator;
 
-    @Nullable private final BorderAnimator mHoverBorderAnimator;
+    @Nullable
+    private final BorderAnimator mHoverBorderAnimator;
 
     public TaskView(Context context) {
         this(context, null);
@@ -583,6 +582,10 @@
      * Enable or disable showing border on hover and focus change
      */
     public void setBorderEnabled(boolean enabled) {
+        if (mBorderEnabled == enabled) {
+            return;
+        }
+
         mBorderEnabled = enabled;
         // Set the animation correctly in case it misses the hover/focus event during state
         // transition
@@ -674,10 +677,10 @@
     public void bind(Task task, RecentsOrientedState orientedState) {
         cancelPendingLoadTasks();
         mTask = task;
-        mTaskIdContainer[0] = mTask.key.id;
-        mTaskIdAttributeContainer[0] = new TaskIdAttributeContainer(task,
-                mTaskThumbnailViewDeprecated, mIconView,
-                STAGE_POSITION_UNDEFINED);
+        mTaskIdContainer = new int[]{mTask.key.id};
+        mTaskIdAttributeContainer = new TaskIdAttributeContainer[]{
+                new TaskIdAttributeContainer(task, mTaskThumbnailViewDeprecated, mIconView,
+                        STAGE_POSITION_UNDEFINED)};
         if (enableRefactorTaskThumbnail()) {
             bindTaskThumbnailView();
         } else {
@@ -696,6 +699,9 @@
      * Sets up an on-click listener and the visibility for show_windows icon on top of the task.
      */
     public void setUpShowAllInstancesListener() {
+        if (mTaskIdAttributeContainer.length == 0) {
+            return;
+        }
         String taskPackageName = mTaskIdAttributeContainer[0].mTask.key.getPackageName();
 
         // icon of the top/left task
@@ -751,19 +757,18 @@
      * Check if given {@code taskId} is tracked in this view
      */
     public boolean containsTaskId(int taskId) {
-        return mTask != null && mTask.key.id == taskId;
+        return Arrays.stream(mTaskIdContainer).anyMatch(myTaskId -> myTaskId == taskId);
     }
 
     /**
-     * @return integer array of two elements to be size consistent with max number of tasks possible
-     *         index 0 will contain the taskId, index 1 will be -1 indicating a null taskID value
+     * Returns a copy of integer array containing taskIds of all tasks in the TaskView.
      */
     public int[] getTaskIds() {
         return Arrays.copyOf(mTaskIdContainer, mTaskIdContainer.length);
     }
 
     public boolean containsMultipleTasks() {
-        return mTaskIdContainer[1] != -1;
+        return mTaskIdContainer.length > 1;
     }
 
     /**
@@ -833,25 +838,6 @@
         return super.dispatchTouchEvent(ev);
     }
 
-    /**
-     * @return taskId that split selection was initiated with,
-     *         {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of
-     *         split selection
-     */
-    protected int getThisTaskCurrentlyInSplitSelection() {
-        SplitSelectStateController splitSelectController =
-                getRecentsView().getSplitSelectController();
-        int initSplitTaskId = INVALID_TASK_ID;
-        for (TaskIdAttributeContainer container : getTaskIdAttributeContainers()) {
-            int taskId = container.getTask().key.id;
-            if (taskId == splitSelectController.getInitialTaskId()) {
-                initSplitTaskId = taskId;
-                break;
-            }
-        }
-        return initSplitTaskId;
-    }
-
     private void onClick(View view) {
         if (getTask() == null) {
             Log.d("b/310064698", "onClick - task is null");
@@ -864,7 +850,8 @@
         RunnableList callbackList = launchTasks();
         Log.d("b/310064698", mTask + " - onClick - callbackList: " + callbackList);
         if (callbackList != null) {
-            callbackList.add(() -> Log.d("b/310064698", mTask + " - onClick - launchCompleted"));
+            callbackList.add(() -> Log.d("b/310064698", Arrays.toString(
+                    getTaskIds()) + " - onClick - launchCompleted"));
         }
         mContainer.getStatsLogManager().logger().withItemInfo(getItemInfo())
                 .log(LAUNCHER_TASK_LAUNCH_TAP);
@@ -872,10 +859,13 @@
 
     /**
      * @return {@code true} if user is already in split select mode and this tap was to choose the
-     *         second app. {@code false} otherwise
+     * second app. {@code false} otherwise
      */
     protected boolean confirmSecondSplitSelectApp() {
         int index = getLastSelectedChildTaskIndex();
+        if (index >= mTaskIdAttributeContainer.length) {
+            return false;
+        }
         TaskIdAttributeContainer container = mTaskIdAttributeContainer[index];
         if (container != null) {
             return getRecentsView().confirmSplitSelect(this, container.getTask(),
@@ -897,6 +887,7 @@
 
     /**
      * Starts the task associated with this view and animates the startup.
+     *
      * @return CompletionStage to indicate the animation completion or null if the launch failed.
      */
     @Nullable
@@ -904,7 +895,7 @@
         if (mTask != null) {
             TestLogging.recordEvent(
                     TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
-            ActivityOptionsWrapper opts =  mContainer.getActivityLaunchOptions(this, null);
+            ActivityOptionsWrapper opts = mContainer.getActivityLaunchOptions(this, null);
             opts.options.setLaunchDisplayId(
                     getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
             if (ActivityManagerWrapper.getInstance()
@@ -936,7 +927,7 @@
                 return null;
             }
         } else {
-            Log.d(TAG, "launchTaskAnimated - mTask is null");
+            Log.d(TAG, "launchTaskAnimated - mTask is null" + Arrays.toString(getTaskIds()));
             return null;
         }
     }
@@ -1006,9 +997,12 @@
                         callback.accept(false);
                     });
                 }
+                Log.d(TAG,
+                        "launchTask - startActivityFromRecents: " + Arrays.toString(getTaskIds()));
             });
         } else {
             callback.accept(false);
+            Log.d(TAG, "launchTask - mTask is null" + Arrays.toString(getTaskIds()));
         }
     }
 
@@ -1092,6 +1086,7 @@
 
     /**
      * See {@link TaskDataChanges}
+     *
      * @param visible If this task view will be visible to the user in overview or hidden
      */
     public void onTaskListVisibilityChanged(boolean visible) {
@@ -1100,6 +1095,7 @@
 
     /**
      * See {@link TaskDataChanges}
+     *
      * @param visible If this task view will be visible to the user in overview or hidden
      */
     public void onTaskListVisibilityChanged(boolean visible, @TaskDataChanges int changes) {
@@ -1188,29 +1184,34 @@
     }
 
     protected boolean showTaskMenuWithContainer(TaskViewIcon iconView) {
-        TaskIdAttributeContainer menuContainer =
-                mTaskIdAttributeContainer[iconView == mIconView ? 0 : 1];
+        Optional<TaskIdAttributeContainer> menuContainer = Arrays.stream(
+                mTaskIdAttributeContainer).filter(
+                        container -> container.getIconView() == iconView).findAny();
+        if (menuContainer.isEmpty()) {
+            return false;
+        }
         DeviceProfile dp = mContainer.getDeviceProfile();
         if (enableOverviewIconMenu() && iconView instanceof IconAppChipView) {
             ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ true);
-            return TaskMenuView.showForTask(menuContainer,
+            return TaskMenuView.showForTask(menuContainer.get(),
                     () -> ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false));
         } else if (dp.isTablet) {
             int alignedOptionIndex = 0;
-            if (getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) && dp.isLandscape) {
+            if (getRecentsView().isOnGridBottomRow(menuContainer.get().getTaskView())
+                    && dp.isLandscape) {
                 if (Flags.enableGridOnlyOverview()) {
                     // With no focused task, there is less available space below the tasks, so align
                     // the arrow to the third option in the menu.
                     alignedOptionIndex = 2;
-                } else  {
+                } else {
                     // Bottom row of landscape grid aligns arrow to second option to avoid clipping
                     alignedOptionIndex = 1;
                 }
             }
-            return TaskMenuViewWithArrow.Companion.showForTask(menuContainer,
+            return TaskMenuViewWithArrow.Companion.showForTask(menuContainer.get(),
                     alignedOptionIndex);
         } else {
-            return TaskMenuView.showForTask(menuContainer);
+            return TaskMenuView.showForTask(menuContainer.get());
         }
     }
 
@@ -1664,9 +1665,6 @@
 
         final Context context = getContext();
         for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
-            if (taskContainer == null) {
-                continue;
-            }
             for (SystemShortcut s : TraceHelper.allowIpcs(
                     "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) {
                 info.addAction(s.createAccessibilityAction(context));
@@ -1702,9 +1700,6 @@
         }
 
         for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
-            if (taskContainer == null) {
-                continue;
-            }
             for (SystemShortcut s : getEnabledShortcuts(this,
                     taskContainer)) {
                 if (s.hasHandlerForAction(action)) {
@@ -1903,13 +1898,13 @@
 
 
     private int getRootViewDisplayId() {
-        Display  display = getRootView().getDisplay();
+        Display display = getRootView().getDisplay();
         return display != null ? display.getDisplayId() : DEFAULT_DISPLAY;
     }
 
     /**
-     *  Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
-     *  IconView is unaffected.
+     * Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
+     * IconView is unaffected.
      *
      * @param taskId is only used when setting visibility to a non-{@link View#VISIBLE} value
      */
@@ -1966,7 +1961,8 @@
         }
 
         @Override
-        public void close() { }
+        public void close() {
+        }
     }
 
     public class TaskIdAttributeContainer {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 3d8484d..f46fdac 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -22,7 +22,6 @@
 import android.graphics.drawable.ColorDrawable
 import android.view.LayoutInflater
 import android.view.View
-import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
 import androidx.core.graphics.drawable.toBitmap
@@ -45,6 +44,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
 @SmallTest
@@ -87,6 +87,12 @@
 
         val bubbleStashController = mock<BubbleStashController>()
         whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
+        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+            .thenReturn(HANDLE_TRANSLATION)
+        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -104,13 +110,13 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(0)
-        assertThat(handle.translationY).isEqualTo(-70)
-        assertThat(overflowView.visibility).isEqualTo(INVISIBLE)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
-        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
-        assertThat(bubbleView.alpha).isEqualTo(1)
-        assertThat(bubbleView.translationY).isEqualTo(-20)
-        assertThat(bubbleView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
 
         // execute the hide bubble animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -120,14 +126,11 @@
         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
-        assertThat(bubbleView.alpha).isEqualTo(1)
-        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
-        assertThat(bubbleView.translationY).isEqualTo(0)
-        assertThat(bubbleBarView.alpha).isEqualTo(0)
-        assertThat(overflowView.alpha).isEqualTo(1)
-        assertThat(overflowView.visibility).isEqualTo(VISIBLE)
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
     }
 
     @Test
@@ -158,6 +161,12 @@
 
         val bubbleStashController = mock<BubbleStashController>()
         whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
+        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+            .thenReturn(HANDLE_TRANSLATION)
+        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -175,13 +184,15 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(0)
-        assertThat(handle.translationY).isEqualTo(-70)
-        assertThat(overflowView.visibility).isEqualTo(INVISIBLE)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
-        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
-        assertThat(bubbleView.alpha).isEqualTo(1)
-        assertThat(bubbleView.translationY).isEqualTo(-20)
-        assertThat(bubbleView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+        verify(bubbleStashController).updateTaskbarTouchRegion()
 
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -189,11 +200,9 @@
         animator.onBubbleClickedWhileAnimating()
 
         assertThat(animatorScheduler.delayedBlock).isNull()
-        assertThat(overflowView.visibility).isEqualTo(VISIBLE)
-        assertThat(overflowView.alpha).isEqualTo(1)
-        assertThat(bubbleView.alpha).isEqualTo(1)
-        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
-        assertThat(bubbleBarView.background).isNotNull()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
     }
 
@@ -217,3 +226,7 @@
         }
     }
 }
+
+private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
+private const val HANDLE_TRANSLATION = -30f
+private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
index c327166..c8f7946 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt
@@ -17,8 +17,6 @@
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_CONTEXTUAL_BUTTONS
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_NAV_BUTTONS
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_START_CONTEXTUAL_BUTTONS
-import com.android.systemui.shared.rotation.RotationButton
-import java.lang.IllegalStateException
 import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
@@ -39,10 +37,9 @@
     private val mockRecentsButton: ImageView = mock()
     private val mockHomeButton: ImageView = mock()
     private val mockImeSwitcher: ImageView = mock()
-    private val mockRotationButton: RotationButton = mock()
     private val mockA11yButton: ImageView = mock()
     private val mockSpace: Space = mock()
-    private val mockConfiguration: Configuration = mock();
+    private val mockConfiguration: Configuration = mock()
 
     private var surfaceRotation = Surface.ROTATION_0
 
@@ -210,7 +207,6 @@
             phoneMode = phoneMode,
             surfaceRotation = surfaceRotation,
             imeSwitcher = mockImeSwitcher,
-            rotationButton = mockRotationButton,
             a11yButton = mockA11yButton,
             space = mockSpace,
         )
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
index 2cfcf38..4fafde8 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/DesktopTaskbarRunningAppsControllerTest.kt
@@ -23,7 +23,6 @@
 import android.os.Process
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
-import android.util.SparseArray
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DesktopVisibilityController
@@ -55,7 +54,9 @@
         super.setup()
         userHandle = Process.myUserHandle()
         taskbarRunningAppsController =
-            DesktopTaskbarRunningAppsController(mockRecentsModel, mockDesktopVisibilityController)
+            DesktopTaskbarRunningAppsController(mockRecentsModel) {
+                mockDesktopVisibilityController
+            }
         taskbarRunningAppsController.init(taskbarControllers)
         taskbarRunningAppsController.setApps(
             ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
@@ -63,20 +64,13 @@
     }
 
     @Test
-    fun updateHotseatItemInfos_null_returnsNull() {
-        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(/* hotseatItems= */ null))
-            .isNull()
-    }
-
-    @Test
     fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() {
         setInDesktopMode(false)
         val hotseatItems =
             createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-                .toTypedArray()
 
-        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems))
-            .isEqualTo(hotseatItems)
+        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
+            .isEqualTo(hotseatItems.toTypedArray())
     }
 
     @Test
@@ -87,23 +81,22 @@
         val runningTasks =
             createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
         whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        taskbarRunningAppsController.updateRunningApps(createSparseArray(hotseatItems))
+        taskbarRunningAppsController.updateRunningApps()
 
         val newHotseatItems =
             taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
 
-        assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(hotseatPackages)
+        assertThat(newHotseatItems.map { it?.targetPackage }).isEqualTo(hotseatPackages)
     }
 
     @Test
     fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() {
         setInDesktopMode(true)
-        val hotseatItems: Array<ItemInfo> =
+        val hotseatItems =
             createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-                .toTypedArray()
 
-        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems))
-            .isEqualTo(hotseatItems)
+        assertThat(taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
+            .isEqualTo(hotseatItems.toTypedArray())
     }
 
     @Test
@@ -114,7 +107,7 @@
         val runningTasks =
             createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
         whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        taskbarRunningAppsController.updateRunningApps(createSparseArray(hotseatItems))
+        taskbarRunningAppsController.updateRunningApps()
 
         val newHotseatItems =
             taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
@@ -126,7 +119,7 @@
                 RUNNING_APP_PACKAGE_1,
                 RUNNING_APP_PACKAGE_2,
             )
-        assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(expectedPackages)
+        assertThat(newHotseatItems.map { it?.targetPackage }).isEqualTo(expectedPackages)
     }
 
     @Test
@@ -139,7 +132,7 @@
                 listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
             )
         whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        taskbarRunningAppsController.updateRunningApps(createSparseArray(hotseatItems))
+        taskbarRunningAppsController.updateRunningApps()
 
         val newHotseatItems =
             taskbarRunningAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
@@ -151,7 +144,7 @@
                 RUNNING_APP_PACKAGE_1,
                 RUNNING_APP_PACKAGE_2,
             )
-        assertThat(newHotseatItems?.map { it.targetPackage }).isEqualTo(expectedPackages)
+        assertThat(newHotseatItems.map { it?.targetPackage }).isEqualTo(expectedPackages)
     }
 
     @Test
@@ -160,7 +153,7 @@
         val runningTasks =
             createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
         whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        taskbarRunningAppsController.updateRunningApps(createSparseArray(emptyList()))
+        taskbarRunningAppsController.updateRunningApps()
 
         assertThat(taskbarRunningAppsController.runningApps).isEqualTo(emptySet<String>())
     }
@@ -171,7 +164,7 @@
         val runningTasks =
             createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
         whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
-        taskbarRunningAppsController.updateRunningApps(createSparseArray(emptyList()))
+        taskbarRunningAppsController.updateRunningApps()
 
         assertThat(taskbarRunningAppsController.runningApps)
             .isEqualTo(setOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
@@ -204,12 +197,6 @@
         whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
     }
 
-    private fun createSparseArray(itemInfos: List<ItemInfo>): SparseArray<ItemInfo> {
-        val sparseArray = SparseArray<ItemInfo>()
-        itemInfos.forEachIndexed { index, itemInfo -> sparseArray[index] = itemInfo }
-        return sparseArray
-    }
-
     private companion object {
         const val HOTSEAT_PACKAGE_1 = "hotseat1"
         const val HOTSEAT_PACKAGE_2 = "hotseat2"
diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
index 8eec903..512557b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
@@ -17,9 +17,11 @@
 package com.android.quickstep;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -34,7 +36,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.quickstep.util.BorderAnimator;
 import com.android.quickstep.views.TaskView;
@@ -74,6 +75,7 @@
 
     @Test
     public void notShowBorderOnBorderDisabled() {
+        presetBorderStatus(/* enabled= */ true);
         mTaskView.setBorderEnabled(/* enabled= */ false);
         MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
         mTaskView.onHoverEvent(MotionEvent.obtain(event));
@@ -86,7 +88,7 @@
     }
 
     @Test
-    public void showBorderOnBorderEnabled() {
+    public void showBorderOnHoverEvent() {
         mTaskView.setBorderEnabled(/* enabled= */ true);
         MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
         mTaskView.onHoverEvent(MotionEvent.obtain(event));
@@ -98,7 +100,18 @@
     }
 
     @Test
+    public void showBorderOnBorderEnabled() {
+        presetBorderStatus(/* enabled= */ false);
+        mTaskView.setBorderEnabled(/* enabled= */ true);
+        verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
+                true);
+        verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
+                true);
+    }
+
+    @Test
     public void hideBorderOnBorderDisabled() {
+        presetBorderStatus(/* enabled= */ true);
         mTaskView.setBorderEnabled(/* enabled= */ false);
         verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ false, /* animated= */
                 true);
@@ -107,13 +120,35 @@
     }
 
     @Test
+    public void notTriggerAnimatorWhenEnableStatusUnchanged() {
+        presetBorderStatus(/* enabled= */ false);
+        // Border is disabled by default, no animator is triggered after it is disabled again
+        mTaskView.setBorderEnabled(/* enabled= */ false);
+        verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */
+                anyBoolean(), /* animated= */ anyBoolean());
+        verify(mFocusAnimator, never()).setBorderVisibility(/* visible= */
+                anyBoolean(), /* animated= */ anyBoolean());
+    }
+
+    private void presetBorderStatus(boolean enabled) {
+        // Make the task view focused and hovered
+        MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
+        mTaskView.onHoverEvent(MotionEvent.obtain(event));
+        mTaskView.requestFocus();
+        mTaskView.setBorderEnabled(/* enabled= */ enabled);
+        // Reset invocation count after presetting status
+        reset(mHoverAnimator);
+        reset(mFocusAnimator);
+    }
+
+    @Test
     public void notShowBorderByDefault() {
         MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
         mTaskView.onHoverEvent(MotionEvent.obtain(event));
-        verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */ false, /* animated= */
-                true);
+        verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */
+                anyBoolean(), /* animated= */ anyBoolean());
         mTaskView.onFocusChanged(true, 0, new Rect());
-        verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */ false, /* animated= */
-                true);
+        verify(mHoverAnimator, never()).setBorderVisibility(/* visible= */
+                anyBoolean(), /* animated= */ anyBoolean());
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
index dbe4624..4d10f0f 100644
--- a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
@@ -53,7 +54,13 @@
 class TaskbarPinningControllerTest : TaskbarBaseTestCase() {
     private val taskbarDragLayer = mock<TaskbarDragLayer>()
     private val taskbarSharedState = mock<TaskbarSharedState>()
-    private val launcherPrefs = mock<LauncherPrefs> { on { get(TASKBAR_PINNING) } doReturn false }
+    private var isInDesktopMode = false
+    private val isInDesktopModeProvider = { isInDesktopMode }
+    private val launcherPrefs =
+        mock<LauncherPrefs> {
+            on { get(TASKBAR_PINNING) } doReturn false
+            on { get(TASKBAR_PINNING_IN_DESKTOP_MODE) } doReturn false
+        }
     private val statsLogger = mock<StatsLogManager.StatsLogger>()
     private val statsLogManager = mock<StatsLogManager> { on { logger() } doReturn statsLogger }
     private lateinit var pinningController: TaskbarPinningController
@@ -64,7 +71,8 @@
         whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs)
         whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer)
         whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager)
-        pinningController = spy(TaskbarPinningController(taskbarActivityContext))
+        pinningController =
+            spy(TaskbarPinningController(taskbarActivityContext, isInDesktopModeProvider))
         pinningController.init(taskbarControllers, taskbarSharedState)
     }
 
@@ -95,7 +103,7 @@
     }
 
     @Test
-    fun testOnCloseCallback_whenPreferenceChanged_shouldAnimateToPinnedTaskbar() {
+    fun testOnCloseCallback_whenLauncherPreferenceChanged_shouldAnimateToPinnedTaskbar() {
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
         doNothing().whenever(pinningController).animateTaskbarPinning(any())
 
@@ -106,7 +114,7 @@
     }
 
     @Test
-    fun testOnCloseCallback_whenPreferenceChanged_shouldAnimateToTransientTaskbar() {
+    fun testOnCloseCallback_whenLauncherPreferenceChanged_shouldAnimateToTransientTaskbar() {
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true)
         doNothing().whenever(pinningController).animateTaskbarPinning(any())
 
@@ -199,4 +207,13 @@
         assertThat(pinningController.isAnimatingTaskbarPinning).isFalse()
         verify(launcherPrefs, times(1)).put(TASKBAR_PINNING, true)
     }
+
+    @Test
+    fun testRecreateTaskbarAndUpdatePinningValue_whenAnimationEnds_shouldUpdateTaskbarPinningDesktopModePref() {
+        isInDesktopMode = true
+        pinningController.recreateTaskbarAndUpdatePinningValue()
+        verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(false)
+        assertThat(pinningController.isAnimatingTaskbarPinning).isFalse()
+        verify(launcherPrefs, times(1)).put(TASKBAR_PINNING_IN_DESKTOP_MODE, true)
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 4ffb6bd..de98703 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -17,6 +17,8 @@
 
 package com.android.quickstep.util
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
 import android.view.ContextThemeWrapper
@@ -39,8 +41,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
@@ -273,6 +277,9 @@
         doNothing()
             .whenever(spySplitAnimationController)
             .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+        doReturn(-1)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
 
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
@@ -294,13 +301,45 @@
     }
 
     @Test
-    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() {
+    fun playsAppropriateSplitLaunchAnimation_playsIconFullscreenLaunchCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
+        doNothing()
+                .whenever(spySplitAnimationController)
+                .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any())
+        doReturn(0)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+                null /* launchingTaskView */,
+                mockAppPairIcon,
+                taskId,
+                taskId2,
+                null /* apps */,
+                null /* wallpapers */,
+                null /* nonApps */,
+                stateManager,
+                depthController,
+                transitionInfo,
+                transaction,
+                {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+                .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0))
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() {
         val spySplitAnimationController = spy(splitAnimationController)
         whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
         doNothing()
             .whenever(spySplitAnimationController)
-            .composeScaleUpLaunchAnimation(any(), any(), any())
-
+            .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+        doReturn(-1)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
             mockAppPairIcon,
@@ -316,7 +355,37 @@
             {} /* finishCallback */
         )
 
-        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
+        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
+                eq(WINDOWING_MODE_MULTI_WINDOW))
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarFullscreen() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+        doNothing()
+                .whenever(spySplitAnimationController)
+                .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+        doReturn(0)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
+        spySplitAnimationController.playSplitLaunchAnimation(
+                null /* launchingTaskView */,
+                mockAppPairIcon,
+                taskId,
+                taskId2,
+                null /* apps */,
+                null /* wallpapers */,
+                null /* nonApps */,
+                stateManager,
+                depthController,
+                transitionInfo,
+                transaction,
+                {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
+                eq(WINDOWING_MODE_FULLSCREEN))
     }
 
     @Test
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 98dade5..7e9e864 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -33,7 +33,6 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Point;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
@@ -115,7 +114,6 @@
     @Thunk final int[] mTempLocation = new int[2];
 
     @Thunk final Rect mTempOnDrawCellToRect = new Rect();
-    final PointF mTmpPointF = new PointF();
 
     protected GridOccupancy mOccupied;
     public GridOccupancy mTmpOccupied;
@@ -197,16 +195,11 @@
     public static final int REORDER_ANIMATION_DURATION = 150;
     @Thunk final float mReorderPreviewAnimationMagnitude;
 
-    private final ArrayList<View> mIntersectingViews = new ArrayList<>();
-    private final Rect mOccupiedRect = new Rect();
     public final int[] mDirectionVector = new int[2];
 
     ItemConfiguration mPreviousSolution = null;
-    private static final int INVALID_DIRECTION = -100;
 
     private final Rect mTempRect = new Rect();
-    private final RectF mTempRectF = new RectF();
-    private final float[] mTmpFloatArray = new float[4];
 
     private static final Paint sPaint = new Paint();
 
@@ -1163,9 +1156,6 @@
             mDragCellSpan[0] = spanX;
             mDragCellSpan[1] = spanY;
 
-            // Apply color extraction on a widget when dragging.
-            applyColorExtractionOnWidget(dragObject, mDragCell, spanX, spanY);
-
             final int oldIndex = mDragOutlineCurrent;
             mDragOutlineAnims[oldIndex].animateOut();
             mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
@@ -1186,19 +1176,6 @@
         }
     }
 
-    /** Applies the local color extraction to a dragging widget object. */
-    private void applyColorExtractionOnWidget(DropTarget.DragObject dragObject, int[] targetCell,
-            int spanX, int spanY) {
-        // Apply local extracted color if the DragView is an AppWidgetHostViewDrawable.
-        View view = dragObject.dragView.getContentView();
-        if (view instanceof LauncherAppWidgetHostView) {
-            int screenId = mCellLayoutContainer.getCellLayoutId(this);
-            cellToRect(targetCell[0], targetCell[1], spanX, spanY, mTempRect);
-
-            ((LauncherAppWidgetHostView) view).handleDrag(mTempRect, this, screenId);
-        }
-    }
-
     @SuppressLint("StringFormatMatches")
     public String getItemMoveDescription(int cellX, int cellY) {
         if (mContainerType == HOTSEAT) {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index f405b93..54aea38 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
 import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
@@ -236,7 +237,8 @@
         DisplayController.INSTANCE.get(context).setPriorityListener(
                 (displayContext, info, flags) -> {
                     if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS
-                            | CHANGE_NAVIGATION_MODE | CHANGE_TASKBAR_PINNING)) != 0) {
+                            | CHANGE_NAVIGATION_MODE | CHANGE_TASKBAR_PINNING
+                            | CHANGE_DESKTOP_MODE)) != 0) {
                         onConfigChanged(displayContext);
                     }
                 });
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7d1f43f..009d709 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1708,7 +1708,7 @@
         AbstractFloatingView.closeAllOpenViews(this);
         getStateManager().goToState(ALL_APPS, alreadyOnHome);
         if (mAppsView.isSearching()) {
-            mAppsView.reset(alreadyOnHome);
+            mAppsView.getSearchUiManager().resetSearch();
         }
         if (mAppsView.getCurrentPage() != tab) {
             mAppsView.switchToTab(tab);
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 6a5001b..13181e8 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -253,6 +253,7 @@
         @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
 
         const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
+        const val TASKBAR_PINNING_DESKTOP_MODE_KEY = "TASKBAR_PINNING_DESKTOP_MODE_KEY"
         const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
         @JvmField
         val ICON_STATE = nonRestorableItem("pref_icon_shape_path", "", EncryptionType.ENCRYPTED)
@@ -272,6 +273,9 @@
         @JvmField
         val TASKBAR_PINNING =
             backedUpItem(TASKBAR_PINNING_KEY, false, EncryptionType.DEVICE_PROTECTED)
+        @JvmField
+        val TASKBAR_PINNING_IN_DESKTOP_MODE =
+            backedUpItem(TASKBAR_PINNING_DESKTOP_MODE_KEY, true, EncryptionType.DEVICE_PROTECTED)
 
         @JvmField
         val DEVICE_TYPE =
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index beab8c1..584d089 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -126,7 +126,6 @@
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetManagerHelper;
-import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
 import com.android.launcher3.widget.util.WidgetSizes;
 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy;
@@ -1744,9 +1743,6 @@
 
         final DragView dv;
         if (contentView instanceof View) {
-            if (contentView instanceof LauncherAppWidgetHostView) {
-                mDragController.addDragListener(new AppWidgetHostViewDragListener(mLauncher));
-            }
             dv = mDragController.startDrag(
                     contentView,
                     draggableView,
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 8026d4a..2f623e2 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -499,18 +499,15 @@
      * Exits search and returns to A-Z apps list. Scroll to the private space header.
      */
     public void resetAndScrollToPrivateSpaceHeader() {
-        if (mTouchHandler != null) {
-            mTouchHandler.endFastScrolling();
-        }
-
-        // Reset the base recycler view after transitioning home.
-        updateHeaderScroll(0);
-
         // Animate to A-Z with 0 time to reset the animation with proper state management.
+        // We can't rely on `animateToSearchState` with delay inside `resetSearch` because that will
+        // conflict with following scrolling to bottom, so we need it with 0 time here.
         animateToSearchState(false, 0);
 
         MAIN_EXECUTOR.getHandler().post(() -> {
             // Reset the search bar after transitioning home.
+            // When `resetSearch` is called after `animateToSearchState` is finished, the inside
+            // `animateToSearchState` with delay is a just no-op and return early.
             mSearchUiManager.resetSearch();
             // Switch to the main tab
             switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN);
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 441bbb5..2f3c2b6 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -1205,7 +1205,7 @@
      * Creates a new instance of {@link StatsLogManager} based on provided context.
      */
     public static StatsLogManager newInstance(Context context) {
-        return Overrides.getObject(StatsLogManager.class,
-                context.getApplicationContext(), R.string.stats_log_manager_class);
+        return Overrides.getObject(
+                StatsLogManager.class, context, R.string.stats_log_manager_class);
     }
 }
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 51bc339..eea1a7d 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -51,7 +51,7 @@
 public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
 
     public static final String TAG = "StateManager";
-    // b/279059025
+    // b/279059025, b/325463989
     private static final boolean DEBUG = true;
 
     private final AnimationState mConfig = new AnimationState();
@@ -240,16 +240,8 @@
     private void goToState(
             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
         if (DEBUG) {
-            String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
-            String truncatedTrace =
-                    Arrays.stream(stackTrace.split("\\n"))
-                            .limit(5)
-                            .skip(1) // Removes the line "java.lang.Exception: tracing state
-                            // transition"
-                            .filter(traceLine -> !traceLine.contains("StateManager.goToState"))
-                            .collect(Collectors.joining("\n"));
             Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state
-                    + ", partial trace:\n" + truncatedTrace);
+                    + ", partial trace:\n" + getTrimmedStackTrace("StateManager.goToState"));
         }
 
         animated &= areAnimatorsEnabled();
@@ -336,17 +328,9 @@
     public AnimatorSet createAtomicAnimation(
             STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
         if (DEBUG) {
-            String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
-            String truncatedTrace =
-                    Arrays.stream(stackTrace.split("\\n"))
-                            .limit(5)
-                            .skip(1) // Removes the line "java.lang.Exception: tracing state
-                            // transition"
-                            .filter(traceLine -> !traceLine.contains(
-                                    "StateManager.createAtomicAnimation"))
-                            .collect(Collectors.joining("\n"));
             Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState
-                    + ", partial trace:\n" + truncatedTrace);
+                    + ", partial trace:\n" + getTrimmedStackTrace(
+                            "StateManager.createAtomicAnimation"));
         }
 
         PendingAnimation builder = new PendingAnimation(config.duration);
@@ -481,7 +465,8 @@
      */
     public void cancelAnimation() {
         if (DEBUG && mConfig.currentAnimation != null) {
-            Log.d(TAG, "cancelAnimation - with ongoing animation");
+            Log.d(TAG, "cancelAnimation - with ongoing animation"
+                    + ", partial trace:\n" + getTrimmedStackTrace("StateManager.cancelAnimation"));
         }
         mConfig.reset();
         // It could happen that a new animation is set as a result of an endListener on the
@@ -579,6 +564,15 @@
         mConfig.playbackController = null;
     }
 
+    private String getTrimmedStackTrace(String callingMethodName) {
+        String stackTrace = Log.getStackTraceString(new Exception());
+        return Arrays.stream(stackTrace.split("\\n"))
+                .skip(2) // Removes the line "java.lang.Exception" and "getTrimmedStackTrace".
+                .filter(traceLine -> !traceLine.contains(callingMethodName))
+                .limit(3)
+                .collect(Collectors.joining("\n"));
+    }
+
     private class StartAnimRunnable implements Runnable {
 
         private final AnimatorSet mAnim;
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 8806e27..92fc38f 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -23,6 +23,8 @@
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET;
 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
+import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_DESKTOP_MODE_KEY;
+import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE;
 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
@@ -88,10 +90,11 @@
     public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3;
     public static final int CHANGE_NAVIGATION_MODE = 1 << 4;
     public static final int CHANGE_TASKBAR_PINNING = 1 << 5;
+    public static final int CHANGE_DESKTOP_MODE = 1 << 6;
 
     public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
             | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE
-            | CHANGE_TASKBAR_PINNING;
+            | CHANGE_TASKBAR_PINNING | CHANGE_DESKTOP_MODE;
 
     private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
     private static final String TARGET_OVERLAY_PACKAGE = "android";
@@ -145,16 +148,22 @@
     private void attachTaskbarPinningSharedPreferenceChangeListener(Context context) {
         mTaskbarPinningPreferenceChangeListener =
                 (sharedPreferences, key) -> {
-                    if (TASKBAR_PINNING_KEY.equals(key)
-                            && mInfo.mIsTaskbarPinned != LauncherPrefs.get(mContext).get(
-                            TASKBAR_PINNING)
-                    ) {
+                    LauncherPrefs prefs = LauncherPrefs.get(mContext);
+                    boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key)
+                            && mInfo.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
+                    boolean isTaskbarPinningDesktopModeChanged =
+                            TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key)
+                                    && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get(
+                                    TASKBAR_PINNING_IN_DESKTOP_MODE);
+                    if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) {
                         handleInfoChange(mWindowContext.getDisplay());
                     }
                 };
 
         LauncherPrefs.get(context).addListener(
                 mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
+        LauncherPrefs.get(context).addListener(
+                mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING_IN_DESKTOP_MODE);
     }
 
     /**
@@ -172,6 +181,13 @@
     }
 
     /**
+     * Handles info change for desktop mode.
+     */
+    public static void handleInfoChangeForDesktopMode(Context context) {
+        INSTANCE.get(context).handleInfoChange(context.getDisplay());
+    }
+
+    /**
      * Enables transient taskbar status for tests.
      */
     @VisibleForTesting
@@ -192,6 +208,8 @@
         if (enableTaskbarPinning()) {
             LauncherPrefs.get(mContext).removeListener(
                     mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
+            LauncherPrefs.get(mContext).removeListener(
+                    mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING_IN_DESKTOP_MODE);
         }
         if (mWindowContext != null) {
             mWindowContext.unregisterComponentCallbacks(this);
@@ -309,9 +327,15 @@
             FileLog.w(TAG,
                     "(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds: " + newInfo.mPerDisplayBounds);
         }
-        if (newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned) {
+        if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned)
+                || (newInfo.mIsTaskbarPinnedInDesktopMode
+                    != oldInfo.mIsTaskbarPinnedInDesktopMode)) {
             change |= CHANGE_TASKBAR_PINNING;
         }
+        if (newInfo.mIsInDesktopMode != oldInfo.mIsInDesktopMode) {
+            change |= CHANGE_DESKTOP_MODE;
+        }
+
         if (DEBUG) {
             Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
         }
@@ -355,6 +379,9 @@
                 new ArrayMap<>();
 
         private final boolean mIsTaskbarPinned;
+        private final boolean mIsTaskbarPinnedInDesktopMode;
+
+        private final boolean mIsInDesktopMode;
 
         public Info(Context displayInfoContext) {
             /* don't need system overrides for external displays */
@@ -414,6 +441,9 @@
             }
 
             mIsTaskbarPinned = LauncherPrefs.get(displayInfoContext).get(TASKBAR_PINNING);
+            mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get(
+                    TASKBAR_PINNING_IN_DESKTOP_MODE);
+            mIsInDesktopMode = wmProxy.isInDesktopMode();
         }
 
         /**
@@ -430,10 +460,14 @@
                 return sTransientTaskbarStatusForTests;
             }
             if (enableTaskbarPinning()) {
+                if (mIsInDesktopMode) {
+                    return !mIsTaskbarPinnedInDesktopMode;
+                }
                 return !mIsTaskbarPinned;
             }
             return true;
         }
+
         /**
          * Returns whether the taskbar is pinned in gesture navigation mode.
          */
@@ -441,6 +475,10 @@
             return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar();
         }
 
+        public boolean isInDesktopMode() {
+            return mIsInDesktopMode;
+        }
+
         /**
          * Returns {@code true} if the bounds represent a tablet.
          */
@@ -501,6 +539,7 @@
         appendFlag(result, change, CHANGE_SUPPORTED_BOUNDS, "CHANGE_SUPPORTED_BOUNDS");
         appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE");
         appendFlag(result, change, CHANGE_TASKBAR_PINNING, "CHANGE_TASKBAR_VARIANT");
+        appendFlag(result, change, CHANGE_DESKTOP_MODE, "CHANGE_DESKTOP_MODE");
         return result.toString();
     }
 
@@ -516,6 +555,8 @@
         pw.println("  densityDpi=" + info.densityDpi);
         pw.println("  navigationMode=" + info.navigationMode.name());
         pw.println("  isTaskbarPinned=" + info.mIsTaskbarPinned);
+        pw.println("  isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode);
+        pw.println("  isInDesktopMode=" + info.mIsInDesktopMode);
         pw.println("  currentSize=" + info.currentSize);
         info.mPerDisplayBounds.forEach((key, value) -> pw.println(
                 "  perDisplayBounds - " + key + ": " + value));
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 4b004f3..0817c0a 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -115,6 +115,13 @@
     }
 
     /**
+     * Returns if we are in desktop mode or not.
+     */
+    public boolean isInDesktopMode() {
+        return false;
+    }
+
+    /**
      * Returns the real bounds for the provided display after applying any insets normalization
      */
     public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) {
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index c3e9ad6..44ab966 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -42,7 +42,6 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
@@ -52,8 +51,7 @@
  * {@inheritDoc}
  */
 public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView
-        implements TouchCompleteListener, View.OnLongClickListener,
-        LocalColorExtractor.Listener {
+        implements TouchCompleteListener, View.OnLongClickListener {
 
     private static final String TAG = "LauncherAppWidgetHostView";
 
@@ -70,13 +68,9 @@
 
     private static final String TRACE_METHOD_NAME = "appwidget load-widget ";
 
-    private final Rect mTempRect = new Rect();
     private final CheckLongPressHelper mLongPressHelper;
     protected final ActivityContext mActivityContext;
 
-    // Maintain the color manager.
-    private final LocalColorExtractor mColorExtractor;
-
     private boolean mIsScrollable;
     private boolean mIsAttachedToWindow;
     private boolean mIsAutoAdvanceRegistered;
@@ -84,11 +78,6 @@
 
     private long mDeferUpdatesUntilMillis = 0;
     RemoteViews mLastRemoteViews;
-    private boolean mHasDeferredColorChange = false;
-    private @Nullable SparseIntArray mDeferredColorChange = null;
-
-    // The following member variables are only used during drag-n-drop.
-    private boolean mIsInDragMode = false;
 
     private boolean mTrackingWidgetUpdate = false;
 
@@ -109,7 +98,6 @@
         if (Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
         }
-        mColorExtractor = new LocalColorExtractor(); // no-op
     }
 
     @Override
@@ -177,8 +165,8 @@
     }
 
     /**
-     * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} and
-     * colors through {@link #onColorsChanged} are currently being deferred.
+     * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} are
+     * currently being deferred.
      * @see #beginDeferringUpdates()
      */
     private boolean isDeferringUpdates() {
@@ -187,9 +175,8 @@
 
     /**
      * Begin deferring the application of any {@link RemoteViews} updates made through
-     * {@link #updateAppWidget} and color changes through {@link #onColorsChanged} until
-     * {@link #endDeferringUpdates()} has been called or the next {@link #updateAppWidget} or
-     * {@link #onColorsChanged} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed.
+     * {@link #updateAppWidget} until {@link #endDeferringUpdates()} has been called or the next
+     * {@link #updateAppWidget} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed.
      */
     public void beginDeferringUpdates() {
         mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS;
@@ -197,26 +184,16 @@
 
     /**
      * Stop deferring the application of {@link RemoteViews} updates made through
-     * {@link #updateAppWidget} and color changes made through {@link #onColorsChanged} and apply
-     * any deferred updates.
+     * {@link #updateAppWidget} and apply any deferred updates.
      */
     public void endDeferringUpdates() {
         RemoteViews remoteViews;
-        SparseIntArray deferredColors;
-        boolean hasDeferredColors;
         mDeferUpdatesUntilMillis = 0;
         remoteViews = mLastRemoteViews;
-        deferredColors = mDeferredColorChange;
-        hasDeferredColors = mHasDeferredColorChange;
-        mDeferredColorChange = null;
-        mHasDeferredColorChange = false;
 
         if (remoteViews != null) {
             updateAppWidget(remoteViews);
         }
-        if (hasDeferredColors) {
-            onColorsChanged(deferredColors);
-        }
     }
 
     public boolean onInterceptTouchEvent(MotionEvent ev) {
@@ -242,7 +219,6 @@
         super.onAttachedToWindow();
         mIsAttachedToWindow = true;
         checkIfAutoAdvance();
-        mColorExtractor.setListener(this);
     }
 
     @Override
@@ -253,7 +229,6 @@
         // state is updated. So isAttachedToWindow() will return true until next frame.
         mIsAttachedToWindow = false;
         checkIfAutoAdvance();
-        mColorExtractor.setListener(null);
     }
 
     @Override
@@ -292,29 +267,6 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         mIsScrollable = checkScrollableRecursively(this);
-
-        if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo info) {
-            mTempRect.set(left, top, right, bottom);
-            mColorExtractor.setWorkspaceLocation(mTempRect, (View) getParent(), info.screenId);
-        }
-    }
-
-    /** Starts the drag mode. */
-    public void startDrag() {
-        mIsInDragMode = true;
-    }
-
-    /** Handles a drag event occurred on a workspace page corresponding to the {@code screenId}. */
-    public void handleDrag(Rect rectInView, View view, int screenId) {
-        if (mIsInDragMode) {
-            mColorExtractor.setWorkspaceLocation(rectInView, view, screenId);
-        }
-    }
-
-    /** Ends the drag mode. */
-    public void endDrag() {
-        mIsInDragMode = false;
-        requestLayout();
     }
 
     /**
@@ -338,20 +290,6 @@
     }
 
     @Override
-    public void onColorsChanged(SparseIntArray colors) {
-        if (isDeferringUpdates()) {
-            mDeferredColorChange = colors;
-            mHasDeferredColorChange = true;
-            return;
-        }
-        mDeferredColorChange = null;
-        mHasDeferredColorChange = false;
-
-        // setColorResources will reapply the view, which must happen in the UI thread.
-        post(() -> setColorResources(colors));
-    }
-
-    @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         info.setClassName(getClass().getName());
diff --git a/src/com/android/launcher3/widget/LocalColorExtractor.java b/src/com/android/launcher3/widget/LocalColorExtractor.java
index 96e7531..7b500c7 100644
--- a/src/com/android/launcher3/widget/LocalColorExtractor.java
+++ b/src/com/android/launcher3/widget/LocalColorExtractor.java
@@ -17,11 +17,8 @@
 package com.android.launcher3.widget;
 
 import android.app.WallpaperColors;
-import android.appwidget.AppWidgetHostView;
 import android.content.Context;
-import android.graphics.Rect;
 import android.util.SparseIntArray;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 
@@ -31,18 +28,6 @@
 /** Extracts the colors we need from the wallpaper at given locations. */
 public class LocalColorExtractor implements ResourceBasedOverride {
 
-    /** Listener for color changes on a screen location. */
-    public interface Listener {
-        /**
-         * Method called when the colors on a registered location has changed.
-         *
-         * {@code extractedColors} maps the color resources {@code android.R.colors.system_*} to
-         * their value, in a format that can be passed directly to
-         * {@link AppWidgetHostView#setColorResources(SparseIntArray)}.
-         */
-        void onColorsChanged(SparseIntArray extractedColors);
-    }
-
     /**
      * Creates a new instance of LocalColorExtractor
      */
@@ -51,19 +36,6 @@
                 R.string.local_colors_extraction_class);
     }
 
-    /** Sets the object that will receive the color changes. */
-    public void setListener(@Nullable Listener listener) {
-        // no-op
-    }
-
-    /**
-     * Sets the location used for color extraction
-     * @param pos position to use for color extraction
-     * @param child view whose coordinate space is used for {@code pos}
-     * @param screenId the workspace screenId
-     */
-    public void setWorkspaceLocation(Rect pos, View child, int screenId) { }
-
     /**
      * Updates the base context to contain the colors override
      */
@@ -76,5 +48,4 @@
     public SparseIntArray generateColorsOverride(WallpaperColors colors) {
         return null;
     }
-
 }
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index faad307..8857774 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.RoundDrawableWrapper;
-import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
 
 /**
  * Extension of {@link DragPreviewProvider} with logic specific to pending widgets/shortcuts
@@ -132,8 +131,6 @@
             }
             if (mAppWidgetHostViewPreview != null) {
                 previewSizeBeforeScale[0] = mAppWidgetHostViewPreview.getMeasuredWidth();
-                launcher.getDragController()
-                        .addDragListener(new AppWidgetHostViewDragListener(launcher));
             }
             if (preview == null && mAppWidgetHostViewPreview == null) {
                 Drawable p = new FastBitmapDrawable(new DatabaseWidgetPreviewLoader(launcher)
diff --git a/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java b/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java
deleted file mode 100644
index 3e54b33..0000000
--- a/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.widget.dragndrop;
-
-import com.android.launcher3.DropTarget;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.dragndrop.DragController;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
-
-/** A drag listener of {@link LauncherAppWidgetHostView}. */
-public final class AppWidgetHostViewDragListener implements DragController.DragListener {
-    private final Launcher mLauncher;
-    private LauncherAppWidgetHostView mAppWidgetHostView;
-
-    public AppWidgetHostViewDragListener(Launcher launcher) {
-        mLauncher = launcher;
-    }
-
-    @Override
-    public void onDragStart(DropTarget.DragObject dragObject, DragOptions unused) {
-        if (dragObject.dragView.getContentView() instanceof LauncherAppWidgetHostView) {
-            mAppWidgetHostView = (LauncherAppWidgetHostView) dragObject.dragView.getContentView();
-            mAppWidgetHostView.startDrag();
-        } else {
-            mLauncher.getDragController().removeDragListener(this);
-        }
-    }
-
-    @Override
-    public void onDragEnd() {
-        mAppWidgetHostView.endDrag();
-        mLauncher.getDragController().removeDragListener(this);
-    }
-}
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
index 29c34be..270a610 100644
--- a/tests/Launcher3Tests.xml
+++ b/tests/Launcher3Tests.xml
@@ -44,6 +44,8 @@
         <option name="run-command" value="settings put global airplane_mode_on 1" />
         <option name="run-command" value="am broadcast -a android.intent.action.AIRPLANE_MODE" />
 
+        <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
+
         <option name="run-command" value="settings put system pointer_location 1" />
         <option name="run-command" value="settings put system show_touches 1" />
     </target_preparer>
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index aefc2db..e378733 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -306,6 +306,7 @@
         context.putObject(LauncherPrefs.INSTANCE, launcherPrefs)
 
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
+        whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
         val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
diff --git a/tests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 2e57ad5..273f0c4 100644
--- a/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -31,6 +31,7 @@
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE
 import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
 import com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING
@@ -94,6 +95,7 @@
         whenever(context.getObject(eq(WindowManagerProxy.INSTANCE))).thenReturn(windowManagerProxy)
         whenever(context.getObject(eq(LauncherPrefs.INSTANCE))).thenReturn(launcherPrefs)
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+        whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
 
         // Mock WindowManagerProxy
         val displayInfo = CachedDisplayInfo(Point(width, height), Surface.ROTATION_0)
@@ -170,4 +172,13 @@
         verify(displayInfoChangeListener)
             .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
     }
+
+    @Test
+    @UiThreadTest
+    fun testTaskbarPinningChangeInDesktopMode() {
+        whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
+        displayController.handleInfoChange(display)
+        verify(displayInfoChangeListener)
+            .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+    }
 }