Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 4ff976d..bc49146 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -536,4 +536,11 @@
     namespace: "launcher"
     description: "Add options to pin/unpin to taskbar to app context menus."
     bug: "375648361"
+}
+
+flag {
+  name: "enable_launcher_icon_shapes"
+  namespace: "launcher"
+  description: "Enable launcher icon shape customizations"
+  bug: "348708061"
 }
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index f3c9467..e8c8505 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -35,7 +35,6 @@
     <string name="launcher_restore_event_logger_class" translatable="false">com.android.quickstep.LauncherRestoreEventLoggerImpl</string>
     <string name="taskbar_edu_tooltip_controller_class" translatable="false">com.android.launcher3.taskbar.TaskbarEduTooltipController</string>
     <string name="nav_handle_long_press_handler_class" translatable="false"></string>
-    <string name="contextual_search_invoker_class" translatable="false"></string>
     <string name="contextual_search_state_manager_class" translatable="false"></string>
 
     <!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 7d193aa..2759816 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1567,7 +1567,8 @@
 
     private boolean isFreeformAnimation(RemoteAnimationTarget[] appTargets) {
         return DesktopModeStatus.canEnterDesktopMode(mLauncher.getApplicationContext())
-                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()
+                && (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()
+                    || DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX.isTrue())
                 && Arrays.stream(appTargets)
                         .anyMatch(app -> app.taskInfo != null && app.taskInfo.isFreeform());
     }
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index e32bcd1..645bef6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -67,7 +67,8 @@
 
     private fun shouldRegisterTransitions(): Boolean =
         DesktopModeStatus.canEnterDesktopMode(context) &&
-            DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue
+            (DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue ||
+                DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue)
 
     companion object {
         private fun buildAppLaunchFilter(): TransitionFilter {
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index c5be13d..f0129b4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -18,8 +18,8 @@
 import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
 import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_APP_DURATION;
-import static com.android.launcher3.QuickstepTransitionManager.getTaskbarToHomeDuration;
 import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.getTaskbarToHomeDuration;
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
 import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
@@ -275,7 +275,8 @@
         if (mControllers.bubbleControllers.isEmpty() || hotseat == null) return;
         boolean hiddenForBubbles =
                 mControllers.bubbleControllers.get().bubbleBarViewController.isHiddenForNoBubbles();
-        hotseat.post(() -> adjustHotseatForBubbleBar(!hiddenForBubbles));
+        if (hiddenForBubbles) return;
+        hotseat.post(() -> adjustHotseatForBubbleBar(/* isBubbleBarVisible= */ true));
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index f346e19..cb4e5e2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -1314,7 +1314,8 @@
             // If the task bar is not start aligned, the navigation bar is located in the center
             // between the taskbar and screen edges, depending on the bubble bar location.
             float navbarWidth = mNavButtonContainer.getWidth();
-            Rect taskbarBounds = mControllers.taskbarViewController.getIconLayoutBounds();
+            Rect taskbarBounds = mControllers.taskbarViewController
+                    .getTransientTaskbarIconLayoutBoundsInParent();
             if (isNavbarOnRight) {
                 if (mNavButtonsView.isLayoutRtl()) {
                     float taskBarEnd = taskbarBounds.right;
@@ -1334,8 +1335,10 @@
     public void onLayoutsUpdated() {
         // no need to do anything if on phone, or if taskbar or navbar views were not placed on
         // screen.
+        Rect transientTaskbarIconLayoutBoundsInParent = mControllers.taskbarViewController
+                .getTransientTaskbarIconLayoutBoundsInParent();
         if (mContext.getDeviceProfile().isPhone
-                || mControllers.taskbarViewController.getIconLayoutBounds().isEmpty()
+                || transientTaskbarIconLayoutBoundsInParent.isEmpty()
                 || mNavButtonsView.getWidth() == 0) {
             return;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index eb47bb0..b6b090c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -212,7 +212,8 @@
      * morphs into the size of where the taskbar icons will be.
      */
     public Animator createRevealAnimToIsStashed(boolean isStashed) {
-        Rect visualBounds = mControllers.taskbarViewController.getIconLayoutVisualBounds();
+        Rect visualBounds = mControllers.taskbarViewController
+                .getTransientTaskbarIconLayoutBounds();
         float startRadius = mStashedHandleRadius;
 
         if (DisplayController.isTransientTaskbar(mActivity)) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 8149f81..0f639f9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -301,7 +301,8 @@
             BubbleStashController bubbleStashController = isTransientTaskbar
                     ? new TransientBubbleStashController(dimensionsProvider, this)
                     : new PersistentBubbleStashController(dimensionsProvider);
-            bubbleStashController.setHotseatVerticalCenter(launcherDp.getHotseatVerticalCenter());
+            bubbleStashController.setBubbleBarVerticalCenterForHome(
+                    launcherDp.getBubbleBarVerticalCenterForHome());
             bubbleControllersOptional = Optional.of(new BubbleControllers(
                     new BubbleBarController(this, bubbleBarView),
                     new BubbleBarViewController(this, bubbleBarView, bubbleBarContainer),
@@ -369,8 +370,9 @@
         applyDeviceProfile(launcherDp);
         mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp);
         mControllers.bubbleControllers.ifPresent(bubbleControllers -> {
-            int hotseatVertCenter = launcherDp.getHotseatVerticalCenter();
-            bubbleControllers.bubbleStashController.setHotseatVerticalCenter(hotseatVertCenter);
+            int bubbleBarVerticalCenter = launcherDp.getBubbleBarVerticalCenterForHome();
+            bubbleControllers.bubbleStashController
+                    .setBubbleBarVerticalCenterForHome(bubbleBarVerticalCenter);
         });
         AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
         // Reapply fullscreen to take potential new screen size into account.
@@ -867,7 +869,8 @@
     }
 
     private ActivityOptionsWrapper getActivityLaunchDesktopOptions(ItemInfo info) {
-        if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue()) {
+        if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue()
+                && !DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue()) {
             return null;
         }
         if (!areDesktopTasksVisible()) {
@@ -1136,6 +1139,10 @@
             return getSetupWindowSize();
         }
 
+        int bubbleBarTop = mControllers.bubbleControllers.map(bubbleControllers ->
+                bubbleControllers.bubbleBarViewController.getBubbleBarWithFlyoutMaximumHeight()
+        ).orElse(0);
+        int taskbarWindowSize;
         boolean shouldTreatAsTransient = DisplayController.isTransientTaskbar(this)
                 || (enableTaskbarPinning() && !isThreeButtonNav());
 
@@ -1152,16 +1159,18 @@
             DeviceProfile transientTaskbarDp = mDeviceProfile.toBuilder(this)
                     .setIsTransientTaskbar(true).build();
 
-            return transientTaskbarDp.taskbarHeight
+            taskbarWindowSize = transientTaskbarDp.taskbarHeight
                     + (2 * transientTaskbarDp.taskbarBottomMargin)
                     + Math.max(extraHeightForTaskbarTooltips, resources.getDimensionPixelSize(
                     R.dimen.transient_taskbar_shadow_blur));
+            return Math.max(taskbarWindowSize, bubbleBarTop);
         }
 
 
-        return mDeviceProfile.taskbarHeight
+        taskbarWindowSize =  mDeviceProfile.taskbarHeight
                 + getCornerRadius()
                 + extraHeightForTaskbarTooltips;
+        return Math.max(taskbarWindowSize, bubbleBarTop);
     }
 
     public int getSetupWindowSize() {
@@ -1429,7 +1438,9 @@
         BubbleTextView.RunningAppState runningAppState =
                 mControllers.taskbarRecentAppsController.getRunningAppState(taskId);
         return runningAppState == BubbleTextView.RunningAppState.MINIMIZED
-                && DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS.isTrue();
+                && (DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS.isTrue()
+                    || DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX.isTrue()
+                    );
     }
 
     private RemoteTransition createUnminimizeRemoteTransition() {
@@ -1634,15 +1645,6 @@
         mControllers.taskbarEduTooltipController.hide();
     }
 
-    /**
-     * Called when we want to open bubblebar when user performs swipes up gesture.
-     */
-    public void onSwipeToOpenBubblebar() {
-        mControllers.bubbleControllers.ifPresent(controllers -> {
-            controllers.bubbleStashController.showBubbleBar(/* expandBubbles= */ true);
-        });
-    }
-
     /** Returns {@code true} if Taskbar All Apps is open. */
     public boolean isTaskbarAllAppsOpen() {
         return mControllers.taskbarAllAppsController.isOpen();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index db70724..826722d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -227,7 +227,9 @@
                     bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
             boolean hiddenForBubbles =
                     bubbleControllers.bubbleBarViewController.isHiddenForNoBubbles();
-            uiController.adjustHotseatForBubbleBar(!hiddenForBubbles);
+            if (!hiddenForBubbles) {
+                uiController.adjustHotseatForBubbleBar(/* isBubbleBarVisible= */ true);
+            }
             uiController.onBubbleBarLocationUpdated(location);
         }, () -> uiController.onBubbleBarLocationUpdated(null));
         // Notify that the ui controller has changed
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index a89bc3a..26a552e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -83,7 +83,7 @@
     protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
     open val shouldShowSearchEdu: Boolean
         get() =
-            ContextualSearchInvoker.newInstance(activityContext)
+            ContextualSearchInvoker(activityContext)
                 .runContextualSearchInvocationChecksAndLogFailures()
 
     private val isTooltipEnabled: Boolean
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 4498fea..ff8e4a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -261,7 +261,7 @@
                 SystemUiProxy.INSTANCE.get(mContext),
                 ContextualEduStatsManager.INSTANCE.get(mContext),
                 new Handler(),
-                ContextualSearchInvoker.newInstance(mContext));
+                new ContextualSearchInvoker(mContext));
         mDefaultComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index ad1a44d..130b9b7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -764,7 +764,7 @@
         ) {
             return 0;
         }
-        Rect iconsBounds = getIconLayoutBounds();
+        Rect iconsBounds = getTransientTaskbarIconLayoutBoundsInParent();
         return getTaskBarIconsEndForBubbleBarLocation(location) - iconsBounds.right;
     }
 
