Merge "Add flag for expressive dismiss task motion in Overview." into main
diff --git a/quickstep/res/drawable/task_header_close_button.xml b/quickstep/res/drawable/task_header_close_button.xml
new file mode 100644
index 0000000..b409158
--- /dev/null
+++ b/quickstep/res/drawable/task_header_close_button.xml
@@ -0,0 +1,24 @@
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="18dp"
+    android:height="18dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
+</vector>
diff --git a/quickstep/res/drawable/task_thumbnail_header_bg.xml b/quickstep/res/drawable/task_thumbnail_header_bg.xml
new file mode 100644
index 0000000..52ac1ae
--- /dev/null
+++ b/quickstep/res/drawable/task_thumbnail_header_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2025 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/materialColorSurfaceBright" />
+    <corners android:topLeftRadius="@dimen/task_thumbnail_header_round_corner_radius"
+        android:topRightRadius="@dimen/task_thumbnail_header_round_corner_radius"/>
+</shape>
diff --git a/quickstep/res/layout/task_thumbnail_view_header.xml b/quickstep/res/layout/task_thumbnail_view_header.xml
new file mode 100644
index 0000000..ecc1559
--- /dev/null
+++ b/quickstep/res/layout/task_thumbnail_view_header.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2025 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.quickstep.views.TaskThumbnailViewHeader
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/task_thumbnail_header_bg">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/task_thumbnail_header_height"
+        android:layout_marginStart="@dimen/task_thumbnail_header_margin_edge"
+        android:layout_marginEnd="@dimen/task_thumbnail_header_margin_edge"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+        <ImageView
+            android:id="@+id/header_app_icon"
+            android:contentDescription="@string/header_app_icon_description"
+            android:layout_width="@dimen/task_thumbnail_header_icon_size"
+            android:layout_height="@dimen/task_thumbnail_header_icon_size"
+            android:layout_marginEnd="@dimen/task_thumbnail_header_margin_between_views"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/header_app_title"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintHorizontal_bias="0"
+            app:layout_constraintVertical_bias="0.5"
+            app:layout_constraintHorizontal_chainStyle="spread_inside" />
+        <TextView
+            android:id="@+id/header_app_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/task_thumbnail_header_margin_between_views"
+            android:layout_marginEnd="@dimen/task_thumbnail_header_margin_between_views"
+            android:text="@string/header_default_app_title"
+            app:layout_constraintStart_toEndOf="@id/header_app_icon"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintHorizontal_bias="0"
+            app:layout_constraintVertical_bias="0.5" />
+        <ImageButton
+            android:id="@+id/header_close_button"
+            android:contentDescription="@string/header_close_icon_description"
+            android:layout_width="@dimen/task_thumbnail_header_icon_size"
+            android:layout_height="@dimen/task_thumbnail_header_icon_size"
+            android:layout_marginStart="@dimen/task_thumbnail_header_margin_between_views"
+            android:src="@drawable/task_header_close_button"
+            android:tint="@android:color/darker_gray"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintVertical_bias="0.5" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</com.android.quickstep.views.TaskThumbnailViewHeader>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index de0b2c7..e57e650 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -84,6 +84,12 @@
     <!--  The size of the icon menu's icon touch target  -->
     <dimen name="task_thumbnail_icon_menu_drawable_touch_size">44dp</dimen>
     <dimen name="task_thumbnail_icon_menu_elevation">4dp</dimen>
+    <!--  The size of the task thumbnail header  -->
+    <dimen name="task_thumbnail_header_height">30dp</dimen>
+    <dimen name="task_thumbnail_header_margin_edge">18dp</dimen>
+    <dimen name="task_thumbnail_header_margin_between_views">9dp</dimen>
+    <dimen name="task_thumbnail_header_icon_size">18dp</dimen>
+    <dimen name="task_thumbnail_header_round_corner_radius">16dp</dimen>
 
     <dimen name="task_icon_cache_default_icon_size">72dp</dimen>
     <item name="overview_modal_max_scale" format="float" type="dimen">1.1</item>
@@ -465,6 +471,7 @@
     <dimen name="bubblebar_icon_spacing_persistent_taskbar">@dimen/bubblebar_icon_spacing</dimen>
     <dimen name="bubblebar_expanded_icon_spacing">12dp</dimen>
     <dimen name="bubblebar_icon_elevation">1dp</dimen>
+    <dimen name="bubblebar_transient_taskbar_min_distance">12dp</dimen>
 
     <!-- Bubble bar dismiss view -->
     <dimen name="bubblebar_dismiss_target_size">@dimen/floating_dismiss_background_size</dimen>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index f126568..324ea31 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -357,4 +357,12 @@
     <!-- Name of Google's new feature to circle to search anything on your phone screen, without
      switching apps. [CHAR_LIMIT=60] -->
     <string name="search_gesture_feature_title">Circle to Search</string>
+
+    <!-- Strings for task thumbnail header in Overview -->
+    <!-- Content description for the header app icon. [CHAR LIMIT=NONE] -->
+    <string name="header_app_icon_description">App icon</string>
+    <!-- Default app title for a task view in Overview. [CHAR LIMIT=NONE] -->
+    <string name="header_default_app_title">App title</string>
+    <!-- Content description for the header close button. [CHAR LIMIT=NONE] -->
+    <string name="header_close_icon_description">Close button</string>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index ee9c6a1..2745129 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -140,6 +140,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SettingsCache;
@@ -194,6 +195,7 @@
     private WindowManager.LayoutParams mWindowLayoutParams;
     private WindowManager.LayoutParams mLastUpdatedLayoutParams;
     private boolean mIsFullscreen;
+    private boolean mIsNotificationShadeExpanded = false;
     // The size we should return to when we call setTaskbarWindowFullscreen(false)
     private int mLastRequestedNonFullscreenSize;
     /**
@@ -269,8 +271,7 @@
         mWindowManager = c.getSystemService(WindowManager.class);
 
         // Inflate views.
-        final boolean isTransientTaskbar = DisplayController.isTransientTaskbar(this)
-                && !isPhoneMode();
+        boolean isTransientTaskbar = isTransientTaskbar();
         int taskbarLayout = isTransientTaskbar ? R.layout.transient_taskbar : R.layout.taskbar;
         mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
         TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
@@ -383,6 +384,11 @@
         dispatchDeviceProfileChanged();
     }
 
+    /** Returns whether current taskbar is transient. */
+    public boolean isTransientTaskbar() {
+        return DisplayController.isTransientTaskbar(this) && !isPhoneMode();
+    }
+
     /**
      * Copy the original DeviceProfile, match the number of hotseat icons and qsb width and update
      * the icon size
@@ -1005,6 +1011,8 @@
      * Hides the taskbar icons and background when the notification shade is expanded.
      */
     private void onNotificationShadeExpandChanged(boolean isExpanded, boolean skipAnim) {
+        boolean isExpandedUpdated = isExpanded != mIsNotificationShadeExpanded;
+        mIsNotificationShadeExpanded = isExpanded;
         // Close all floating views within the Taskbar window to make sure nothing is shown over
         // the notification shade.
         if (isExpanded) {
@@ -1018,11 +1026,18 @@
         anim.play(mControllers.taskbarDragLayerController.getNotificationShadeBgTaskbar()
                 .animateToValue(alpha));
 
-        mControllers.bubbleControllers.ifPresent(controllers -> {
-            BubbleBarViewController bubbleBarViewController = controllers.bubbleBarViewController;
-            anim.play(bubbleBarViewController.getBubbleBarAlpha().get(0).animateToValue(alpha));
-        });
-
+        if (isExpandedUpdated) {
+            mControllers.bubbleControllers.ifPresent(controllers -> {
+                BubbleBarViewController bubbleBarViewController =
+                        controllers.bubbleBarViewController;
+                anim.play(bubbleBarViewController.getBubbleBarAlpha().get(0).animateToValue(alpha));
+                MultiPropertyFactory<View>.MultiProperty handleAlpha =
+                        controllers.bubbleStashController.getHandleViewAlpha();
+                if (handleAlpha != null) {
+                    anim.play(handleAlpha.animateToValue(alpha));
+                }
+            });
+        }
         anim.start();
         if (skipAnim) {
             anim.end();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index ea6d82b..e44bce1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -57,6 +57,7 @@
     var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat()
     var translationYForSwipe = 0f
     var translationYForStash = 0f
+    var translationXForBubbleBar = 0f
 
     private val transientBackgroundBounds = context.transientTaskbarBounds
 
@@ -244,12 +245,12 @@
             setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)),
         )
         strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
