Merge "Improve taskId logging for Overview" into main
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/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 1a94424..1d772b5 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,7 +298,7 @@
                 new VoiceInteractionWindowController(this),
                 new TaskbarTranslationController(this),
                 new TaskbarSpringOnStashController(this),
-                createTaskbarRecentAppsController(isPcMode),
+                createTaskbarRecentAppsController(),
                 TaskbarEduTooltipController.newInstance(this),
                 new KeyboardQuickSwitchController(),
                 new TaskbarPinningController(this),
@@ -314,16 +307,14 @@
         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..30954ed 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;
@@ -426,9 +425,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/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/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/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/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/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/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/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index c100687..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) {
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 9a4434a..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;
 
 /**
@@ -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 cc5ce01..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) {
@@ -1726,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
@@ -1741,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.
@@ -1825,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);
@@ -1836,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 {
@@ -1859,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);
                 }
@@ -1869,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));
@@ -1969,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);
@@ -2349,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);
@@ -2504,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();
@@ -2742,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;
     }
 
     /**
@@ -4280,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);
@@ -4760,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);
@@ -4781,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();
@@ -5239,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,
@@ -5887,8 +5886,7 @@
         }
 
         taskView.setShowScreenshot(true);
-        for (TaskIdAttributeContainer container :
-                taskView.getTaskIdAttributeContainers()) {
+        for (TaskIdAttributeContainer container : taskView.getTaskIdAttributeContainers()) {
             if (container == null) {
                 continue;
             }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index f766f70..43965b2 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);
@@ -674,10 +673,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 +695,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 +753,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 +834,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");
@@ -873,10 +855,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(),
@@ -898,6 +883,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
@@ -905,7 +891,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()
@@ -1096,6 +1082,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) {
@@ -1104,6 +1091,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) {
@@ -1192,29 +1180,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());
         }
     }
 
@@ -1668,9 +1661,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));
@@ -1706,9 +1696,6 @@
         }
 
         for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
-            if (taskContainer == null) {
-                continue;
-            }
             for (SystemShortcut s : getEnabledShortcuts(this,
                     taskContainer)) {
                 if (s.hasHandlerForAction(action)) {
@@ -1907,13 +1894,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
      */
@@ -1970,7 +1957,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/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/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/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);