@@ -892,26 +892,46 @@
     }
 
     /**
-     * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
+     * Returns whether the given MotionEvent, *in screen coordinates*, is within any Taskbar item's
      * touch bounds.
      */
     public boolean isEventOverAnyItem(MotionEvent ev) {
         getLocationOnScreen(mTempOutLocation);
-        int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0];
-        int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1];
-        return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
+        int xInOurCoordinates = (int) ev.getRawX() - mTempOutLocation[0];
+        int yInOurCoordinates = (int) ev.getRawY() - mTempOutLocation[1];
+        return isShown() && getTaskbarIconsActualBounds().contains(xInOurCoordinates,
+                yInOurCoordinates);
+    }
+
+    /**
+     * Returns the current visual taskbar icons bounds (unlike `mIconLayoutBounds` which contains
+     * bounds for transient mode only).
+     */
+    private Rect getTaskbarIconsActualBounds() {
+        View[] iconViews = getIconViews();
+        if (iconViews.length == 0) {
+            return new Rect();
+        }
+
+        int[] firstIconViewLocation = new int[2];
+        int[] lastIconViewLocation = new int[2];
+        iconViews[0].getLocationOnScreen(firstIconViewLocation);
+        iconViews[iconViews.length - 1].getLocationOnScreen(lastIconViewLocation);
+
+        return new Rect(firstIconViewLocation[0], 0, lastIconViewLocation[0] + mIconTouchSize,
+                getHeight());
     }
 
     /**
      * Gets visual bounds of the taskbar view. The visual bounds correspond to the taskbar touch
      * area, rather than layout placement in the parent view.
      */
-    public Rect getIconLayoutVisualBounds() {
+    public Rect getTransientTaskbarIconLayoutBounds() {
         return new Rect(mIconLayoutBounds);
     }
 
     /** Gets taskbar layout bounds in parent view. */
-    public Rect getIconLayoutBounds() {
+    public Rect getTransientTaskbarIconLayoutBoundsInParent() {
         Rect actualBounds = new Rect(mIconLayoutBounds);
         actualBounds.top = getTop();
         actualBounds.bottom = getBottom();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 604d32d..4d77ab2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -203,6 +203,10 @@
     private class TaskbarViewGestureListener extends GestureDetector.SimpleOnGestureListener {
         @Override
         public boolean onDown(@NonNull MotionEvent event) {
+            if (event.isFromSource(InputDevice.SOURCE_MOUSE)
+                    && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
+                maybeShowPinningView(event);
+            }
             return true;
         }
 
@@ -212,11 +216,16 @@
         }
 
         @Override
-        public void onLongPress(MotionEvent event) {
-            if (DisplayController.isPinnedTaskbar(mActivity)) {
-                mControllers.taskbarPinningController.showPinningView(mTaskbarView,
-                        event.getRawX());
+        public void onLongPress(@NonNull MotionEvent event) {
+            maybeShowPinningView(event);
+        }
+
+        private void maybeShowPinningView(@NonNull MotionEvent event) {
+            if (!DisplayController.isPinnedTaskbar(mActivity) || mTaskbarView.isEventOverAnyItem(
+                    event)) {
+                return;
             }
+            mControllers.taskbarPinningController.showPinningView(mTaskbarView, event.getRawX());
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt
index 704d6cf..17da533 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt
@@ -38,7 +38,7 @@
                 super.triggerAllAppsButtonLongClick()
 
                 val contextualSearchInvoked =
-                    ContextualSearchInvoker.newInstance(activity).show(ENTRYPOINT_LONG_PRESS_META)
+                    ContextualSearchInvoker(activity).show(ENTRYPOINT_LONG_PRESS_META)
                 if (contextualSearchInvoked) {
                     val runningPackage =
                         TopTaskTracker.INSTANCE[activity].getCachedTopTask(
@@ -59,8 +59,7 @@
     }
 
     open fun longPressAllAppsToStartContextualSearch(context: Context): Boolean =
-        ContextualSearchInvoker.newInstance(context)
-            .runContextualSearchInvocationChecksAndLogFailures()
+        ContextualSearchInvoker(context).runContextualSearchInvocationChecksAndLogFailures()
 
     companion object {
         @JvmStatic
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index bc5f9a3..4acf2fe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -356,12 +356,12 @@
         return mTaskbarView.getMaxNumIconViews();
     }
 
-    public Rect getIconLayoutVisualBounds() {
-        return mTaskbarView.getIconLayoutVisualBounds();
+    public Rect getTransientTaskbarIconLayoutBounds() {
+        return mTaskbarView.getTransientTaskbarIconLayoutBounds();
     }
 
-    public Rect getIconLayoutBounds() {
-        return mTaskbarView.getIconLayoutBounds();
+    public Rect getTransientTaskbarIconLayoutBoundsInParent() {
+        return mTaskbarView.getTransientTaskbarIconLayoutBoundsInParent();
     }
 
     public View[] getIconViews() {
@@ -559,14 +559,14 @@
         if (mControllers.getSharedState().startTaskbarVariantIsTransient) {
             float transY =
                     mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight
-                            - mTaskbarView.getIconLayoutVisualBounds().bottom)
+                            - mTaskbarView.getTransientTaskbarIconLayoutBounds().bottom)
                             - (mPersistentTaskbarDp.taskbarHeight
                                     - mTransientTaskbarDp.taskbarIconSize) / 2f;
             taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY);
         } else {
             float transY =
                     -mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight
-                            - mTaskbarView.getIconLayoutVisualBounds().bottom)
+                            - mTaskbarView.getTransientTaskbarIconLayoutBounds().bottom)
                             - (mTransientTaskbarDp.taskbarHeight
                                     - mTransientTaskbarDp.taskbarIconSize) / 2f;
             taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarParentViewHeightUpdateNotifier.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarParentViewHeightUpdateNotifier.kt
new file mode 100644
index 0000000..f69ad74
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarParentViewHeightUpdateNotifier.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles
+
+/** Controls the parent view height. */
+interface BubbleBarParentViewHeightUpdateNotifier {
+
+    /** Notify parent that top boundary should be updated. */
+    fun updateTopBoundary()
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
index 2d3642b..4b8924c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
@@ -91,7 +91,7 @@
         when {
             canUnstash() && swipeState.passedUnstash -> {
                 swipeState.currentState = COLLAPSED
-                bubbleStashController.showBubbleBar(expandBubbles = false)
+                bubbleStashController.showBubbleBar(expandBubbles = false, bubbleBarGesture = true)
             }
             canStash() && !swipeState.passedUnstash -> {
                 swipeState.currentState = STASHED
@@ -103,7 +103,7 @@
     /** Finish tracking swipe gesture. Animate views back to resting state */
     fun finish() {
         if (swipeState.passedUnstash && swipeState.startState in setOf(STASHED, COLLAPSED)) {
-            bubbleStashController.showBubbleBar(expandBubbles = true)
+            bubbleStashController.showBubbleBar(expandBubbles = true, bubbleBarGesture = true)
         }
         if (animatedSwipeTranslation.value == 0f) {
             reset()
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 833be61..219a2b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -381,6 +381,7 @@
         super.onInitializeAccessibilityNodeInfoInternal(info);
         // Always show only expand action as the menu is only for collapsed bubble bar
         info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
         info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all,
                 getResources().getString(R.string.bubble_bar_action_dismiss_all)));
         if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
@@ -395,10 +396,8 @@
     @Override
     public boolean performAccessibilityActionInternal(int action,
             @androidx.annotation.Nullable Bundle arguments) {
-        if (super.performAccessibilityActionInternal(action, arguments)) {
-            return true;
-        }
-        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND
+                || action == AccessibilityNodeInfo.ACTION_CLICK) {
             mController.expandBubbleBar();
             return true;
         }
@@ -416,7 +415,7 @@
                     BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR);
             return true;
         }
-        return false;
+        return super.performAccessibilityActionInternal(action, arguments);
     }
 
     @SuppressLint("RtlHardcoded")
@@ -1307,6 +1306,10 @@
         return getBubbleBarCollapsedHeight() + mPointerSize;
     }
 
+    float getArrowHeight() {
+        return mPointerSize;
+    }
+
     float getBubbleBarCollapsedHeight() {
         // the pointer is invisible when collapsed
         return getScaledIconSize() + 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 fd08078..1e0a778 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -95,7 +95,6 @@
     private TaskbarInsetsController mTaskbarInsetsController;
     private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider;
     private View.OnClickListener mBubbleClickListener;
-    private View.OnClickListener mBubbleBarClickListener;
     private BubbleView.Controller mBubbleViewController;
     private BubbleBarOverflow mOverflowBubble;
 
@@ -171,20 +170,18 @@
                 mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
         mBubbleBarViewAnimator = new BubbleBarViewAnimator(
                 mBarView, mBubbleStashController, mBubbleBarFlyoutController,
-                mBubbleBarController::showExpandedView);
+                createBubbleBarParentViewController(), mBubbleBarController::showExpandedView);
         mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
         onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
                 dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
         mBubbleBarScaleY.updateValue(1f);
         mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
-        mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
         mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
         if (!Flags.enableOptionalBubbleOverflow()) {
             showOverflow(true);
         }
-        mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
@@ -211,7 +208,8 @@
 
             @Override
             public void expandBubbleBar() {
-                BubbleBarViewController.this.expandBubbleBar();
+                BubbleBarViewController.this.setExpanded(
+                        /* isExpanded= */ true, /* maybeShowEdu*/ true);
             }
 
             @Override
@@ -319,20 +317,18 @@
     private FlyoutCallbacks createFlyoutCallbacks() {
         return new FlyoutCallbacks() {
             @Override
-            public void extendTopBoundary(int space) {
-                int defaultSize = mActivity.getDefaultTaskbarWindowSize();
-                mActivity.setTaskbarWindowSize(defaultSize + space);
-            }
-
-            @Override
-            public void resetTopBoundary() {
-                mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize());
-            }
-
-            @Override
             public void flyoutClicked() {
                 interruptAnimationForTouch();
-                expandBubbleBar();
+                setExpanded(/* isExpanded= */ true, /* maybeShowEdu*/ true);
+            }
+        };
+    }
+
+    private BubbleBarParentViewHeightUpdateNotifier createBubbleBarParentViewController() {
+        return new BubbleBarParentViewHeightUpdateNotifier() {
+            @Override
+            public void updateTopBoundary() {
+                mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize());
             }
         };
     }
@@ -360,25 +356,6 @@
         mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
     }
 
-    private void expandBubbleBar() {
-        if (mShouldShowEducation) {
-            mShouldShowEducation = false;
-            // Get the bubble bar bounds on screen
-            Rect bounds = new Rect();
-            mBarView.getBoundsOnScreen(bounds);
-            // Calculate user education reference position in Screen coordinates
-            Point position = new Point(bounds.centerX(), bounds.top);
-            // Show user education relative to the reference point
-            mSystemUiProxy.showUserEducation(position);
-        } else {
-            // ensure that the bubble bar has the correct translation. we may have just interrupted
-            // the animation by touching the bubble bar.
-            mBubbleBarTranslationY.animateToValue(mBubbleStashController.getBubbleBarTranslationY())
-                    .start();
-            setExpanded(true);
-        }
-    }
-
     private void collapseBubbleBar() {
         setExpanded(false);
         mBubbleStashController.stashBubbleBar();
@@ -391,6 +368,22 @@
         }
     }
 
+    /** Shows the education view if it was previously requested. */
+    private boolean maybeShowEduView() {
+        if (mShouldShowEducation) {
+            mShouldShowEducation = false;
+            // Get the bubble bar bounds on screen
+            Rect bounds = new Rect();
+            mBarView.getBoundsOnScreen(bounds);
+            // Calculate user education reference position in Screen coordinates
+            Point position = new Point(bounds.centerX(), bounds.top);
+            // Show user education relative to the reference point
+            mSystemUiProxy.showUserEducation(position);
+            return true;
+        }
+        return false;
+    }
+
     /** Notifies that the IME became visible. */
     public void onImeVisible() {
         if (isAnimatingNewBubble()) {
@@ -447,6 +440,11 @@
         return mBarView.getBubbleBarCollapsedHeight();
     }
 
+    /** Returns the bubble bar arrow height.*/
+    public float getBubbleBarArrowHeight() {
+        return mBarView.getArrowHeight();
+    }
+
     /**
      * @see BubbleBarView#getRelativePivotX()
      */
@@ -580,6 +578,19 @@
         return mHiddenForNoBubbles;
     }
 
+    /** Returns maximum height of the bubble bar with the flyout view. */
+    public int getBubbleBarWithFlyoutMaximumHeight() {
+        if (!isBubbleBarVisible()) return 0;
+        int bubbleBarTopOnHome = (int) (mBubbleStashController.getBubbleBarVerticalCenterForHome()
+                + mBarView.getBubbleBarCollapsedHeight() / 2);
+        int result = (int) (bubbleBarTopOnHome + mBarView.getArrowHeight());
+        if (isAnimatingNewBubble()) {
+            // when animating new bubble add the maximum height of the flyout view
+            result += mBubbleBarFlyoutController.getMaximumFlyoutHeight();
+        }
+        return result;
+    }
+
     /**
      * Sets whether the bubble bar should be hidden because there are no bubbles.
      */
@@ -946,13 +957,25 @@
         mBarView.setSelectedBubble(newlySelected.getView());
     }
 