-
+        val currentTranslationX = translationXForBubbleBar * progress
         lastDrawnTransientRect.set(
-            transientBackgroundBounds.left + halfWidthDelta,
+            transientBackgroundBounds.left + halfWidthDelta + currentTranslationX,
             bottom - newBackgroundHeight,
-            transientBackgroundBounds.right - halfWidthDelta,
-            bottom
+            transientBackgroundBounds.right - halfWidthDelta + currentTranslationX,
+            bottom,
         )
         val horizontalInset = fullWidth * widthInsetPercentage
         lastDrawnTransientRect.inset(horizontalInset, 0f)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 8b52112..59ef577 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -254,6 +254,11 @@
         invalidate();
     }
 
+    protected void setBackgroundTranslationXForBubbleBar(float translationX) {
+        mBackgroundRenderer.setTranslationXForBubbleBar(translationX);
+        invalidate();
+    }
+
     /** Returns the bounds in DragLayer coordinates of where the transient background was drawn. */
     protected RectF getLastDrawnTransientRect() {
         return mBackgroundRenderer.getLastDrawnTransientRect();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 925e10b..68c252a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -198,6 +198,13 @@
     }
 
     /**
+     * Sets the translation of the background for the bubble bar.
+     */
+    public void setTranslationXForBubbleBar(float transX) {
+        mTaskbarDragLayer.setBackgroundTranslationXForBubbleBar(transX);
+    }
+
+    /**
      * Sets the translation of the background during the spring on stash animation.
      */
     public void setTranslationYForStash(float transY) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 60de066..e0be39d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -40,6 +40,7 @@
 import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS;
 import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
+import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_BUBBLE_BAR_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_NAV_BAR_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_PINNING_ANIM;
@@ -81,6 +82,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.TaskItemInfo;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
@@ -109,6 +111,8 @@
 
     private static final Runnable NO_OP = () -> { };
 
+    public static long TRANSLATION_X_FOR_BUBBLEBAR_ANIM_DURATION_MS = 250;
+
     public static final int ALPHA_INDEX_HOME = 0;
     public static final int ALPHA_INDEX_KEYGUARD = 1;
     public static final int ALPHA_INDEX_STASH = 2;
@@ -132,6 +136,7 @@
     private static final int TRANSITION_FADE_OUT_DURATION = 83;
 
     private final TaskbarActivityContext mActivity;
+    private @Nullable TaskbarDragLayerController mDragLayerController;
     private final TaskbarView mTaskbarView;
     private final MultiValueAlpha mTaskbarIconAlpha;
     private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
@@ -144,15 +149,22 @@
             this::updateTaskbarIconsScale);
 
     private final AnimatedFloat mTaskbarIconTranslationXForPinning = new AnimatedFloat(
-            this::updateTaskbarIconTranslationXForPinning);
+            () -> updateTaskbarIconTranslationXForPinning());
 
     private final AnimatedFloat mIconsTranslationXForNavbar = new AnimatedFloat(
             this::updateTranslationXForNavBar);
 
+    private final AnimatedFloat mTranslationXForBubbleBar = new AnimatedFloat(
+            this::updateTranslationXForBubbleBar);
+
     @Nullable
     private Animator mTaskbarShiftXAnim;
     @Nullable
     private BubbleBarLocation mCurrentBubbleBarLocation;
+    @Nullable
+    private BubbleControllers mBubbleControllers = null;
+    @Nullable
+    private ObjectAnimator mTranslationXAnimation;
 
     private final AnimatedFloat mTaskbarIconTranslationYForPinning = new AnimatedFloat(
             this::updateTranslationY);
@@ -174,11 +186,12 @@
     private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener =
             (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                 if (!taskbarRecentsLayoutTransition()) {
-                    updateTaskbarIconTranslationXForPinning();
+                    // update shiftX is handled with the animation at the end of the method
+                    updateTaskbarIconTranslationXForPinning(/* updateShiftXForBubbleBar = */ false);
                 }
-                if (BubbleBarController.isBubbleBarEnabled()) {
-                    mControllers.navbarButtonsViewController.onLayoutsUpdated();
-                }
+                if (mBubbleControllers == null) return;
+                mControllers.navbarButtonsViewController.onLayoutsUpdated();
+                adjustTaskbarXForBubbleBar();
             };
 
     // Animation to align icons with Launcher, created lazily. This allows the controller to be
@@ -222,11 +235,11 @@
         mIsRtl = Utilities.isRtl(mTaskbarView.getResources());
         mTaskbarLeftRightMargin = mActivity.getResources().getDimensionPixelSize(
                 R.dimen.transient_taskbar_padding);
-
     }
 
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        controllers.bubbleControllers.ifPresent(bc -> mBubbleControllers = bc);
         mTaskbarView.init(TaskbarViewCallbacksFactory.newInstance(mActivity).create(
                 mActivity, mControllers, mTaskbarView));
         mTaskbarView.getLayoutParams().height = mActivity.isPhoneMode()
@@ -252,7 +265,7 @@
                 controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
         mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController
                 .getTaskbarNavButtonTranslationYForInAppDisplay();
-
+        mDragLayerController = controllers.taskbarDragLayerController;
         mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
 
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
@@ -270,24 +283,59 @@
     @Override
     public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
         updateCurrentBubbleBarLocation(location);
-        if (!shouldMoveTaskbarOnBubbleBarLocationUpdate()) return;
-        cancelTaskbarShiftAnimation();
-        // reset translation x, taskbar will position icons with the updated location
-        mIconsTranslationXForNavbar.updateValue(0);
-        mTaskbarView.onBubbleBarLocationUpdated(location);
+        if (mActivity.isTransientTaskbar()) {
+            translateTaskbarXForBubbleBar(/* animate= */ false);
+        } else if (mActivity.shouldStartAlignTaskbar()) {
+            cancelTaskbarShiftAnimation();
+            // reset translation x, taskbar will position icons with the updated location
+            mIconsTranslationXForNavbar.updateValue(0);
+            mTaskbarView.onBubbleBarLocationUpdated(location);
+        }
     }
 
     /** Animates start aligned taskbar accordingly to the bubble bar position. */
     @Override
     public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
-        if (!updateCurrentBubbleBarLocation(location)
-                || !shouldMoveTaskbarOnBubbleBarLocationUpdate()) {
-            return;
+        boolean locationUpdated = updateCurrentBubbleBarLocation(location);
+        if (mActivity.isTransientTaskbar()) {
+            translateTaskbarXForBubbleBar(/* animate= */ true);
+        } else if (locationUpdated && mActivity.shouldStartAlignTaskbar()) {
+            cancelTaskbarShiftAnimation();
+            float translationX = mTaskbarView.getTranslationXForBubbleBarPosition(location);
+            mTaskbarShiftXAnim = createTaskbarIconsShiftAnimator(translationX);
+            mTaskbarShiftXAnim.start();
         }
-        cancelTaskbarShiftAnimation();
-        float translationX = mTaskbarView.getTranslationXForBubbleBarPosition(location);
-        mTaskbarShiftXAnim = createTaskbarIconsShiftAnimator(translationX);
-        mTaskbarShiftXAnim.start();
+    }
+
+    private void translateTaskbarXForBubbleBar(boolean animate) {
+        cancelCurrentTranslationXAnimation();
+        if (!mActivity.isTransientTaskbar()) return;
+        int shiftX = getTransientTaskbarShiftXForBubbleBar();
+        if (animate) {
+            mTranslationXAnimation = mTranslationXForBubbleBar.animateToValue(shiftX);
+            mTranslationXAnimation.setInterpolator(EMPHASIZED);
+            mTranslationXAnimation.setDuration(TRANSLATION_X_FOR_BUBBLEBAR_ANIM_DURATION_MS);
+            mTranslationXAnimation.start();
+        } else {
+            mTranslationXForBubbleBar.updateValue(shiftX);
+        }
+    }
+
+    private void cancelCurrentTranslationXAnimation() {
+        if (mTranslationXAnimation != null) {
+            if (mTranslationXAnimation.isRunning()) {
+                mTranslationXAnimation.cancel();
+            }
+            mTranslationXAnimation = null;
+        }
+    }
+
+    private int getTransientTaskbarShiftXForBubbleBar() {
+        if (mBubbleControllers == null || !mActivity.isTransientTaskbar()) {
+            return 0;
+        }
+        return mBubbleControllers.bubbleBarViewController
+                .getTransientTaskbarTranslationXForBubbleBar(mCurrentBubbleBarLocation);
     }
 
     /** Updates the mCurrentBubbleBarLocation, returns {@code} true if location is updated. */
@@ -300,13 +348,6 @@
         }
     }
 
-    /** Returns whether taskbar should be moved on the bubble bar location update. */
-    private boolean shouldMoveTaskbarOnBubbleBarLocationUpdate() {
-        return mControllers.bubbleControllers.isPresent()
-                && mActivity.shouldStartAlignTaskbar()
-                && mActivity.isThreeButtonNav();
-    }
-
     private void cancelTaskbarShiftAnimation() {
         if (mTaskbarShiftXAnim != null) {
             mTaskbarShiftXAnim.cancel();
@@ -450,16 +491,26 @@
     }
 
     void updateTaskbarIconTranslationXForPinning() {
+        updateTaskbarIconTranslationXForPinning(/* updateShiftXForBubbleBar = */ true);
+    }
+
+    void updateTaskbarIconTranslationXForPinning(boolean updateShiftXForBubbleBar) {
         View[] iconViews = mTaskbarView.getIconViews();
         float scale = mTaskbarIconTranslationXForPinning.value;
         float transientTaskbarAllAppsOffset = mActivity.getResources().getDimension(
                 mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(true));
         float persistentTaskbarAllAppsOffset = mActivity.getResources().getDimension(
                 mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(false));
-
+        if (mBubbleControllers != null && updateShiftXForBubbleBar) {
+            cancelCurrentTranslationXAnimation();
+            int translationXForTransientTaskbar = mBubbleControllers.bubbleBarViewController
+                    .getTransientTaskbarTranslationXForBubbleBar(mCurrentBubbleBarLocation);
+            float currentTranslationXForTransientTaskbar = mapRange(scale,
+                    translationXForTransientTaskbar, 0);
+            mTranslationXForBubbleBar.updateValue(currentTranslationXForTransientTaskbar);
+        }
         float allAppIconTranslateRange = mapRange(scale, transientTaskbarAllAppsOffset,
                 persistentTaskbarAllAppsOffset);
-
         // Task icons are laid out so the taskbar content is centered. The taskbar width (used for
         // centering taskbar icons) depends on the all apps button X translation, and is different
         // for persistent and transient taskbar. If the offset used for current taskbar layout is
@@ -551,13 +602,23 @@
     }
 
     private void updateTranslationXForNavBar() {
+        updateIconViewsTranslationX(INDEX_NAV_BAR_ANIM, mIconsTranslationXForNavbar.value);
+    }
+
+    private void updateTranslationXForBubbleBar() {
+        float translationX = mTranslationXForBubbleBar.value;
+        updateIconViewsTranslationX(INDEX_BUBBLE_BAR_ANIM, translationX);
+        if (mDragLayerController != null) {
+            mDragLayerController.setTranslationXForBubbleBar(translationX);
+        }
+    }
+
+    private void updateIconViewsTranslationX(int translationXChannel, float translationX) {
         View[] iconViews = mTaskbarView.getIconViews();
-        float translationX = mIconsTranslationXForNavbar.value;
-        for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) {
-            View iconView = iconViews[iconIndex];
+        for (View iconView : iconViews) {
             MultiTranslateDelegate translateDelegate =
                     ((Reorderable) iconView).getTranslateDelegate();
-            translateDelegate.getTranslationX(INDEX_NAV_BAR_ANIM).setValue(translationX);
+            translateDelegate.getTranslationX(translationXChannel).setValue(translationX);
         }
     }
 
@@ -811,6 +872,13 @@
         if (mTaskbarView.updateMaxNumIcons()) {
             commitRunningAppsToUI();
         }
+        adjustTaskbarXForBubbleBar();
+    }
+
+    private void adjustTaskbarXForBubbleBar() {
+        if (mBubbleControllers != null && mActivity.isTransientTaskbar()) {
+            translateTaskbarXForBubbleBar(/* animate= */ true);
+        }
     }
 
     /**
@@ -841,7 +909,17 @@
         setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, interpolator);
         setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, interpolator);
         setter.setFloat(mTaskbarNavButtonTranslationYForInAppDisplay, VALUE, offsetY, interpolator);
-
+        if (mBubbleControllers != null
+                && mCurrentBubbleBarLocation != null
+                && mActivity.isTransientTaskbar()) {
+            int offsetX = mBubbleControllers.bubbleBarViewController
+                    .getTransientTaskbarTranslationXForBubbleBar(mCurrentBubbleBarLocation);
+            if (offsetX != 0) {
+                // if taskbar should be adjusted for the bubble bar adjust the taskbar translation
+                mTranslationXForBubbleBar.updateValue(offsetX);
+                setter.setFloat(mTranslationXForBubbleBar, VALUE, 0, interpolator);
+            }
+        }
         int collapsedHeight = mActivity.getDefaultTaskbarWindowSize();
         int expandedHeight = Math.max(collapsedHeight, taskbarDp.taskbarHeight + offsetY);
         setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowSize(
@@ -1042,8 +1120,8 @@
     }
 
     private boolean bubbleBarHasBubbles() {
-        return mControllers.bubbleControllers.isPresent()
-                && mControllers.bubbleControllers.get().bubbleBarViewController.hasBubbles();
+        return mBubbleControllers != null
+                && mBubbleControllers.bubbleBarViewController.hasBubbles();
     }
 
     public void onRotationChanged(DeviceProfile deviceProfile) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 2d4d279..c001123 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -1339,6 +1339,14 @@
         return getScaledIconSize() + mIconOverlapAmount + 2 * mBubbleBarPadding;
     }
 
+    float getCollapsedWidthForIconSizeAndPadding(int iconSize, int bubbleBarPadding) {
+        final int bubbleChildCount = Math.min(getBubbleChildCount(), MAX_VISIBLE_BUBBLES_COLLAPSED);
+        if (bubbleChildCount == 0) return 0;
+        final int spacesCount = bubbleChildCount - 1;
+        final float horizontalPadding = 2 * bubbleBarPadding;
+        return iconSize * bubbleChildCount + mIconOverlapAmount * spacesCount + horizontalPadding;
+    }
+
     /** Returns the child count excluding the overflow if it's present. */
     int getBubbleChildCount() {
         return hasOverflow() ? getChildCount() - 1 : getChildCount();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0b627d2..afbc932 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -122,7 +122,8 @@
     private float mBubbleBarSwipeUpTranslationY;
     // Modified when bubble bar is springing back into the stash handle.
     private float mBubbleBarStashTranslationY;
-
+    // Minimum distance between the BubbleBar and the taskbar
+    private final int mBubbleBarTaskbarMinDistance;
     // Whether the bar is hidden for a sysui state.
     private boolean mHiddenForSysui;
     // Whether the bar is hidden because there are no bubbles.
@@ -150,10 +151,11 @@
         mBubbleBarContainer = bubbleBarContainer;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
-        mIconSize = activity.getResources().getDimensionPixelSize(
-                R.dimen.bubblebar_icon_size);
-        mDragElevation = activity.getResources().getDimensionPixelSize(
-                R.dimen.bubblebar_drag_elevation);
+        Resources res = activity.getResources();
+        mIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+        mBubbleBarTaskbarMinDistance = res.getDimensionPixelSize(
+                R.dimen.bubblebar_transient_taskbar_min_distance);
+        mDragElevation = res.getDimensionPixelSize(R.dimen.bubblebar_drag_elevation);
         mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity);
     }
 
@@ -664,6 +666,45 @@
         }
     }
 