+    /** @see #setExpanded(boolean, boolean) */
+    public void setExpanded(boolean isExpanded) {
+        setExpanded(isExpanded, /* maybeShowEdu= */ false);
+    }
+
     /**
      * Sets whether the bubble bar should be expanded (not unstashed, but have the contents
      * within it expanded). This method notifies SystemUI that the bubble bar is expanded and
      * showing a selected bubble. This method should ONLY be called from UI events originating
      * from Launcher.
+     *
+     * @param isExpanded whether the bar should be expanded
+     * @param maybeShowEdu whether we should show the edu view before expanding
      */
-    public void setExpanded(boolean isExpanded) {
+    public void setExpanded(boolean isExpanded, boolean maybeShowEdu) {
+        // if we're trying to expand try showing the edu view instead
+        if (maybeShowEdu && isExpanded && !mBarView.isExpanded() && maybeShowEduView()) {
+            return;
+        }
         if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
             adjustTaskbarAndHotseatToBubbleBarState(isExpanded);
@@ -1012,7 +1035,11 @@
         }
     }
 
-    /** Marks as should show education. */
+    /**
+     * Stores a request to show the education view for later processing when appropriate.
+     *
+     * @see #maybeShowEduView()
+     */
     public void prepareToShowEducation() {
         mShouldShowEducation = true;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index cb592e6..68917ff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -102,7 +102,8 @@
                 new TaskbarViewPropertiesProvider() {
                     @Override
                     public Rect getTaskbarViewBounds() {
-                        return taskbarControllers.taskbarViewController.getIconLayoutBounds();
+                        return taskbarControllers.taskbarViewController
+                                .getTransientTaskbarIconLayoutBoundsInParent();
                     }
 
                     @Override
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 447dad1..f5a6655 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -25,6 +25,7 @@
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
+import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleView
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
@@ -39,6 +40,7 @@
     private val bubbleBarView: BubbleBarView,
     private val bubbleStashController: BubbleStashController,
     private val bubbleBarFlyoutController: BubbleBarFlyoutController,
+    private val bubbleBarParentViewHeightUpdateNotifier: BubbleBarParentViewHeightUpdateNotifier,
     private val onExpanded: Runnable,
     private val scheduler: Scheduler = HandlerScheduler(bubbleBarView),
 ) {
@@ -342,7 +344,7 @@
                 scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity))
                 return@addEndListener
             }
-            animatingBubble = null
+            clearAnimatingBubble()
             if (!canceled) bubbleStashController.stashBubbleBarImmediate()
             bubbleBarView.relativePivotY = 1f
             bubbleBarView.scaleY = 1f
@@ -378,7 +380,7 @@
                     moveToState(AnimatingBubble.State.ANIMATING_OUT)
                     bubbleBarFlyoutController.collapseFlyout {
                         onFlyoutRemoved()
-                        animatingBubble = null
+                        clearAnimatingBubble()
                     }
                     bubbleStashController.showBubbleBarImmediate()
                     bubbleStashController.updateTaskbarTouchRegion()
@@ -437,7 +439,7 @@
             moveToState(AnimatingBubble.State.ANIMATING_OUT)
             bubbleBarFlyoutController.collapseFlyout {
                 onFlyoutRemoved()
-                animatingBubble = null
+                clearAnimatingBubble()
             }
             bubbleStashController.showBubbleBarImmediate()
             bubbleStashController.updateTaskbarTouchRegion()
@@ -515,7 +517,7 @@
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
         bubbleBarView.relativePivotY = 1f
-        animatingBubble = null
+        clearAnimatingBubble()
     }
 
     /** Notifies the animator that the taskbar area was touched during an animation. */
@@ -523,7 +525,7 @@
         cancelFlyout()
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
-        animatingBubble = null
+        clearAnimatingBubble()
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.onNewBubbleAnimationInterrupted(
@@ -672,7 +674,7 @@
     private fun cancelHideAnimation() {
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
-        animatingBubble = null
+        clearAnimatingBubble()
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.showBubbleBarImmediate()
     }
@@ -700,6 +702,14 @@
     private fun moveToState(state: AnimatingBubble.State) {
         val animatingBubble = this.animatingBubble ?: return
         this.animatingBubble = animatingBubble.copy(state = state)
+        if (state == AnimatingBubble.State.ANIMATING_IN) {
+            bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary()
+        }
+    }
+
+    private fun clearAnimatingBubble() {
+        animatingBubble = null
+        bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary()
     }
 
     private fun expandBubbleBar() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index 908e97c..63db012 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -35,6 +35,8 @@
     private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container),
 ) {
 
+    val maximumFlyoutHeight: Int = BubbleBarFlyoutView.getMaximumViewHeight(container.context)
+
     private companion object {
         const val EXPAND_ANIMATION_DURATION_MS = 400L
         const val COLLAPSE_ANIMATION_DURATION_MS = 350L
@@ -61,6 +63,8 @@
             return rect
         }
 
+    fun getFlyoutMaxHeight(): Int = BubbleBarFlyoutView.getMaximumViewHeight(container.context)
+
     fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onInit: () -> Unit, onEnd: () -> Unit) {
         flyout?.let(container::removeView)
         val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
@@ -102,11 +106,10 @@
                 }
         }
         animator.addListener(
-            onStart = { extendTopBoundary() },
             onEnd = {
                 endAction()
                 flyout.setOnClickListener { callbacks.flyoutClicked() }
-            },
+            }
         )
         animator.start()
     }
@@ -114,14 +117,13 @@
     fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
         val flyout = flyout ?: return
         hideFlyout(AnimationType.FADE) {
-            callbacks.resetTopBoundary()
             flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
         }
     }
 
     fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) {
         val flyout = flyout ?: return
-        flyout.updateData(message) { extendTopBoundary() }
+        flyout.updateData(message) {}
     }
 
     fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
@@ -131,14 +133,6 @@
         flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) }
     }
 
-    private fun extendTopBoundary() {
-        val flyout = flyout ?: return
-        val flyoutTop = flyout.top + flyout.translationY
-        // If the top position of the flyout is negative, then it's bleeding over the
-        // top boundary of its parent view
-        if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
-    }
-
     fun cancelFlyout(endAction: () -> Unit) {
         hideFlyout(AnimationType.FADE) {
             cleanupFlyoutView()
@@ -184,7 +178,6 @@
     private fun cleanupFlyoutView() {
         container.removeView(flyout)
         this@BubbleBarFlyoutController.flyout = null
-        callbacks.resetTopBoundary()
     }
 
     fun hasFlyout() = flyout != null
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index 216f5e3..75bf937 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.content.res.Resources
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Outline
@@ -44,11 +45,27 @@
     scheduler: FlyoutScheduler? = null,
 ) : ConstraintLayout(context) {
 
-    private companion object {
-        // the minimum progress of the expansion animation before the content starts fading in.
-        const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f
+    companion object {
         // the rate multiple for the background color animation relative to the morph animation.
         const val BACKGROUND_COLOR_CHANGE_RATE = 5
+        // the minimum progress of the expansion animation before the content starts fading in.
+        private const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f
+
+        private const val TEXT_ROW_HEIGHT_SP = 20
+        private const val MAX_ROWS_COUNT = 3
+
+        /** Returns the maximum possible height of the flyout view. */
+        fun getMaximumViewHeight(context: Context): Int {
+            val verticalPaddings = getFlyoutPadding(context) * 2
+            val textSizeSp = TEXT_ROW_HEIGHT_SP * MAX_ROWS_COUNT
+            val textSizePx = textSizeSp * Resources.getSystem().displayMetrics.scaledDensity
+            val triangleHeight =
+                context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_height)
+            return verticalPaddings + textSizePx.toInt() + triangleHeight
+        }
+
+        private fun getFlyoutPadding(context: Context) =
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding)
     }
 
     private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this)
@@ -61,10 +78,7 @@
     private val message: TextView by
         lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_text) }
 
-    private val flyoutPadding by
-        lazy(LazyThreadSafetyMode.NONE) {
-            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding)
-        }
+    private val flyoutPadding by lazy(LazyThreadSafetyMode.NONE) { getFlyoutPadding(context) }
 
     private val triangleHeight by
         lazy(LazyThreadSafetyMode.NONE) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
index e2f010a..0804a62 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
@@ -18,11 +18,6 @@
 
 /** Callbacks that the flyout uses to notify of events. */
 interface FlyoutCallbacks {
-    /** Requests to extend the top boundary of the parent to fully include the flyout. */
-    fun extendTopBoundary(space: Int)
-
-    /** Resets the top boundary of the parent. */
-    fun resetTopBoundary()
 
     /** The flyout was clicked. */
     fun flyoutClicked()
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index d9589bb..595dac3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -74,6 +74,9 @@
     val isBubblesShowingOnOverview: Boolean
         get() = launcherState == BubbleLauncherState.OVERVIEW
 
+    /** Bubble bar vertical center for launcher home. */
+    var bubbleBarVerticalCenterForHome: Int
+
     /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */
     var isSysuiLocked: Boolean
 
@@ -121,9 +124,6 @@
     /** Set a bubble bar location */
     fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation)
 
-    /** Set the hotseat vertical center that bubble bar will align with. */
-    fun setHotseatVerticalCenter(hotseatVerticalCenter: Int)
-
     /**
      * Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded
      * bubble bar based on the controller implementation.
@@ -131,7 +131,17 @@
     fun stashBubbleBar()
 
     /** Shows the bubble bar, and expands bubbles depending on [expandBubbles]. */
-    fun showBubbleBar(expandBubbles: Boolean)
+    fun showBubbleBar(expandBubbles: Boolean) {
+        showBubbleBar(expandBubbles = expandBubbles, bubbleBarGesture = false)
+    }
+
+    /**
+     * Shows the bubble bar, and expands bubbles depending on [expandBubbles].
+     *
+     * Set [bubbleBarGesture] to true if this request originates from a touch gesture on the bubble
+     * bar.
+     */
+    fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean)
 
     // TODO(b/354218264): Move to BubbleBarViewAnimator
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 45f5568..9d8c0ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -47,7 +47,7 @@
     private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory<View>.MultiProperty
     private lateinit var bubbleBarScaleAnimator: AnimatedFloat
     private lateinit var controllersAfterInitAction: ControllersAfterInitAction
-    private var hotseatVerticalCenter: Int = 0
+    override var bubbleBarVerticalCenterForHome: Int = 0
 
     override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP
         set(state) {
@@ -97,7 +97,7 @@
     override val bubbleBarTranslationYForHotseat: Float
         get() {
             val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
-            return -hotseatVerticalCenter + bubbleBarHeight / 2
+            return -bubbleBarVerticalCenterForHome + bubbleBarHeight / 2
         }
 
     override val bubbleBarTranslationY: Float
@@ -159,10 +159,6 @@
         animatorSet.setDuration(BAR_STASH_DURATION).start()
     }
 
-    override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
-        this.hotseatVerticalCenter = hotseatVerticalCenter
-    }
-
     override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY)
 
     override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
@@ -180,8 +176,8 @@
         updateExpandedState(expand = false)
     }
 
-    override fun showBubbleBar(expandBubbles: Boolean) {
-        updateExpandedState(expandBubbles)
+    override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) {
+        updateExpandedState(expand = expandBubbles, bubbleBarGesture = bubbleBarGesture)
     }
 
     override fun stashBubbleBarImmediate() {
@@ -235,13 +231,14 @@
         // no op since does not have a handle view
     }
 
-    private fun updateExpandedState(expand: Boolean) {
+    private fun updateExpandedState(expand: Boolean, bubbleBarGesture: Boolean = false) {
         if (bubbleBarViewController.isHiddenForNoBubbles) {
             // If there are no bubbles the bar is invisible, nothing to do here.
             return
         }
         if (bubbleBarViewController.isExpanded != expand) {
-            bubbleBarViewController.isExpanded = expand
+            val maybeShowEdu = expand && bubbleBarGesture
+            bubbleBarViewController.setExpanded(expand, maybeShowEdu)
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 22d504f..df00696 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -78,7 +78,7 @@
         context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
 
     private var animator: AnimatorSet? = null
-    private var hotseatVerticalCenter: Int = 0
+    override var bubbleBarVerticalCenterForHome: Int = 0
 
     override var isStashed: Boolean = false
         @VisibleForTesting set
@@ -124,7 +124,7 @@
     override val bubbleBarTranslationYForHotseat: Float
         get() {
             val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
-            return -hotseatVerticalCenter + bubbleBarHeight / 2
+            return -bubbleBarVerticalCenterForHome + bubbleBarHeight / 2
         }
 
     override val bubbleBarTranslationYForTaskbar: Float =
@@ -182,10 +182,6 @@
             .start()
     }
 
-    override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
-        this.hotseatVerticalCenter = hotseatVerticalCenter
-    }
-
     override fun showBubbleBarImmediate() {
         showBubbleBarImmediate(bubbleBarTranslationY)
     }
@@ -250,8 +246,12 @@
         updateStashedAndExpandedState(stash = true, expand = false)
     }
 
-    override fun showBubbleBar(expandBubbles: Boolean) {
-        updateStashedAndExpandedState(stash = false, expandBubbles)
+    override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) {
+        updateStashedAndExpandedState(
+            stash = false,
+            expand = expandBubbles,
+            bubbleBarGesture = bubbleBarGesture,
+        )
     }
 
     override fun getDiffBetweenHandleAndBarCenters(): Float {
@@ -481,7 +481,11 @@
     }
 
     @VisibleForTesting
-    fun updateStashedAndExpandedState(stash: Boolean, expand: Boolean) {
+    fun updateStashedAndExpandedState(
+        stash: Boolean,
+        expand: Boolean,
+        bubbleBarGesture: Boolean = false,
+    ) {
         if (bubbleBarViewController.isHiddenForNoBubbles) {
             // If there are no bubbles the bar and handle are invisible, nothing to do here.
             return
@@ -502,7 +506,8 @@
                 }
         }
         if (bubbleBarViewController.isExpanded != expand) {
-            bubbleBarViewController.isExpanded = expand
+            val maybeShowEdu = expand && bubbleBarGesture
+            bubbleBarViewController.setExpanded(expand, maybeShowEdu)
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 1f6c671..3507937 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -87,10 +87,16 @@
         mCurrentHomeIntent = createHomeIntent();
         mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(mContext.getPackageName());
         ResolveInfo info = context.getPackageManager().resolveActivity(mMyHomeIntent, 0);
+        ActivityInfo myHomeActivityInfo = info == null ? null : info.activityInfo;
+        int myHomeConfigChanges = myHomeActivityInfo == null ? 0 : myHomeActivityInfo.configChanges;
         ComponentName myHomeComponent =
-                new ComponentName(context.getPackageName(), info.activityInfo.name);
-        mMyHomeIntent.setComponent(myHomeComponent);
-        mConfigChangesMap.append(myHomeComponent.hashCode(), info.activityInfo.configChanges);
+                myHomeActivityInfo == null
+                        ? mMyHomeIntent.resolveActivity(context.getPackageManager())
+                        : new ComponentName(context.getPackageName(), myHomeActivityInfo.name);
+        if (myHomeComponent != null) {
+            mMyHomeIntent.setComponent(myHomeComponent);
+            mConfigChangesMap.append(myHomeComponent.hashCode(), myHomeConfigChanges);
+        }
         mSetupWizardPkg = context.getString(R.string.setup_wizard_pkg);
 
         ComponentName fallbackComponent = new ComponentName(mContext, RecentsActivity.class);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 6c4c74c..59d4547 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -313,7 +313,7 @@
         setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner);
         setUnfoldAnimationListener(mUnfoldAnimationListener);
         setDesktopTaskListener(mDesktopTaskListener);
-        setAssistantOverridesRequested(ContextualSearchInvoker.newInstance(mContext)
+        setAssistantOverridesRequested(new ContextualSearchInvoker(mContext)
                 .getSysUiAssistOverrideInvocationTypes());
         mStateChangeCallbacks.forEach(Runnable::run);
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 8edb16f..75e7c64 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -286,8 +286,7 @@
         @Override
         public void onAssistantOverrideInvoked(int invocationType) {
             executeForTouchInteractionService(tis -> {
-                if (!ContextualSearchInvoker.newInstance(tis)
-                        .tryStartAssistOverride(invocationType)) {
+                if (!new ContextualSearchInvoker(tis).tryStartAssistOverride(invocationType)) {
                     Log.w(TAG, "Failed to invoke Assist override");
                 }
             });
@@ -524,7 +523,8 @@
         }
 
         protected void onOverviewTargetChange() {
-            for (Runnable listener : mOnOverviewTargetChangeListeners) {
+            Set<Runnable> listeners = new HashSet<>(mOnOverviewTargetChangeListeners);
+            for (Runnable listener : listeners) {
                 listener.run();
             }
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index e158975..86f9829 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -36,11 +36,13 @@
 import com.android.launcher3.LauncherAnimationRunner
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
 import com.android.launcher3.R
+import com.android.launcher3.compat.AccessibilityManagerCompat
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory
 import com.android.launcher3.statemanager.StatefulContainer
 import com.android.launcher3.taskbar.TaskbarUIController
+import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL
 import com.android.launcher3.util.ContextTracker
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.RunnableList
@@ -355,6 +357,9 @@
         if (state == HOME || state == BG_LAUNCHER) {
             cleanupRecentsWindow()
         }
+        if (state === DEFAULT) {
+            AccessibilityManagerCompat.sendStateEventToTest(baseContext, OVERVIEW_STATE_ORDINAL)
+        }
     }
 
     private fun getStateName(state: RecentsState?): String {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 778c231..6b61298 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -114,7 +114,8 @@
                 if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
                         && mStashedOrCollapsedOnDown) {
                     // Taps on the handle / collapsed state should open the bar
-                    mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+                    mBubbleStashController.showBubbleBar(
+                            /* expandBubbles= */ true, /* bubbleBarGesture= */ true);
                 }
                 break;
         }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
index 155d095..107babd 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
@@ -62,7 +62,7 @@
         mStatsLogManager = StatsLogManager.newInstance(context);
         mVibratorWrapper = VibratorWrapper.INSTANCE.get(mContext);
         mContextualSearchHapticManager = ContextualSearchHapticManager.INSTANCE.get(context);
-        mContextualSearchInvoker = ContextualSearchInvoker.newInstance(mContext);
+        mContextualSearchInvoker = new ContextualSearchInvoker(mContext);
     }
 
     /** Creates NavHandleLongPressHandler as specified by overrides */
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 3eba9c0..99c2c1c 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -195,7 +195,7 @@
                         LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary),
                 getTheme());
 
-        startBackgroundAnimation(getDP().isTablet);
+        setUpBackgroundAnimation(getDP().isTablet);
         getIDP().addOnChangeListener(mOnIDPChangeListener);
     }
 
@@ -220,7 +220,7 @@
         Executors.UI_HELPER_EXECUTOR.execute(runnable);
     }
 
-    private void startBackgroundAnimation(boolean forTablet) {
+    private void setUpBackgroundAnimation(boolean forTablet) {
         if (mVibrator == null) {
             return;
         }
@@ -264,7 +264,6 @@
                     };
         }
         mAnimatedBackground.addAnimatorListener(mBackgroundAnimatorListener);