+    /**
+     * Returns the translation X of the transient taskbar according to the bubble bar location
+     * regardless of the current taskbar mode.
+     */
+    public int getTransientTaskbarTranslationXForBubbleBar(BubbleBarLocation location) {
+        int taskbarShift = 0;
+        if (!isBubbleBarVisible() || mTaskbarViewPropertiesProvider == null) return taskbarShift;
+        Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds();
+        if (taskbarViewBounds.isEmpty()) return taskbarShift;
+        int actualDistance =
+                getDistanceBetweenTransientTaskbarAndBubbleBar(location, taskbarViewBounds);
+        if (actualDistance < mBubbleBarTaskbarMinDistance) {
+            taskbarShift = mBubbleBarTaskbarMinDistance - actualDistance;
+            if (!location.isOnLeft(mBarView.isLayoutRtl())) {
+                taskbarShift = -taskbarShift;
+            }
+        }
+        return taskbarShift;
+    }
+
+    private int getDistanceBetweenTransientTaskbarAndBubbleBar(BubbleBarLocation location,
+            Rect taskbarViewBounds) {
+        Resources res = mActivity.getResources();
+        DeviceProfile transientDp = mActivity.getTransientTaskbarDeviceProfile();
+        int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res, transientDp);
+        int transientPadding = getBubbleBarPaddingFromDeviceProfile(res, transientDp);
+        int transientWidthWithMargin = (int) (mBarView.getCollapsedWidthForIconSizeAndPadding(
+                transientIconSize, transientPadding) + mBarView.getHorizontalMargin());
+        int distance;
+        if (location.isOnLeft(mBarView.isLayoutRtl())) {
+            distance = taskbarViewBounds.left - transientWidthWithMargin;
+        } else {
+            int displayWidth = res.getDisplayMetrics().widthPixels;
+            int bubbleBarLeft = displayWidth - transientWidthWithMargin;
+            distance = bubbleBarLeft - taskbarViewBounds.right;
+        }
+        return distance;
+    }
+
     //
     // Modifying view related properties.
     //
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 595dac3..fec1eaf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -24,6 +24,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.util.MultiPropertyFactory
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 import java.io.PrintWriter
@@ -172,6 +173,9 @@
     /** Returns bounds of the handle */
     fun getHandleBounds(bounds: Rect)
 
+    /** Returns MultiValueAlpha of the handle view when the handle view is shown. */
+    fun getHandleViewAlpha(): MultiPropertyFactory<View>.MultiProperty? = null
+
     /**
      * Returns bubble bar Y position according to [isBubblesShowingOnHome] and
      * [isBubblesShowingOnOverview] values. Default implementation only analyse
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 3e3f569..9c148e2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -512,6 +512,14 @@
         }
     }
 
+    override fun getHandleViewAlpha(): MultiPropertyFactory<View>.MultiProperty? =
+        // only return handle alpha if the bubble bar is stashed and has bubbles
+        if (isStashed && bubbleBarViewController.hasBubbles()) {
+            stashHandleViewAlpha
+        } else {
+            null
+        }
+
     private fun Animator.updateTouchRegionOnAnimationEnd(): Animator {
         doOnEnd { onIsStashedChanged() }
         return this
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 505f2cb..e7e9f51 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -17,7 +17,6 @@
 package com.android.quickstep.fallback.window
 
 import android.content.Context
-import android.os.Handler
 import android.util.Log
 import android.view.Display
 import com.android.launcher3.Flags
@@ -25,7 +24,7 @@
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.DaggerSingletonTracker
-import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors
 import com.android.quickstep.DisplayModel
 import com.android.quickstep.FallbackWindowInterface
 import com.android.quickstep.dagger.QuickstepBaseAppComponent
@@ -51,15 +50,13 @@
 
     init {
         if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
-            MAIN_EXECUTOR.execute {
-                displayManager.registerDisplayListener(displayListener, Handler.getMain())
-                // In the scenario where displays were added before this display listener was
-                // registered, we should store the RecentsDisplayResources for those displays
-                // directly.
-                displayManager.displays
-                    .filter { getDisplayResource(it.displayId) == null }
-                    .forEach { storeRecentsDisplayResource(it.displayId, it) }
-            }
+            displayManager.registerDisplayListener(displayListener, Executors.MAIN_EXECUTOR.handler)
+            // In the scenario where displays were added before this display listener was
+            // registered, we should store the RecentsDisplayResources for those displays
+            // directly.
+            displayManager.displays
+                .filter { getDisplayResource(it.displayId) == null }
+                .forEach { storeRecentsDisplayResource(it.displayId, it) }
             tracker.addCloseable { destroy() }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index f5bef05e..afe988d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -242,7 +242,7 @@
 
     private void cancelLongPress(String reason) {
         if (DEBUG_NAV_HANDLE) {
-            Log.d(TAG, "cancelLongPress");
+            Log.d(TAG, "cancelLongPress: " + reason);
         }
         mGestureState.setIsInExtendedSlopRegion(false);
         MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 870a479..f33eb5e 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -293,15 +293,15 @@
                 float upDist = -displacement;
                 boolean isTrackpadGesture = mGestureState.isTrackpadGesture();
                 float squaredHypot = squaredHypot(displacementX, displacementY);
-                boolean isInExtendedSlopRegion = !mGestureState.isInExtendedSlopRegion();
+                boolean isInExtendedSlopRegion = mGestureState.isInExtendedSlopRegion();
                 boolean passedSlop = isTrackpadGesture
                         || (squaredHypot >= mSquaredTouchSlop
-                        && isInExtendedSlopRegion);
+                        && !isInExtendedSlopRegion);
                 if (DEBUG) {
                     Log.d(TAG, "ACTION_MOVE: passedSlop=" + passedSlop
                             + " ( " + isTrackpadGesture
                             + " || (" + squaredHypot + " >= " + mSquaredTouchSlop
-                            + " && " + isInExtendedSlopRegion + " ))");
+                            + " && " + !isInExtendedSlopRegion + " ))");
                 }
 
                 if (!mPassedSlopOnThisGesture && passedSlop) {
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
index d2cb595..0ee2bd2 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt
@@ -20,7 +20,6 @@
  * Container to hold [com.android.launcher3.DeviceProfile] related to Recents.
  *
  * @property isLargeScreen whether the current device posture has a large screen
+ * @property canEnterDesktopMode whether the current device can enter Desktop UI mode
  */