-        mAnimatedBackground.playAnimation();
     }
 
     private void setSetupUIVisible(boolean visible) {
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
index bd454c0..3be8ea6 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
@@ -21,8 +21,8 @@
 import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH
 import android.content.Context
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import com.android.internal.app.AssistUtils
-import com.android.launcher3.R
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD
@@ -31,23 +31,26 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME
-import com.android.launcher3.util.ResourceBasedOverride
+import com.android.quickstep.BaseContainerInterface
 import com.android.quickstep.DeviceConfigWrapper
+import com.android.quickstep.OverviewComponentObserver
+import com.android.quickstep.RecentsAnimationDeviceState
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.TopTaskTracker
+import com.android.quickstep.views.RecentsView
 import com.android.systemui.shared.system.QuickStepContract
 
 /** Handles invocations and checks for Contextual Search. */
-open class ContextualSearchInvoker
+class ContextualSearchInvoker
 internal constructor(
-    protected val context: Context,
+    private val context: Context,
     private val contextualSearchStateManager: ContextualSearchStateManager,
     private val topTaskTracker: TopTaskTracker,
     private val systemUiProxy: SystemUiProxy,
-    protected val statsLogManager: StatsLogManager,
+    private val statsLogManager: StatsLogManager,
     private val contextualSearchHapticManager: ContextualSearchHapticManager,
     private val contextualSearchManager: ContextualSearchManager?,
-) : ResourceBasedOverride {
+) {
     constructor(
         context: Context
     ) : this(
@@ -61,7 +64,7 @@
     )
 
     /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
-    open fun getSysUiAssistOverrideInvocationTypes(): IntArray {
+    fun getSysUiAssistOverrideInvocationTypes(): IntArray {
         val overrideInvocationTypes = com.android.launcher3.util.IntArray()
         if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
             overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)
@@ -183,7 +186,15 @@
         if (contextualSearchManager == null) {
             return false
         }
-        contextualSearchManager.startContextualSearch(entryPoint)
+        val recentsContainerInterface = getRecentsContainerInterface()
+        if (recentsContainerInterface?.isInLiveTileMode() == true) {
+            Log.i(TAG, "Contextual Search invocation attempted: live tile")
+            endLiveTileMode(recentsContainerInterface) {
+                contextualSearchManager.startContextualSearch(entryPoint)
+            }
+        } else {
+            contextualSearchManager.startContextualSearch(entryPoint)
+        }
         return true
     }
 
@@ -199,6 +210,42 @@
         return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L
     }
 
+    @VisibleForTesting
+    fun getRecentsContainerInterface(): BaseContainerInterface<*, *>? {
+        val rads = RecentsAnimationDeviceState(context)
+        val observer = OverviewComponentObserver(context, rads)
+        try {
+            return observer.containerInterface
+        } finally {
+            observer.onDestroy()
+            rads.destroy()
+        }
+    }
+
+    /**
+     * End the live tile mode.
+     *
+     * @param onCompleteRunnable Runnable to run when the live tile is paused. May run immediately.
+     */
+    private fun endLiveTileMode(
+        recentsContainerInterface: BaseContainerInterface<*, *>?,
+        onCompleteRunnable: Runnable,
+    ) {
+        val recentsViewContainer = recentsContainerInterface?.createdContainer
+        if (recentsViewContainer == null) {
+            onCompleteRunnable.run()
+            return
+        }
+        val recentsView: RecentsView<*, *> = recentsViewContainer.getOverviewPanel()
+        recentsView.switchToScreenshot {
+            recentsView.finishRecentsAnimation(
+                true, /* toRecents */
+                false, /* shouldPip */
+                onCompleteRunnable,
+            )
+        }
+    }
+
     companion object {
         private const val TAG = "ContextualSearchInvoker"
         const val SHADE_EXPANDED_SYSUI_FLAGS =
@@ -208,14 +255,5 @@
             (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or
                 QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or
                 QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED)
-
-        @JvmStatic
-        fun newInstance(context: Context): ContextualSearchInvoker {
-            return ResourceBasedOverride.Overrides.getObject(
-                ContextualSearchInvoker::class.java,
-                context,
-                R.string.contextual_search_invoker_class,
-            )
-        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
index 334ff06..f75d3b3 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -224,7 +224,7 @@
                 UI_HELPER_EXECUTOR.getHandler().postDelayed(
                         () -> {
                             boolean contextualSearchInvoked =
-                                    ContextualSearchInvoker.newInstance(mContext).show(
+                                    new ContextualSearchInvoker(mContext).show(
                                             ENTRYPOINT_SYSTEM_ACTION);
                             if (contextualSearchInvoked) {
                                 String runningPackage =
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ec169a8..6fc33dc 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -5322,12 +5322,12 @@
         pendingAnimation.addEndListener(aBoolean -> {
             mSplitSelectStateController.launchSplitTasks(
                     aBoolean1 -> {
+                        InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
                         if (FeatureFlags.enableSplitContextually()) {
                             mSplitSelectStateController.resetState();
                         } else {
                             resetFromSplitSelectionState();
                         }
-                        InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
                     });
         });
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 3586dfb..082971c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -1566,6 +1566,7 @@
     }
 
     private fun onModalnessUpdated(modalness: Float) {
+        isClickable = modalness == 0f
         taskContainers.forEach {
             it.iconView.setModalAlpha(1 - modalness)
             it.digitalWellBeingToast?.bannerOffsetPercentage = modalness
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
index 0103e7e..5cee434 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
@@ -17,14 +17,17 @@
 package com.android.launcher3.model.data
 
 import android.content.ComponentName
-import android.content.Context
 import android.content.Intent
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE
 import com.android.launcher3.model.data.TaskViewItemInfo.Companion.createTaskViewAtom
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.launcher3.util.UserIconInfo
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay
 import com.android.quickstep.recents.di.RecentsDependencies
@@ -48,10 +51,12 @@
 /** Test for [TaskViewItemInfo] */
 @RunWith(AndroidJUnit4::class)
 class TaskViewItemInfoTest {
-    private val context = mock<Context>()
+    private val context = SandboxContext(InstrumentationRegistry.getInstrumentation().targetContext)
     private val taskView = mock<TaskView>()
     private val recentsView = mock<RecentsView<*, *>>()
     private val overlayFactory = mock<TaskOverlayFactory>()
+    private val userCache = mock<UserCache>()
+    private val userInfo = mock<UserIconInfo>()
 
     @Before
     fun setUp() {
@@ -59,7 +64,10 @@
         whenever(taskView.context).thenReturn(context)
         whenever(taskView.recentsView).thenReturn(recentsView)
         whenever(recentsView.indexOfChild(taskView)).thenReturn(TASK_VIEW_INDEX)
-        RecentsDependencies.initialize(InstrumentationRegistry.getInstrumentation().targetContext)
+        whenever(userInfo.isPrivate).thenReturn(false)
+        whenever(userCache.getUserInfo(any())).thenReturn(userInfo)
+        context.putObject(UserCache.INSTANCE, userCache)
+        RecentsDependencies.initialize(context)
     }
 
     @Test
@@ -69,9 +77,8 @@
         whenever(taskView.taskContainers).thenReturn(taskContainers)
 
         val taskViewItemInfo = TaskViewItemInfo(taskContainers[0])
-        val taskViewAtom = taskViewItemInfo.taskViewAtom
 
-        assertThat(taskViewAtom)
+        assertThat(taskViewItemInfo.taskViewAtom)
             .isEqualTo(
                 createTaskViewAtom(
                     type = 0,
@@ -80,6 +87,7 @@
                     cardinality = 1,
                 )
             )
+        assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0)
     }
 
     @Test
@@ -90,9 +98,8 @@
         whenever(taskView.taskContainers).thenReturn(taskContainers)
 
         val taskViewItemInfo = TaskViewItemInfo(taskContainers[0])
-        val taskViewAtom = taskViewItemInfo.taskViewAtom
 
-        assertThat(taskViewAtom)
+        assertThat(taskViewItemInfo.taskViewAtom)
             .isEqualTo(
                 createTaskViewAtom(
                     type = 1,
@@ -101,6 +108,7 @@
                     cardinality = 2,
                 )
             )
+        assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0)
     }
 
     @Test
@@ -115,9 +123,8 @@
         whenever(taskView.taskContainers).thenReturn(taskContainers)
 
         val taskViewItemInfo = TaskViewItemInfo(taskContainers[0])
-        val taskViewAtom = taskViewItemInfo.taskViewAtom
 
-        assertThat(taskViewAtom)
+        assertThat(taskViewItemInfo.taskViewAtom)
             .isEqualTo(
                 createTaskViewAtom(
                     type = 2,
@@ -126,6 +133,29 @@
                     cardinality = 3,
                 )
             )
+        assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0)
+    }
+
+    @Test
+    fun privateTask() {
+        val taskContainers = listOf(createTaskContainer(createTask(1)))
+        whenever(taskView.type).thenReturn(TaskViewType.SINGLE)
+        whenever(taskView.taskContainers).thenReturn(taskContainers)
+        whenever(userInfo.isPrivate).thenReturn(true)
+
+        val taskViewItemInfo = TaskViewItemInfo(taskContainers[0])
+
+        assertThat(taskViewItemInfo.taskViewAtom)
+            .isEqualTo(
+                createTaskViewAtom(
+                    type = 0,
+                    index = TASK_VIEW_INDEX,
+                    componentName = "${PACKAGE}/${CLASS}",
+                    cardinality = 1,
+                )
+            )
+        assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE)
+            .isEqualTo(FLAG_NOT_PINNABLE)
     }
 
     private fun createTask(id: Int) =
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
index cc8582c..5867e77 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -272,7 +272,8 @@
     private val taskbarIconsCentered: Boolean
         get() {
             return getOnUiThread {
-                val iconLayoutBounds = taskbarViewController.iconLayoutBounds
+                val iconLayoutBounds =
+                    taskbarViewController.transientTaskbarIconLayoutBoundsInParent
                 val availableWidth = taskbarUnitTestRule.activityContext.deviceProfile.widthPx
                 iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2
             }
@@ -282,7 +283,7 @@
         get() {
             return getOnUiThread {
                 taskbarUnitTestRule.activityContext.deviceProfile.widthPx -
-                    taskbarViewController.iconLayoutBounds.right
+                    taskbarViewController.transientTaskbarIconLayoutBoundsInParent.right
             }
         }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
index 2e471b8..024ec4c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
@@ -36,6 +36,7 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.never
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
@@ -206,7 +207,7 @@
             bubbleBarSwipeController.start()
             bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
         }
-        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true)
     }
 
     @Test
@@ -227,11 +228,12 @@
             bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
             bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
         }
-        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true)
         verify(bubbleStashController).stashBubbleBar()
 
         getInstrumentation().runOnMainSync { bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) }
-        verify(bubbleStashController, times(2)).showBubbleBar(expandBubbles = false)
+        verify(bubbleStashController, times(2))
+            .showBubbleBar(expandBubbles = false, bubbleBarGesture = true)
     }
 
     @Test
@@ -241,9 +243,9 @@
             bubbleBarSwipeController.start()
             bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
         }
-        verify(bubbleStashController, never()).showBubbleBar(expandBubbles = true)
+        verify(bubbleStashController, never()).showBubbleBar(expandBubbles = eq(true), any())
         getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
-        verify(bubbleStashController).showBubbleBar(expandBubbles = true)
+        verify(bubbleStashController).showBubbleBar(expandBubbles = true, bubbleBarGesture = true)
     }
 
     @Test
@@ -253,12 +255,12 @@
             bubbleBarSwipeController.start()
             bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
         }
-        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true)
         getInstrumentation().runOnMainSync {
             bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
             bubbleBarSwipeController.finish()
         }
-        verify(bubbleStashController, never()).showBubbleBar(expandBubbles = true)
+        verify(bubbleStashController, never()).showBubbleBar(expandBubbles = eq(true), any())
     }
 
     @Test
@@ -334,7 +336,7 @@
             bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
             bubbleBarSwipeController.finish()
         }
-        verify(bubbleStashController).showBubbleBar(expandBubbles = true)
+        verify(bubbleStashController).showBubbleBar(expandBubbles = true, bubbleBarGesture = true)
     }
 
     @Test
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 8beceb0..29d142f 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
@@ -36,6 +36,7 @@
 import com.android.launcher3.R
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
+import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleView
 import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
@@ -69,6 +70,7 @@
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
+    private lateinit var bubbleBarParentViewController: TestBubbleBarParentViewHeightUpdateNotifier
     private lateinit var overflowView: BubbleView
     private lateinit var bubbleView: BubbleView
     private lateinit var bubble: BubbleBarBubble
@@ -84,6 +86,7 @@
     @Before
     fun setUp() {
         animatorScheduler = TestBubbleBarViewAnimatorScheduler()
+        bubbleBarParentViewController = TestBubbleBarParentViewHeightUpdateNotifier()
         PhysicsAnimatorTestUtils.prepareForTest()
         setupFlyoutController()
     }
@@ -102,6 +105,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -122,7 +126,7 @@
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         // execute the hide bubble animation
@@ -135,6 +139,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
@@ -156,6 +161,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -178,7 +184,7 @@
         assertThat(animator.isAnimating).isTrue()
 
         verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         // verify the hide bubble animation is pending
@@ -188,6 +194,7 @@
 
         waitForFlyoutToHide()
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(animatorScheduler.delayedBlock).isNull()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
@@ -209,6 +216,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -255,6 +263,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -266,7 +275,7 @@
         // let the animation start and wait for it to complete
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         // execute the hide bubble animation
@@ -282,7 +291,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.onStashStateChangingWhileAnimating()
         }
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(animator.isAnimating).isFalse()
         verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
 
@@ -306,6 +315,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -344,6 +354,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -389,6 +400,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -440,6 +452,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -454,7 +467,7 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         // verify the hide bubble animation is pending
@@ -469,6 +482,7 @@
 
         waitForFlyoutToHide()
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(handle.alpha).isEqualTo(0)
         assertThat(handle.translationY)
             .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
@@ -495,6 +509,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -510,7 +525,7 @@
         assertThat(animator.isAnimating).isTrue()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -522,6 +537,7 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(0)
         assertThat(handle.translationY).isEqualTo(0)
@@ -550,6 +566,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -585,6 +602,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -600,7 +618,7 @@
         assertThat(animator.isAnimating).isTrue()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -608,6 +626,7 @@
 
         waitForFlyoutToHide()
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
@@ -629,6 +648,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -678,6 +698,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -689,7 +710,7 @@
         // wait for the animation to start
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(animator.isAnimating).isTrue()
@@ -704,6 +725,7 @@
 
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
 
         verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(animator.isAnimating).isFalse()
@@ -724,6 +746,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -745,7 +768,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         barAnimator.assertIsRunning()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -754,6 +777,7 @@
         waitForFlyoutToHide()
 
         assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         // the bubble bar translation y should be back to its initial value
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
@@ -778,6 +802,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -832,6 +857,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -896,6 +922,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpanded,
                 animatorScheduler,
             )
@@ -921,7 +948,7 @@
         // verify there is a pending hide animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -933,6 +960,7 @@
 
         waitForFlyoutToHide()
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(bubbleBarView.isExpanded).isTrue()
@@ -954,6 +982,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -986,7 +1015,7 @@
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
         assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
             .isEqualTo("updated message")
@@ -1001,6 +1030,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
@@ -1022,6 +1052,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -1042,7 +1073,7 @@
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
@@ -1075,6 +1106,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
@@ -1096,6 +1128,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -1116,7 +1149,7 @@
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
@@ -1137,6 +1170,7 @@
             // the flyout should now reverse and expand
             animatorTestRule.advanceTimeBy(400)
         }
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
 
         assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
             .isEqualTo("updated message")
@@ -1159,6 +1193,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3)
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
@@ -1180,6 +1215,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -1200,7 +1236,7 @@
         assertThat(bubbleBarView.scaleY).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isTrue()
-
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
         waitForFlyoutToShow()
 
         assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
@@ -1216,7 +1252,6 @@
         PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) {
             bubbleBarView.alpha < 0.5
         }
-
         // we're about to interrupt the animation which will cancel the current animation and start
         // a new one. pause the scheduler to delay starting the new animation. this allows us to run
         // the test deterministically
@@ -1229,9 +1264,11 @@
             animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
         }
 
+        // since animation was interrupted there shouldn`t be additional calls to adjust window
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
-
         // verify there's a new job scheduled and start it. this is starting the animation from the
         // handle back to the bar
         assertThat(animatorScheduler.pausedBlock).isNotNull()
@@ -1240,9 +1277,9 @@
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
-
         waitForFlyoutToShow()
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
         assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
             .isEqualTo("updated message")
         assertThat(handle.alpha).isEqualTo(0)
@@ -1258,7 +1295,6 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
         waitForFlyoutToHide()
-
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         // verify the hide animation was rescheduled and run it
@@ -1271,6 +1307,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3)
         assertThat(handle.alpha).isEqualTo(1)
         assertThat(handle.translationY).isEqualTo(0)
         assertThat(bubbleBarView.alpha).isEqualTo(0)
@@ -1292,6 +1329,7 @@
                 bubbleBarView,
                 bubbleStashController,
                 flyoutController,
+                bubbleBarParentViewController,
                 onExpandedNoOp,
                 animatorScheduler,
             )
@@ -1392,10 +1430,6 @@
             }
         val flyoutCallbacks =
             object : FlyoutCallbacks {
-                override fun extendTopBoundary(space: Int) {}
-
-                override fun resetTopBoundary() {}
-
                 override fun flyoutClicked() {}
             }
         val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
@@ -1478,6 +1512,16 @@
             delayedBlock = null
         }
     }
+
+    private class TestBubbleBarParentViewHeightUpdateNotifier :
+        BubbleBarParentViewHeightUpdateNotifier {
+
+        var timesInvoked: Int = 0
+
+        override fun updateTopBoundary() {
+            timesInvoked++
+        }
+    }
 }
 
 private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index 103c769..91fe6a6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -128,46 +128,6 @@
     }
 
     @Test
-    fun showFlyout_extendsTopBoundary() {
-        // set negative translation for the flyout so that it will request to extend the top
-        // boundary
-        flyoutTy = -50f
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            setupAndShowFlyout()
-            assertThat(flyoutContainer.childCount).isEqualTo(1)
-        }
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animatorTestRule.advanceTimeBy(showAnimationDuration)
-        }
-        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
-    }
-
-    @Test
-    fun showFlyout_withinBoundary() {
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            setupAndShowFlyout()
-            assertThat(flyoutContainer.childCount).isEqualTo(1)
-        }
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animatorTestRule.advanceTimeBy(showAnimationDuration)
-        }
-        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
-    }
-
-    @Test
-    fun collapseFlyout_resetsTopBoundary() {
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            setupAndShowFlyout()
-            assertThat(flyoutContainer.childCount).isEqualTo(1)
-            flyoutController.collapseFlyout {}
-            animatorTestRule.advanceTimeBy(hideAnimationDuration)
-        }
-        assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
-    }
-
-    @Test
     fun cancelFlyout_fadesOutFlyout() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             setupAndShowFlyout()
@@ -178,7 +138,6 @@
             animatorTestRule.advanceTimeBy(hideAnimationDuration)
             assertThat(flyoutView.alpha).isEqualTo(0f)
         }
-        assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
     }
 
     @Test
@@ -217,7 +176,6 @@
             assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
                 .isEqualTo("new message")
         }
-        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
     }
 
     @Test
@@ -246,7 +204,6 @@
             animatorTestRule.advanceTimeBy(showAnimationDuration)
             assertThat(flyout.alpha).isEqualTo(1)
         }
-        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
     }
 
     @Test