-data class RecentsDeviceProfile(
-    val isLargeScreen: Boolean,
-)
+data class RecentsDeviceProfile(val isLargeScreen: Boolean, val canEnterDesktopMode: Boolean)
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
index c64453d..8450f09 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.recents.data
 
 import com.android.quickstep.views.RecentsViewContainer
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 
 /**
  * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains
@@ -26,5 +27,10 @@
     RecentsDeviceProfileRepository {
 
     override fun getRecentsDeviceProfile() =
-        with(container.deviceProfile) { RecentsDeviceProfile(isLargeScreen = isTablet) }
+        with(container.deviceProfile) {
+            RecentsDeviceProfile(
+                isLargeScreen = isTablet,
+                canEnterDesktopMode = DesktopModeStatus.canEnterDesktopMode(container.asContext()),
+            )
+        }
 }
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index dd83af6..2b364f9 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -182,6 +182,7 @@
                         dispatcherProvider = inject(),
                         getThumbnailPositionUseCase = inject(),
                         tasksRepository = inject(),
+                        deviceProfileRepository = inject(),
                         splashAlphaUseCase = inject(scopeId),
                     )
                 TaskOverlayViewModel::class.java -> {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 36a86f2..6118544 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -24,18 +24,35 @@
 sealed class TaskThumbnailUiState {
     data object Uninitialized : TaskThumbnailUiState()
 
-    data object LiveTile : TaskThumbnailUiState()
-
     data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
 
-    data class SnapshotSplash(
-        val snapshot: Snapshot,
-        val splash: Drawable?,
-    ) : TaskThumbnailUiState()
+    data class SnapshotSplash(val snapshot: Snapshot, val splash: Drawable?) :
+        TaskThumbnailUiState()
 
-    data class Snapshot(
-        val bitmap: Bitmap,
-        @Surface.Rotation val thumbnailRotation: Int,
-        @ColorInt val backgroundColor: Int
-    )
+    sealed class LiveTile : TaskThumbnailUiState() {
+        data class WithHeader(val header: ThumbnailHeader) : LiveTile()
+
+        data object WithoutHeader : LiveTile()
+    }
+
+    sealed class Snapshot {
+        abstract val bitmap: Bitmap
+        abstract val thumbnailRotation: Int
+        abstract val backgroundColor: Int
+
+        data class WithHeader(
+            override val bitmap: Bitmap,
+            @Surface.Rotation override val thumbnailRotation: Int,
+            @ColorInt override val backgroundColor: Int,
+            val header: ThumbnailHeader,
+        ) : Snapshot()
+
+        data class WithoutHeader(
+            override val bitmap: Bitmap,
+            @Surface.Rotation override val thumbnailRotation: Int,
+            @ColorInt override val backgroundColor: Int,
+        ) : Snapshot()
+    }
+
+    data class ThumbnailHeader(val icon: Drawable, val title: String)
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index b040723..4a78729 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -22,11 +22,13 @@
 import android.graphics.Rect
 import android.util.AttributeSet
 import android.util.Log
+import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewOutlineProvider
 import android.widget.FrameLayout
 import androidx.annotation.ColorInt
 import androidx.core.view.isInvisible
+import com.android.launcher3.Flags.enableDesktopExplodedView
 import com.android.launcher3.R
 import com.android.launcher3.util.ViewPool
 import com.android.launcher3.util.coroutines.DispatcherProvider
@@ -39,6 +41,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.views.FixedSizeImageView
+import com.android.quickstep.views.TaskThumbnailViewHeader
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -66,6 +69,8 @@
     private val splashBackground: View by lazy { findViewById(R.id.splash_background) }
     private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) }
 
+    private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
+
     private var uiState: TaskThumbnailUiState = Uninitialized
 
     private val bounds = Rect()
@@ -86,6 +91,12 @@
         defStyleAttr: Int,
     ) : super(context, attrs, defStyleAttr)
 
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+
+        maybeCreateHeader()
+    }
+
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
         viewAttachedScope =
@@ -102,7 +113,7 @@
                 resetViews()
                 when (viewModelUiState) {
                     is Uninitialized -> {}
-                    is LiveTile -> drawLiveWindow()
+                    is LiveTile -> drawLiveWindow(viewModelUiState)
                     is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
                     is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
                 }
@@ -179,14 +190,20 @@
         splashIcon.alpha = 0f
         scrimView.alpha = 0f
         setBackgroundColor(Color.BLACK)
+        taskThumbnailViewHeader?.isInvisible = true
     }
 
     private fun drawBackground(@ColorInt background: Int) {
         setBackgroundColor(background)
     }
 
-    private fun drawLiveWindow() {
+    private fun drawLiveWindow(liveTile: LiveTile) {
         liveTileView.isInvisible = false
+
+        if (liveTile is LiveTile.WithHeader) {
+            taskThumbnailViewHeader?.isInvisible = false
+            taskThumbnailViewHeader?.setHeader(liveTile.header)
+        }
     }
 
     private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) {
@@ -197,6 +214,11 @@
     }
 
     private fun drawSnapshot(snapshot: Snapshot) {
+        if (snapshot is Snapshot.WithHeader) {
+            taskThumbnailViewHeader?.isInvisible = false
+            taskThumbnailViewHeader?.setHeader(snapshot.header)
+        }
+
         drawBackground(snapshot.backgroundColor)
         thumbnailView.setImageBitmap(snapshot.bitmap)
         thumbnailView.isInvisible = false
@@ -210,4 +232,14 @@
     private companion object {
         const val TAG = "TaskThumbnailView"
     }
+
+    private fun maybeCreateHeader() {
+        if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
+            taskThumbnailViewHeader =
+                LayoutInflater.from(context)
+                    .inflate(R.layout.task_thumbnail_view_header, this, false)
+                    as TaskThumbnailViewHeader
+            addView(taskThumbnailViewHeader)
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index b5b2fc9..a154c3c 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -18,11 +18,14 @@
 
 import android.annotation.ColorInt
 import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.graphics.Matrix
 import android.util.Log
 import androidx.core.graphics.ColorUtils
+import com.android.launcher3.Flags.enableDesktopExplodedView
 import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState
 import com.android.quickstep.recents.viewmodel.RecentsViewData
@@ -32,6 +35,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.systemui.shared.recents.model.Task
 import kotlin.math.max
@@ -51,6 +55,7 @@
     taskContainerData: TaskContainerData,
     dispatcherProvider: DispatcherProvider,
     private val tasksRepository: RecentTasksRepository,
+    private val deviceProfileRepository: RecentsDeviceProfileRepository,
     private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
     private val splashAlphaUseCase: SplashAlphaUseCase,
 ) : TaskThumbnailViewModel {
@@ -90,7 +95,7 @@
                 //                )
                 when {
                     taskVal == null -> Uninitialized
-                    isRunning -> LiveTile
+                    isRunning -> createLiveTileState(taskVal)
                     isBackgroundOnly(taskVal) ->
                         BackgroundOnly(taskVal.colorBackground.removeAlpha())
                     isSnapshotSplashState(taskVal) ->
@@ -129,7 +134,46 @@
     private fun createSnapshotState(task: Task): Snapshot {
         val thumbnailData = task.thumbnail
         val bitmap = thumbnailData?.thumbnail!!
-        return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
+        var thumbnailHeader = maybeCreateHeader(task)
+        return if (thumbnailHeader != null)
+            Snapshot.WithHeader(
+                bitmap,
+                thumbnailData.rotation,
+                task.colorBackground.removeAlpha(),
+                thumbnailHeader,
+            )
+        else
+            Snapshot.WithoutHeader(
+                bitmap,
+                thumbnailData.rotation,
+                task.colorBackground.removeAlpha(),
+            )
+    }
+
+    private fun shouldHaveThumbnailHeader(task: Task): Boolean {
+        return deviceProfileRepository.getRecentsDeviceProfile().canEnterDesktopMode &&
+            enableDesktopExplodedView() &&
+            task.key.windowingMode == WINDOWING_MODE_FREEFORM
+    }
+
+    private fun maybeCreateHeader(task: Task): ThumbnailHeader? {
+        // Header is only needed when this task is a desktop task and Overivew exploded view is
+        // enabled.
+        if (!shouldHaveThumbnailHeader(task)) {
+            return null
+        }
+
+        // TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
+        // null.
+        val icon = task.icon ?: return null
+        val titleDescription = task.titleDescription ?: return null
+        return ThumbnailHeader(icon, titleDescription)
+    }
+
+    private fun createLiveTileState(task: Task): LiveTile {
+        val thumbnailHeader = maybeCreateHeader(task)
+        return if (thumbnailHeader != null) LiveTile.WithHeader(thumbnailHeader)
+        else LiveTile.WithoutHeader
     }
 
     @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
new file mode 100644
index 0000000..9eb294a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.launcher3.R
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
+
+class TaskThumbnailViewHeader
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
+
+    private val headerTitleView: TextView by lazy { findViewById(R.id.header_app_title) }
+    private val headerIconView: ImageView by lazy { findViewById(R.id.header_app_icon) }
+
+    fun setHeader(header: ThumbnailHeader) {
+        headerTitleView.setText(header.title)
+        headerIconView.setImageDrawable(header.icon)
+    }
+}
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 f642345..b24926b 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
@@ -507,6 +507,45 @@
         assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT)
     }
 
+    @Test
+    fun getHandleViewAlpha_stashedHasBubbles_alphaPropertyReturned() {
+        // Given BubbleBar is stashed and has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        mTransientBubbleStashController.isStashed = true
+
+        // When handle view alpha property
+        val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha()
+
+        // Then the stash handle alpha property should not be null
+        assertThat(alphaProperty).isNotNull()
+    }
+
+    @Test
+    fun getHandleViewAlpha_stashedHasNoBubblesBar_alphaPropertyIsNull() {
+        // Given BubbleBar is stashed and has no bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        mTransientBubbleStashController.isStashed = true
+
+        // When handle view alpha property
+        val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha()
+
+        // Then the stash handle alpha property should be null
+        assertThat(alphaProperty).isNull()
+    }
+
+    @Test
+    fun getHandleViewAlpha_unstashedHasBubbles_alphaPropertyIsNull() {
+        // Given BubbleBar is not stashed and has bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+        mTransientBubbleStashController.isStashed = false
+
+        // When handle view alpha property
+        val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha()
+
+        // Then the stash handle alpha property should be null
+        assertThat(alphaProperty).isNull()
+    }
+
     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/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index 0738336..2b337c7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -17,6 +17,12 @@
 package com.android.quickstep
 
 import android.graphics.PointF
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManagerGlobal
+import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+import android.view.DisplayInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.R
@@ -39,6 +45,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -63,8 +70,21 @@
     private val flingSpeed =
         -(sandboxContext.resources.getDimension(R.dimen.quickstep_fling_threshold_speed) + 1)
 
+    private val displayManager: DisplayManager =
+        sandboxContext.spyService(DisplayManager::class.java)
+
     @Before
     fun setup() {
+        val display =
+            Display(
+                DisplayManagerGlobal.getInstance(),
+                DEFAULT_DISPLAY,
+                DisplayInfo(),
+                DEFAULT_DISPLAY_ADJUSTMENTS,
+            )
+        whenever(displayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(display)
+        whenever(displayManager.displays).thenReturn(arrayOf(display))
+
         sandboxContext.initDaggerComponent(
             DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
         )
@@ -72,8 +92,10 @@
             RotationTouchHelper.INSTANCE,
             mock(RotationTouchHelper::class.java),
         )
-        val deviceState = mock(RecentsAnimationDeviceState::class.java)
-        sandboxContext.putObject(RecentsAnimationDeviceState.INSTANCE, deviceState)
+        sandboxContext.putObject(
+            RecentsAnimationDeviceState.INSTANCE,
+            mock(RecentsAnimationDeviceState::class.java),
+        )
 
         gestureState = spy(GestureState(OverviewComponentObserver.INSTANCE.get(sandboxContext), 0))
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
index fc2f029..4e90903 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt
@@ -18,9 +18,7 @@
 
 class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository {
     private var recentsDeviceProfile =
-        RecentsDeviceProfile(
-            isLargeScreen = false,
-        )
+        RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false)
 
     override fun getRecentsDeviceProfile() = recentsDeviceProfile
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
deleted file mode 100644
index abe4142..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImplTest.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.recents.data
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.FakeInvariantDeviceProfileTest
-import com.android.quickstep.views.RecentsViewContainer
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [RecentsDeviceProfileRepositoryImpl] */
-@RunWith(AndroidJUnit4::class)
-class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
-    private val recentsViewContainer = mock<RecentsViewContainer>()
-
-    private val systemUnderTest = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
-
-    @Test
-    fun deviceProfileMappedCorrectly() {
-        initializeVarsForTablet()
-        val tabletDeviceProfile = newDP()
-        whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
-
-        assertThat(systemUnderTest.getRecentsDeviceProfile())
-            .isEqualTo(RecentsDeviceProfile(isLargeScreen = true))
-    }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index a777bd4..a956c9c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -16,16 +16,23 @@
 
 package com.android.quickstep.task.thumbnail
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.content.ComponentName
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.Color
 import android.graphics.Matrix
 import android.graphics.drawable.Drawable
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.view.Surface
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
 import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
 import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfile
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
 import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -34,6 +41,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.ThumbnailHeader
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
@@ -44,6 +52,7 @@
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
@@ -52,6 +61,8 @@
 /** Test for [TaskThumbnailView] */
 @RunWith(AndroidJUnit4::class)
 class TaskThumbnailViewModelImplTest {
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
     private val dispatcher = StandardTestDispatcher()
     private val testScope = TestScope(dispatcher)
 
@@ -59,6 +70,7 @@
     private val taskContainerData = TaskContainerData()
     private val dispatcherProvider = TestDispatcherProvider(dispatcher)
     private val tasksRepository = FakeTasksRepository()
+    private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
     private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
     private val splashAlphaUseCase: SplashAlphaUseCase = mock()
 
@@ -68,12 +80,18 @@
             taskContainerData,
             dispatcherProvider,
             tasksRepository,
+            deviceProfileRepository,
             mGetThumbnailPositionUseCase,
             splashAlphaUseCase,
         )
     }
 
-    private val tasks = (0..5).map(::createTaskWithId)
+    private val fullscreenTaskIdRange: IntRange = 0..5
+    private val freeformTaskIdRange: IntRange = 6..10
+
+    private val fullscreenTasks = fullscreenTaskIdRange.map(::createTaskWithId)
+    private val freeformTasks = freeformTaskIdRange.map(::createFreeformTaskWithId)
+    private val tasks = fullscreenTasks + freeformTasks
 
     @Test
     fun initialStateIsUninitialized() =
@@ -88,7 +106,7 @@
             recentsViewData.runningTaskIds.value = setOf(taskId)
             systemUnderTest.bind(taskId)
 
-            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
         }
 
     @Test
@@ -108,7 +126,7 @@
             assertThat(systemUnderTest.uiState.first())
                 .isEqualTo(
                     SnapshotSplash(
-                        Snapshot(
+                        Snapshot.WithoutHeader(
                             backgroundColor = Color.rgb(1, 1, 1),
                             bitmap = expectedThumbnailData.thumbnail!!,
                             thumbnailRotation = Surface.ROTATION_0,
@@ -127,7 +145,7 @@
             tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
             recentsViewData.runningTaskIds.value = setOf(runningTaskId)
             systemUnderTest.bind(runningTaskId)
-            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile.WithoutHeader)
 
             systemUnderTest.bind(stoppedTaskId)
             assertThat(systemUnderTest.uiState.first())
@@ -175,7 +193,7 @@
             assertThat(systemUnderTest.uiState.first())
                 .isEqualTo(
                     SnapshotSplash(
-                        Snapshot(
+                        Snapshot.WithoutHeader(
                             backgroundColor = Color.rgb(2, 2, 2),
                             bitmap = expectedThumbnailData.thumbnail!!,
                             thumbnailRotation = Surface.ROTATION_270,
@@ -202,7 +220,7 @@
             assertThat(systemUnderTest.uiState.first())
                 .isEqualTo(
                     SnapshotSplash(
-                        Snapshot(
+                        Snapshot.WithoutHeader(
                             backgroundColor = Color.rgb(2, 2, 2),
                             bitmap = expectedThumbnailData.thumbnail!!,
                             thumbnailRotation = Surface.ROTATION_0,
@@ -213,6 +231,57 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+    fun bindRunningTask_inDesktop_thenStateIs_LiveTile_withHeader() =
+        testScope.runTest {
+            deviceProfileRepository.setRecentsDeviceProfile(
+                RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)
+            )
+
+            val taskId = freeformTaskIdRange.first
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "Task $taskId", expectedIconData)
+            tasksRepository.seedTasks(freeformTasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            recentsViewData.runningTaskIds.value = setOf(taskId)
+            systemUnderTest.bind(taskId)
+
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(LiveTile.WithHeader(ThumbnailHeader(expectedIconData, "Task $taskId")))
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW)
+    fun bindStoppedTaskWithThumbnail_inDesktop_thenStateIs_SnapshotSplash_withHeader() =
+        testScope.runTest {
+            deviceProfileRepository.setRecentsDeviceProfile(
+                RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)
+            )
+
+            val taskId = freeformTaskIdRange.first
+            val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_0)
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "Task $taskId", expectedIconData)
+            tasksRepository.seedTasks(freeformTasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot.WithHeader(
+                            backgroundColor = Color.rgb(taskId, taskId, taskId),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_0,
+                            header = ThumbnailHeader(expectedIconData, "Task $taskId"),
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
     fun getSnapshotMatrix_MissingThumbnail() =
         testScope.runTest {
             val taskId = 2
@@ -269,9 +338,38 @@
         }
 
     private fun createTaskWithId(taskId: Int) =
-        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
-            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
-        }
+        Task(
+                Task.TaskKey(
+                    taskId,
+                    WINDOWING_MODE_FULLSCREEN,
+                    Intent(),
+                    ComponentName("", ""),
+                    0,
+                    2000,
+                )
+            )
+            .apply {
+                colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+                titleDescription = "Task $taskId"
+                icon = mock<Drawable>()
+            }
+
+    private fun createFreeformTaskWithId(taskId: Int) =
+        Task(
+                Task.TaskKey(
+                    taskId,
+                    WINDOWING_MODE_FREEFORM,
+                    Intent(),
+                    ComponentName("", ""),
+                    0,
+                    2000,
+                )
+            )
+            .apply {
+                colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+                titleDescription = "Task $taskId"
+                icon = mock<Drawable>()
+            }
 
     private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
         val bitmap = mock<Bitmap>()
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt
new file mode 100644
index 0000000..418d66c
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.launcher3.FakeInvariantDeviceProfileTest
+import com.android.quickstep.recents.data.RecentsDeviceProfile
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() {
+    private val recentsViewContainer: RecentsViewContainer = mock()
+
+    private lateinit var mockitoSession: StaticMockitoSession
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .mockStatic(DesktopModeStatus::class.java)
+                .startMocking()
+        whenever(recentsViewContainer.asContext()).thenReturn(context)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    fun deviceProfileMappedCorrectlyForPhone() {
+        val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
+        initializeVarsForPhone()
+        val phoneDeviceProfile = newDP()
+        whenever(recentsViewContainer.deviceProfile).thenReturn(phoneDeviceProfile)
+
+        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
+        assertThat(deviceProfileRepo.getRecentsDeviceProfile())
+            .isEqualTo(RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false))
+    }
+
+    @Test
+    fun deviceProfileMappedCorrectlyForTablet() {
+        val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer)
+        initializeVarsForTablet()
+        val tabletDeviceProfile = newDP()
+        whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile)
+
+        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+        assertThat(deviceProfileRepo.getRecentsDeviceProfile())
+            .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true))
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index c24e974..08ce5e7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -124,6 +124,7 @@
     }
 
     @Test
+    @ScreenRecordRule.ScreenRecord // b/373417111
     public void testLaunchShortcut_fromTaskbarAllApps() {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
@@ -134,7 +135,6 @@
 
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/349439239
     public void testLaunchAppInSplitscreen_fromTaskbarAllApps() {
         getTaskbar().openAllApps()
                 .getAppIcon(TEST_APP_NAME)
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index e44caa4..7563493 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -681,7 +681,7 @@
         } else {
             // Wrap the main icon in AID
             try (LauncherIcons li = LauncherIcons.obtain(context)) {
-                result = li.wrapToAdaptiveIcon(mainIcon, null);
+                result = li.wrapToAdaptiveIcon(mainIcon);
             }
         }
         if (result == null) {
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 67fe889..f1b461b 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -264,8 +264,7 @@
                 try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
                     // Since we just want the scale, avoid heavy drawing operations
                     Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(
-                            new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null),
-                            null, null, null));
+                            new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null)));
                 }
 
                 // Shrink very tiny bit so that the clip path is smaller than the original bitmap
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/IconShape.kt
index 1377610..53ce33f 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/IconShape.kt
@@ -74,13 +74,11 @@
                 setBounds(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
             }
 
-        normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, AREA_CALC_SIZE, null)
+        normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, AREA_CALC_SIZE)
         return pickBestShape(drawable.iconMask, themeManager.iconState.iconMask)
     }
 
     interface ShapeDelegate {
-        fun enableShapeDetection() = false
-
         fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
 
         fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
@@ -107,8 +105,6 @@
 
         override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) =
             path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW)