@@ -290,18 +247,8 @@
 
     class FakeFlyoutCallbacks : FlyoutCallbacks {
 
-        var topBoundaryExtendedSpace = 0
-        var topBoundaryReset = false
         var flyoutClicked = false
 
-        override fun extendTopBoundary(space: Int) {
-            topBoundaryExtendedSpace = space
-        }
-
-        override fun resetTopBoundary() {
-            topBoundaryReset = true
-        }
-
         override fun flyoutClicked() {
             flyoutClicked = true
         }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index f795ab1..88b39d3 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -37,6 +37,7 @@
 import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
 import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
@@ -76,7 +77,7 @@
             PersistentBubbleStashController(DefaultDimensionsProvider())
         setUpBubbleBarView()
         setUpBubbleBarController()
-        persistentTaskBarStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
+        persistentTaskBarStashController.bubbleBarVerticalCenterForHome = HOTSEAT_VERTICAL_CENTER
         persistentTaskBarStashController.init(
             taskbarInsetsController,
             bubbleBarViewController,
@@ -342,6 +343,45 @@
         verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
     }
 
+    @Test
+    fun showBubbleBar_expand_bubbleBarGesture() {
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+
+        persistentTaskBarStashController.showBubbleBar(
+            expandBubbles = true,
+            bubbleBarGesture = true,
+        )
+
+        verify(bubbleBarViewController).setExpanded(true, true)
+    }
+
+    @Test
+    fun showBubbleBar_expand_notBubbleBarGesture() {
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+
+        persistentTaskBarStashController.showBubbleBar(
+            expandBubbles = true,
+            bubbleBarGesture = false,
+        )
+
+        verify(bubbleBarViewController).setExpanded(true, false)
+    }
+
+    @Test
+    fun showBubbleBar_notExpanding_bubbleBarGesture() {
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+
+        persistentTaskBarStashController.showBubbleBar(
+            expandBubbles = false,
+            bubbleBarGesture = true,
+        )
+
+        verify(bubbleBarViewController, never()).setExpanded(any(), any())
+    }
+
     private fun advanceTimeBy(advanceMs: Long) {
         // Advance animator for on-device tests
         getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index 1bbd12a..f642345 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -109,7 +109,7 @@
         setUpStashedHandleView()
         setUpBubbleStashedHandleViewController()
         PhysicsAnimatorTestUtils.prepareForTest()
-        mTransientBubbleStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
+        mTransientBubbleStashController.bubbleBarVerticalCenterForHome = HOTSEAT_VERTICAL_CENTER
         mTransientBubbleStashController.init(
             taskbarInsetsController,
             bubbleBarViewController,
@@ -362,6 +362,57 @@
     }
 
     @Test
+    fun updateStashedAndExpandedState_expand_bubbleBarGesture() {
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = true,
+                bubbleBarGesture = true,
+            )
+        }
+
+        verify(bubbleBarViewController).setExpanded(true, true)
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_expand_notBubbleBarGesture() {
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = true,
+                bubbleBarGesture = false,
+            )
+        }
+
+        verify(bubbleBarViewController).setExpanded(true, false)
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_notExpanding_bubbleBarGesture() {
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = false,
+                bubbleBarGesture = true,
+            )
+        }
+
+        verify(bubbleBarViewController, never()).setExpanded(any(), any())
+    }
+
+    @Test
     fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
         // Given screen is locked and bubble bar has bubbles
         getInstrumentation().runOnMainSync {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
index 543ffe6..88774be 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
@@ -31,6 +31,8 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -46,13 +48,17 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.DeviceConfigWrapper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TopTaskTracker;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -73,6 +79,9 @@
     private @Mock StatsLogManager.StatsLogger mMockStatsLogger;
     private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager;
     private @Mock ContextualSearchManager mMockContextualSearchManager;
+    private @Mock BaseContainerInterface mMockContainerInterface;
+    private @Mock RecentsViewContainer mMockRecentsViewContainer;
+    private @Mock RecentsView mMockRecentsView;
     private ContextualSearchInvoker mContextualSearchInvoker;
 
     @Before
@@ -86,10 +95,15 @@
         when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true);
         when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true);
         when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger);
+        when(mMockContainerInterface.getCreatedContainer()).thenReturn(mMockRecentsViewContainer);
+        when(mMockRecentsViewContainer.getOverviewPanel()).thenReturn(mMockRecentsView);
 
-        mContextualSearchInvoker = new ContextualSearchInvoker(context, mMockStateManager,
+        mContextualSearchInvoker = spy(new ContextualSearchInvoker(context, mMockStateManager,
                 mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager,
-                mMockContextualSearchHapticManager, mMockContextualSearchManager);
+                mMockContextualSearchHapticManager, mMockContextualSearchManager
+        ));
+        doReturn(mMockContainerInterface).when(mContextualSearchInvoker)
+                .getRecentsContainerInterface();
     }
 
     @Test