-
-        override fun enableShapeDetection() = true
     }
 
     /** Rounded square with [radiusRatio] as a ratio of its half edge size */
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 04d88b0..d142066 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -22,7 +22,6 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -56,8 +55,7 @@
 
     protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize,
             ConcurrentLinkedQueue<LauncherIcons> pool) {
-        super(context, fillResIconDpi, iconBitmapSize,
-                IconShape.INSTANCE.get(context).getShape().enableShapeDetection());
+        super(context, fillResIconDpi, iconBitmapSize);
         mThemeController = ThemeManager.INSTANCE.get(context).getThemeController();
         mPool = pool;
     }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 211c351..0732379 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -120,7 +120,8 @@
             @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
             @NonNull SQLiteDatabase source,
-            boolean isDestNewDb) {
+            boolean isDestNewDb,
+            ModelDelegate modelDelegate) {
 
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
             return true;
@@ -174,7 +175,6 @@
             return true;
         } catch (Exception e) {
             Log.e(TAG, "Error during grid migration", e);
-
             return false;
         } finally {
             Log.v(TAG, "Workspace migration completed in "
@@ -182,6 +182,8 @@
 
             // Save current configuration, so that the migration does not run again.
             destDeviceState.writeToPrefs(context);
+            // Notify if we've migrated successfully
+            modelDelegate.gridMigrationComplete(srcDeviceState, destDeviceState);
         }
     }
 
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 0b12af8..1729153 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -53,6 +53,7 @@
         target: DatabaseHelper,
         source: SQLiteDatabase,
         isDestNewDb: Boolean,
+        modelDelegate: ModelDelegate,
     ) {
         if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
             return
@@ -132,6 +133,9 @@
 
             // Save current configuration, so that the migration does not run again.
             destDeviceState.writeToPrefs(context)
+
+            // Notify if we've migrated successfully
+            modelDelegate.gridMigrationComplete(srcDeviceState, destDeviceState)
         }
     }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 4e57944..44b7e8b 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -438,12 +438,12 @@
         ModelDbController dbController = mApp.getModel().getModelDbController();
         if (Flags.gridMigrationRefactor()) {
             try {
-                dbController.attemptMigrateDb(restoreEventLogger);
+                dbController.attemptMigrateDb(restoreEventLogger, mModelDelegate);
             } catch (Exception e) {
                 FileLog.e(TAG, "Failed to migrate grid", e);
             }
         } else {
-            dbController.tryMigrateDB(restoreEventLogger);
+            dbController.tryMigrateDB(restoreEventLogger, mModelDelegate);
         }
         Log.d(TAG, "loadWorkspace: loading default favorites if necessary");
         dbController.loadDefaultFavoritesIfNecessary();
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index e76391f..0138390 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -361,7 +361,8 @@
     /**
      * Migrates the DB. If the migration failed, it clears the DB.
      */