@@ -244,6 +258,64 @@
         }
     }
 
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_liveTile() {
+        when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true);
+        ArgumentCaptor<Runnable> switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class);
+        ArgumentCaptor<Runnable> finishRecentsAnimationCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+
+        assertTrue("Expected invocation unchecked to succeed",
+                mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                        CONTEXTUAL_SEARCH_ENTRY_POINT));
+        verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture());
+        switchToScreenshotCaptor.getValue().run();
+        verify(mMockRecentsView).finishRecentsAnimation(anyBoolean(), anyBoolean(),
+                finishRecentsAnimationCaptor.capture());
+        finishRecentsAnimationCaptor.getValue().run();
+        verify(mMockContextualSearchManager).startContextualSearch(CONTEXTUAL_SEARCH_ENTRY_POINT);
+        verifyNoMoreInteractions(mMockStatsLogManager);
+    }
+
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_liveTile_failsToSwitchToScreenshot() {
+        when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true);
+        ArgumentCaptor<Runnable> switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class);
+        ArgumentCaptor<Runnable> finishRecentsAnimationCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+
+        assertTrue("Expected invocation unchecked to succeed",
+                mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                        CONTEXTUAL_SEARCH_ENTRY_POINT));
+        verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture());
+
+        // Don't run switchToScreenshot's callback. Therefore, recents animation should not finish.
+        verify(mMockRecentsView, never()).finishRecentsAnimation(anyBoolean(), anyBoolean(),
+                finishRecentsAnimationCaptor.capture());
+        // And ContextualSearch should not start.
+        verify(mMockContextualSearchManager, never()).startContextualSearch(anyInt());
+        verifyNoMoreInteractions(mMockStatsLogManager);
+    }
+
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_liveTile_failsToFinishRecentsAnimation() {
+        when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true);
+        ArgumentCaptor<Runnable> switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class);
+        ArgumentCaptor<Runnable> finishRecentsAnimationCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+
+        assertTrue("Expected invocation unchecked to succeed",
+                mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                        CONTEXTUAL_SEARCH_ENTRY_POINT));
+        verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture());
+        switchToScreenshotCaptor.getValue().run();
+        verify(mMockRecentsView).finishRecentsAnimation(anyBoolean(), anyBoolean(),
+                finishRecentsAnimationCaptor.capture());
+        // Don't run finishRecentsAnimation's callback. Therefore ContextualSearch should not start.
+        verify(mMockContextualSearchManager, never()).startContextualSearch(anyInt());
+        verifyNoMoreInteractions(mMockStatsLogManager);
+    }
+
     private AutoCloseable overrideSearchHapticCommitFlag(boolean value) {
         return TestExtensions.overrideNavConfigFlag(
                 "ENABLE_SEARCH_HAPTIC_COMMIT",
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
index 26189df..ae96c09c 100644
--- a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
@@ -30,6 +30,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.quickstep.SystemUiProxy
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -74,7 +75,10 @@
     }
 
     @Test
-    @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+    @EnableFlags(
+        FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
+        FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun registerTransitions_appLaunchFlagEnabled_registersTransition() {
         transitionManager.registerTransitions()
 
@@ -82,7 +86,10 @@
     }
 
     @Test
-    @DisableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+    @DisableFlags(
+        FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
+        FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun registerTransitions_appLaunchFlagDisabled_doesntRegisterTransition() {
         transitionManager.registerTransitions()
 
@@ -90,7 +97,10 @@
     }
 
     @Test
-    @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+    @EnableFlags(
+        FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
+        FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun registerTransitions_usesCorrectFilter() {
         transitionManager.registerTransitions()
         val filterArgumentCaptor = argumentCaptor<TransitionFilter>()
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 887a708..c477633 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -163,8 +163,8 @@
     </declare-styleable>
 
     <declare-styleable name="GridDimension">
-        <attr name="minDeviceWidthPx" format="float"/>
-        <attr name="minDeviceHeightPx" format="float"/>
+        <attr name="minDeviceWidthDp" format="integer"/>
+        <attr name="minDeviceHeightDp" format="integer"/>
         <attr name="numGridDimension" format="integer"/>
         <attr name="dbFile" />
         <attr name="defaultLayoutId"/>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index eaa720e..247ee48 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -980,7 +980,7 @@
     public boolean shouldDrawAppContrastTile() {
         return mDisplay == DISPLAY_WORKSPACE && shouldTextBeVisible()
                 && PillColorProvider.getInstance(getContext()).isMatchaEnabled()
-                && enableContrastTiles() && TextUtils.isEmpty(getText());
+                && enableContrastTiles() && !TextUtils.isEmpty(getText());
     }
 
     public void setTextVisibility(boolean visible) {
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index ee72c22..df5f520 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.CellAndSpan;
 import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.ParcelableSparseArray;
 import com.android.launcher3.util.Themes;
@@ -78,6 +79,8 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
+import com.google.android.msdl.data.model.MSDLToken;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -204,6 +207,8 @@
 
     private static final Paint sPaint = new Paint();
 
+    private final MSDLPlayerWrapper mMSDLPlayerWrapper;
+
     // Related to accessible drag and drop
     DragAndDropAccessibilityDelegate mTouchHelper;
 
@@ -237,6 +242,8 @@
         mContainerType = a.getInteger(R.styleable.CellLayout_containerType, WORKSPACE);
         a.recycle();
 
+        mMSDLPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context);
+
         // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show
         // the user where a dragged item will land when dropped.
         setWillNotDraw(false);
@@ -1153,6 +1160,9 @@
             DropTarget.DragObject dragObject) {
         if (mDragCell[0] != cellX || mDragCell[1] != cellY || mDragCellSpan[0] != spanX
                 || mDragCellSpan[1] != spanY) {
+            if (Flags.msdlFeedback()) {
+                mMSDLPlayerWrapper.playToken(MSDLToken.DRAG_INDICATOR_DISCRETE);
+            }
             mDragCell[0] = cellX;
             mDragCell[1] = cellY;
             mDragCellSpan[0] = spanX;
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 09225e7..9b6fe4e 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -1861,13 +1861,15 @@
      * the bubble bar.
      *
      * <p>Does not check for visible bubbles persistence, so caller should call
-     * {@link #shouldAdjustHotseatForBubbleBar} first.
+     * {@link #shouldAdjustHotseatOrQsbForBubbleBar} first.
      *
      * <p>If there's no adjustment needed, this method returns {@code 0}.
-     * @see #shouldAdjustHotseatForBubbleBar(Context, boolean)
+     * @see #shouldAdjustHotseatOrQsbForBubbleBar(Context, boolean)
      */
     public float getHotseatAdjustedBorderSpaceForBubbleBar(Context context) {
-        if (!shouldAdjustHotseatForBubbleBar(context)) return 0;
+        if (shouldAlignBubbleBarWithQSB() || !shouldAdjustHotseatOrQsbForBubbleBar(context)) {
+            return 0;
+        }
         // The adjustment is shrinking the hotseat's width by 1 icon on either side.
         int iconsWidth =
                 iconSizePx * numShownHotseatIcons + hotseatBorderSpace * (numShownHotseatIcons - 1);
@@ -1880,27 +1882,37 @@
      * Returns the hotseat icon translation X for the cellX index.
      *
      * <p>Does not check for visible bubbles persistence, so caller should call
-     * {@link #shouldAdjustHotseatForBubbleBar} first.
+     * {@link #shouldAdjustHotseatOrQsbForBubbleBar} first.
      *
      * <p>If there's no adjustment needed, this method returns {@code 0}.
-     * @see #shouldAdjustHotseatForBubbleBar(Context, boolean)
+     * @see #shouldAdjustHotseatOrQsbForBubbleBar(Context, boolean)
      */
     public float getHotseatAdjustedTranslation(Context context, int cellX) {
-        if (!shouldAdjustHotseatForBubbleBar(context)) return 0;
         float borderSpace = getHotseatAdjustedBorderSpaceForBubbleBar(context);
+        if (borderSpace == 0) return borderSpace;
         float borderSpaceDelta = borderSpace - hotseatBorderSpace;
         return iconSizePx + cellX * borderSpaceDelta;
     }
 
-    /** Returns whether hotseat should be adjusted for the bubble bar. */
-    public boolean shouldAdjustHotseatForBubbleBar(Context context, boolean hasBubbles) {
-        return hasBubbles && shouldAdjustHotseatForBubbleBar(context);
+    /** Returns whether hotseat or QSB should be adjusted for the bubble bar. */
+    public boolean shouldAdjustHotseatOrQsbForBubbleBar(Context context, boolean hasBubbles) {
+        return hasBubbles && shouldAdjustHotseatOrQsbForBubbleBar(context);
     }
 
-    private boolean shouldAdjustHotseatForBubbleBar(Context context) {
-        // only need to adjust if bubble bar is enabled, when QSB is on top of the hotseat and
-        // there's not enough space for the bubble bar to the right of the hotseat.
-        return !isQsbInline && getHotseatLayoutPadding(context).right <= mBubbleBarSpaceThresholdPx;
+    /** Returns whether hotseat should be adjusted for the bubble bar. */
+    public boolean shouldAdjustHotseatForBubbleBar(Context context, boolean hasBubbles) {
+        return shouldAlignBubbleBarWithHotseat()
+                && shouldAdjustHotseatOrQsbForBubbleBar(context, hasBubbles);
+    }
+
+    /** Returns whether hotseat or QSB should be adjusted for the bubble bar. */
+    public boolean shouldAdjustHotseatOrQsbForBubbleBar(Context context) {
+        // only need to adjust if QSB is on top of the hotseat and there's not enough space for the
+        // bubble bar to either side of the hotseat.
+        if (isQsbInline) return false;
+        Rect hotseatPadding = getHotseatLayoutPadding(context);
+        int hotseatMinHorizontalPadding = Math.min(hotseatPadding.left, hotseatPadding.right);
+        return hotseatMinHorizontalPadding <= mBubbleBarSpaceThresholdPx;
     }
 
     /**
@@ -2055,15 +2067,29 @@
     }
 
     /**
-     * Returns the number of pixels the hotseat icons vertical center is translated from the bottom
-     * of the screen.
+     * Returns the number of pixels the hotseat icons or QSB vertical center is translated from the
+     * bottom of the screen.
      */
-    public int getHotseatVerticalCenter() {
-        return hotseatBarSizePx
-                - (isQsbInline ? 0 : hotseatQsbVisualHeight)
-                - hotseatQsbSpace
-                - (hotseatCellHeightPx / 2)
-                + ((hotseatCellHeightPx - iconSizePx) / 2);
+    public int getBubbleBarVerticalCenterForHome() {
+        if (shouldAlignBubbleBarWithHotseat()) {
+            return hotseatBarSizePx
+                    - (isQsbInline ? 0 : hotseatQsbVisualHeight)
+                    - hotseatQsbSpace
+                    - (hotseatCellHeightPx / 2)
+                    + ((hotseatCellHeightPx - iconSizePx) / 2);
+        } else {
+            return hotseatBarSizePx - (hotseatQsbVisualHeight / 2);
+        }
+    }
+
+    /** Returns whether bubble bar should be aligned with the hotseat. */
+    public boolean shouldAlignBubbleBarWithQSB() {
+        return !shouldAlignBubbleBarWithHotseat();
+    }
+
+    /** Returns whether bubble bar should be aligned with the hotseat. */
+    public boolean shouldAlignBubbleBarWithHotseat() {
+        return isQsbInline || isGestureMode;
     }
 
     /**
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index b2ccba4..6be8098 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -186,9 +186,10 @@
      */
     public void adjustForBubbleBar(boolean isBubbleBarVisible) {
         DeviceProfile dp = mActivity.getDeviceProfile();
-        float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
-        boolean shouldAdjustHotseat = isBubbleBarVisible
-                && Float.compare(adjustedBorderSpace, 0f) != 0;
+        boolean shouldAdjust = isBubbleBarVisible
+                && dp.shouldAdjustHotseatOrQsbForBubbleBar(getContext());
+        boolean shouldAdjustHotseat = shouldAdjust
+                && dp.shouldAlignBubbleBarWithHotseat();
         ShortcutAndWidgetContainer icons = getShortcutsAndWidgets();
         // update the translation provider for future layout passes of hotseat icons.
         if (shouldAdjustHotseat) {
@@ -209,9 +210,12 @@
                 animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx));
             }
         }
+        //TODO(b/381109832) refactor & simplify adjustment logic
+        boolean shouldAdjustQsb =
+                shouldAdjustHotseat || (shouldAdjust && dp.shouldAlignBubbleBarWithQSB());
         if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) {
             final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets();
-            final float targetInsetFraction = shouldAdjustHotseat
+            final float targetInsetFraction = shouldAdjustQsb
                     ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
             ValueAnimator qsbAnimator =
                     ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction);
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index e1d84be..c044c52 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -654,9 +654,9 @@
      * Parses through the xml to find GridDimension specs. Then calls findBestRowCount to get the
      * correct row count for this GridOption.
      *
-     * @return the result of {@link #findBestRowCount(List, Info)}.
+     * @return the result of {@link #findBestRowCount(List, int, int)}.
      */
-    public static GridDimension getRowCount(ResourceHelper resourceHelper, Context context,
+    private static GridDimension getRowCount(ResourceHelper resourceHelper, Context context,
             Info displayInfo) {
         ArrayList<GridDimension> rowCounts = new ArrayList<>();
 
@@ -674,24 +674,40 @@
             throw new RuntimeException(e);
         }
 
-        return findBestRowCount(rowCounts, displayInfo);
+        // Finds the min width and height in dp for all displays.
+        int[] dimens = findMinWidthAndHeightDpForDevice(displayInfo);
+
+        return findBestRowCount(rowCounts, dimens[0], dimens[1]);
     }
 
     /**
      * @return the biggest row count that fits the display dimensions spec using GridDimension to
-     * determine that. If no best row count is found, return -1.
+     * determine that. If no best row count is found, return null.
      */
-    public static GridDimension findBestRowCount(List<GridDimension> list, Info displayInfo) {
+    private static GridDimension findBestRowCount(List<GridDimension> list, int minWidthDp,
+            int minHeightDp) {
+        GridDimension selectedRow = null;
+        for (GridDimension item: list) {
+            if (minWidthDp >= item.mMinDeviceWidthDp && minHeightDp >= item.mMinDeviceHeightDp) {
+                if (selectedRow == null || selectedRow.mNumGridDimension < item.mNumGridDimension) {
+                    selectedRow = item;
+                }
+            }
+        }
+        return selectedRow;
+    }
+
+    private static int[] findMinWidthAndHeightDpForDevice(Info displayInfo) {
         int minWidthPx = Integer.MAX_VALUE;
         int minHeightPx = Integer.MAX_VALUE;
         for (WindowBounds bounds : displayInfo.supportedBounds) {
             boolean isTablet = displayInfo.isTablet(bounds);
             if (isTablet && displayInfo.getDeviceType() == TYPE_MULTI_DISPLAY) {
-                // For split displays, take half width per page
+                // For split displays, take half width per page.
                 minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
                 minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
             } else if (!isTablet && bounds.isLandscape()) {
-                // We will use transposed layout in this case
+                // We will use transposed layout in this case.
                 minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
                 minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
             } else {
@@ -700,18 +716,10 @@
             }
         }
 
-        GridDimension selectedRow = null;
-        for (GridDimension item: list) {
-            if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
-                if (selectedRow == null || selectedRow.mNumGridDimension < item.mNumGridDimension) {
-                    selectedRow = item;
-                }
-            }
-        }
-        if (selectedRow != null) {
-            return selectedRow;
-        }
-        return null;
+        int minWidthDp = (int) dpiFromPx(minWidthPx, DisplayMetrics.DENSITY_DEVICE_STABLE);
+        int minHeightDp = (int) dpiFromPx(minHeightPx, DisplayMetrics.DENSITY_DEVICE_STABLE);
+
+        return new int[]{minWidthDp, minHeightDp};
     }
 
     /**
@@ -1237,8 +1245,8 @@
 
     public static final class GridDimension {
         final int mNumGridDimension;
-        final float mMinDeviceWidthPx;
-        final float mMinDeviceHeightPx;
+        final int mMinDeviceWidthDp;
+        final int mMinDeviceHeightDp;
         final String mDbFile;
         final int mDefaultLayoutId;
         final int mDemoModeLayoutId;
@@ -1248,8 +1256,8 @@
             TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridDimension);
 
             mNumGridDimension = (int) a.getFloat(R.styleable.GridDimension_numGridDimension, 0);
-            mMinDeviceWidthPx = a.getFloat(R.styleable.GridDimension_minDeviceWidthPx, 0);
-            mMinDeviceHeightPx = a.getFloat(R.styleable.GridDimension_minDeviceHeightPx, 0);
+            mMinDeviceWidthDp = a.getInt(R.styleable.GridDimension_minDeviceWidthDp, 0);
+            mMinDeviceHeightDp = a.getInt(R.styleable.GridDimension_minDeviceHeightDp, 0);
             mDbFile = a.getString(R.styleable.GridDimension_dbFile);
             mDefaultLayoutId = a.getResourceId(
                     R.styleable.GridDimension_defaultLayoutId, 0);
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 95dbf5f..69a5a83 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -116,6 +116,7 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
+import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.OverlayEdgeEffect;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.RunnableList;
@@ -133,6 +134,8 @@
 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy;
 
+import com.google.android.msdl.data.model.MSDLToken;
+
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -299,6 +302,8 @@
 
     private final StatsLogManager mStatsLogManager;
 
+    private final MSDLPlayerWrapper mMSDLPlayerWrapper;
+
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -331,6 +336,7 @@
         setMotionEventSplittingEnabled(true);
         setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
         mStatsLogManager = StatsLogManager.newInstance(context);
+        mMSDLPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context);
     }
 
     @Override
@@ -2679,7 +2685,9 @@
         ItemInfo info = dragObject.dragInfo;
         boolean userFolderPending = willCreateUserFolder(info, mDragOverView, false);
         if (mDragMode == DRAG_MODE_NONE && userFolderPending) {
-
+            if (Flags.msdlFeedback()) {
+                mMSDLPlayerWrapper.playToken(MSDLToken.DRAG_INDICATOR_DISCRETE);
+            }
             mFolderCreateBg = new PreviewBackground(getContext());
             mFolderCreateBg.setup(mLauncher, mLauncher, null,
                     mDragOverView.getMeasuredWidth(), mDragOverView.getPaddingTop());