-    public void attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger) throws Exception {
+    public void attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger,
+            ModelDelegate modelDelegate) throws Exception {
         createDbIfNotExists();
 
         if (shouldResetDb()) {
@@ -389,7 +390,7 @@
             boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
             GridSizeMigrationLogic gridSizeMigrationLogic = new GridSizeMigrationLogic();
             gridSizeMigrationLogic.migrateGrid(mContext, srcDeviceState, destDeviceState,
-                    mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb);
+                    mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb, modelDelegate);
         } catch (Exception e) {
             resetLauncherDb(restoreEventLogger);
             throw new Exception("attemptMigrateDb: Failed to migrate grid", e);
@@ -403,8 +404,9 @@
     /**
      * Migrates the DB if needed. If the migration failed, it clears the DB.
      */
-    public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
-        if (!migrateGridIfNeeded()) {
+    public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger,
+            ModelDelegate modelDelegate) {
+        if (!migrateGridIfNeeded(modelDelegate)) {
             if (restoreEventLogger != null) {
                 if (LauncherPrefs.get(mContext).get(NO_DB_FILES_RESTORED)) {
                     restoreEventLogger.logLauncherItemsRestoreFailed(DATA_TYPE_DB_FILE, 1,
@@ -434,7 +436,7 @@
      * @return true if migration was success or ignored, false if migration failed
      * and the DB should be reset.
      */
-    private boolean migrateGridIfNeeded() {
+    private boolean migrateGridIfNeeded(ModelDelegate modelDelegate) {
         createDbIfNotExists();
         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
             // If we have already create a new DB, ignore migration
@@ -468,7 +470,8 @@
             DeviceGridState destDeviceState = new DeviceGridState(idp);
             boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
             return GridSizeMigrationDBController.migrateGridIfNeeded(mContext, srcDeviceState,
-                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb);
+                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb,
+                    modelDelegate);
         } catch (Exception e) {
             FileLog.e(TAG, "migrateGridIfNeeded: Failed to migrate grid", e);
             return false;
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index 2264d35..5a2aef0 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -123,6 +123,11 @@
     @WorkerThread
     public void modelLoadComplete() { }
 
+    /** Called when grid migration has completed as part of grid size refactor. */
+    @WorkerThread
+    public void gridMigrationComplete(
+            @NonNull DeviceGridState src, @NonNull DeviceGridState dest) { }
+
     /**
      * Called when the delegate is no loner needed
      */
diff --git a/src/com/android/launcher3/util/MultiTranslateDelegate.java b/src/com/android/launcher3/util/MultiTranslateDelegate.java
index 38c87c8..ce006c4 100644
--- a/src/com/android/launcher3/util/MultiTranslateDelegate.java
+++ b/src/com/android/launcher3/util/MultiTranslateDelegate.java
@@ -38,6 +38,7 @@
     public static final int INDEX_TASKBAR_REVEAL_ANIM = 4;
     public static final int INDEX_TASKBAR_PINNING_ANIM = 5;
     public static final int INDEX_NAV_BAR_ANIM = 6;
+    public static final int INDEX_BUBBLE_BAR_ANIM = 7;
 
     // Affect all items inside of a MultipageCellLayout
     public static final int INDEX_CELLAYOUT_MULTIPAGE_SPACING = 3;
@@ -48,7 +49,7 @@
     // Specific for hotseat items when adjusting for bubbles
     public static final int INDEX_BUBBLE_ADJUSTMENT_ANIM = 3;
 
-    public static final int COUNT = 7;
+    public static final int COUNT = 8;
 
     private final MultiPropertyFactory<View> mTranslationX;
     private final MultiPropertyFactory<View> mTranslationY;
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 6739387..22857b1 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -465,8 +465,7 @@
         bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
 
         try (LauncherIcons li = LauncherIcons.obtain(l)) {
-            Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable, null,
-                    null, null));
+            Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable));
         }
 
         bounds.inset(
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 954dc8f..bfbdb18 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -58,7 +58,7 @@
     @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
 
     @Before
-    fun setUp() {
+    open fun setUp() {
         context = ApplicationProvider.getApplicationContext()
         // make sure to reset values
         useTwoPanels = false
@@ -83,7 +83,7 @@
 
     protected fun initializeVarsForPhone(
         isGestureMode: Boolean = true,
-        isVerticalBar: Boolean = false
+        isVerticalBar: Boolean = false,
     ) {
         val (x, y) = if (isVerticalBar) Pair(2400, 1080) else Pair(1080, 2400)
 
@@ -94,8 +94,8 @@
                     if (isVerticalBar) 118 else 0,
                     if (isVerticalBar) 74 else 118,
                     if (!isGestureMode && isVerticalBar) 126 else 0,
-                    if (isGestureMode) 63 else if (isVerticalBar) 0 else 126
-                )
+                    if (isGestureMode) 63 else if (isVerticalBar) 0 else 126,
+                ),
             )
 
         whenever(info.isTablet(any())).thenReturn(false)
@@ -121,7 +121,7 @@
                             PointF(80f, 104f),
                             PointF(80f, 104f),
                             PointF(80f, 104f),
-                            PointF(80f, 104f)
+                            PointF(80f, 104f),
                         )
                         .toTypedArray()
 
@@ -143,7 +143,7 @@
                             PointF(80f, 104f),
                             PointF(80f, 104f),
                             PointF(80f, 104f),
-                            PointF(80f, 104f)
+                            PointF(80f, 104f),
                         )
                         .toTypedArray()
                 allAppsIconSize = floatArrayOf(60f, 60f, 60f, 60f)
@@ -174,7 +174,7 @@
 
     protected fun initializeVarsForTablet(
         isLandscape: Boolean = false,
-        isGestureMode: Boolean = true
+        isGestureMode: Boolean = true,
     ) {
         val (x, y) = if (isLandscape) Pair(2560, 1600) else Pair(1600, 2560)
 
@@ -203,7 +203,7 @@
                             PointF(102f, 120f),
                             PointF(120f, 104f),
                             PointF(102f, 120f),
-                            PointF(102f, 120f)
+                            PointF(102f, 120f),
                         )
                         .toTypedArray()
 
@@ -225,7 +225,7 @@
                             PointF(96f, 142f),
                             PointF(126f, 126f),
                             PointF(96f, 142f),
-                            PointF(96f, 142f)
+                            PointF(96f, 142f),
                         )
                         .toTypedArray()
                 allAppsIconSize = FloatArray(4) { 60f }
@@ -288,7 +288,7 @@
                             PointF(80f, 104f),
                             PointF(80f, 104f),
                             PointF(68f, 116f),
-                            PointF(80f, 102f)
+                            PointF(80f, 102f),
                         )
                         .toTypedArray()
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
index a9082e2..47110fa 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
@@ -61,9 +61,11 @@
             ModelDbController controller = model.getModelDbController();
             // Migrate any previous data so that the DB state is correct
             if (Flags.gridMigrationRefactor()) {
-                controller.attemptMigrateDb(null /* restoreEventLogger */);
+                controller.attemptMigrateDb(
+                        null /* restoreEventLogger */, model.getModelDelegate());
             } else {
-                controller.tryMigrateDB(null /* restoreEventLogger */);
+                controller.tryMigrateDB(null /* restoreEventLogger */,
+                        model.getModelDelegate());
             }
 
             // Create DB again to load fresh data
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
index 8d072d8..547d05e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
@@ -31,8 +31,9 @@
         loadModelSync()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             modelDbController.run {
-                if (Flags.gridMigrationRefactor()) attemptMigrateDb(null /* restoreEventLogger */)
-                else tryMigrateDB(null /* restoreEventLogger */)
+                if (Flags.gridMigrationRefactor())
+                    attemptMigrateDb(null /* restoreEventLogger */, modelDelegate)
+                else tryMigrateDB(null /* restoreEventLogger */, modelDelegate)
                 createEmptyDB()
                 clearEmptyDbFlag()
             }
@@ -74,7 +75,7 @@
         loadModelSync()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             val controller: ModelDbController = modelDbController
-            controller.attemptMigrateDb(null /* restoreEventLogger */)
+            controller.attemptMigrateDb(null /* restoreEventLogger */, modelDelegate)
             modelDbController.newTransaction().use { transaction ->
                 val values =
                     ContentValues().apply {
diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
index b4ee090..38fad6b 100644
--- a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
+++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
@@ -25,6 +25,7 @@
 import com.android.launcher3.Flags
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.model.ModelDbController
+import com.android.launcher3.model.ModelDelegate
 import com.android.launcher3.provider.RestoreDbTask
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.TestUtil
@@ -34,6 +35,7 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 
 /**
  * Makes sure to test {@code RestoreDbTask#removeOldDBs}, we need to remove all the dbs that are not
@@ -49,6 +51,8 @@
     @Rule
     val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
 
+    val modelDelegate = mock<ModelDelegate>()
+
     @Before
     fun setUp() {
         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_NARROW_GRID_RESTORE)
@@ -68,9 +72,9 @@
     fun oldDatabasesNotPresentAfterRestore() {
         val dbController = ModelDbController(getInstrumentation().targetContext)
         if (Flags.gridMigrationRefactor()) {
-            dbController.attemptMigrateDb(null)
+            dbController.attemptMigrateDb(null, modelDelegate)
         } else {
-            dbController.tryMigrateDB(null)
+            dbController.tryMigrateDB(null, modelDelegate)
         }
         TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
             assert(backAndRestoreRule.getDatabaseFiles().size == 1) {
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 20684b3..f6aa31a 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -55,7 +55,6 @@
      */
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/349439239
     public void testDeleteFromWorkspace() {
         for (String appName : new String[]{GMAIL_APP_NAME, STORE_APP_NAME, TEST_APP_NAME}) {
             final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(appName);
diff --git a/tests/src/com/android/launcher3/model/GridMigrationTest.kt b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
index b8ffe74..8bd0c60 100644
--- a/tests/src/com/android/launcher3/model/GridMigrationTest.kt
+++ b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
@@ -32,6 +32,8 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
 
 private val phoneContext = InstrumentationRegistry.getInstrumentation().targetContext
 
@@ -87,6 +89,8 @@
     @Rule
     val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
 
+    val modelDelegate = mock<ModelDelegate>()
+
     @Before
     fun setup() {
         setFlagsRule.setFlags(true, Flags.FLAG_ONE_GRID_SPECS)
@@ -102,6 +106,7 @@
                 dst.dbHelper,
                 src.dbHelper.readableDatabase,
                 true,
+                modelDelegate,
             )
         } else {
             GridSizeMigrationDBController.migrateGridIfNeeded(
@@ -111,6 +116,7 @@
                 dst.dbHelper,
                 src.dbHelper.readableDatabase,
                 true,
+                modelDelegate,
             )
         }
     }
@@ -149,11 +155,12 @@
     }
 
     /**
-     * Migrate src into dst and compare to target. This method validates 3 things:
+     * Migrate src into dst and compare to target. This method validates 4 things:
      * 1. dst has the same number of items as src after the migration, meaning, none of the items
      *    were removed during the migration.
      * 2. dst is valid, meaning that none of the items overlap with each other.
      * 3. dst is equal to target to ensure we don't unintentionally change the migration logic.
+     * 4. migration notifies the complete callback.
      */
     private fun runTest(src: GridMigrationData, dst: GridMigrationData, target: GridMigrationData) {
         migrate(src, dst)
@@ -162,6 +169,7 @@
         }
         validateDb(dst)
         compare(dst, target, src)
+        verify(modelDelegate).gridMigrationComplete(src.gridState, dst.gridState)
     }
 
     // Copying the src db for all tests.
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index 460ffc4..3e3e643 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -51,7 +51,6 @@
     @Test
     @PortraitLandscape
     public void testDragIcon() throws Throwable {
-        mLauncher.enableDebugTracing(); // b/289161193
         commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
 
         waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
@@ -72,7 +71,6 @@
                 TestUtil.DEFAULT_UI_TIMEOUT);
         assertNotNull("Widget not found on the workspace", widget);
         widget.launch(getAppPackageName());
-        mLauncher.disableDebugTracing(); // b/289161193
     }
 
     /**