Merge "Support both grid and large tile with gridOnlyOverivew" into main
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index b299edf..e0a597c 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -71,4 +71,11 @@
     metadata {
       purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
+
+flag {
+    name: "enable_expressive_dismiss_task_motion"
+    namespace: "launcher_overview"
+    description: "Enables expressive motion and animations for dismissing a task in Overview."
+    bug: "381239462"
+}
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/redesigned_gesture_tutorial_fragment.xml b/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml
index b004dfd..7530c28 100644
--- a/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml
@@ -147,6 +147,7 @@
             android:layout_height="wrap_content"
             android:layout_marginTop="104dp"
             android:accessibilityHeading="true"
+            android:accessibilityTraversalBefore="@id/gesture_tutorial_fragment_feedback_subtitle"
             android:gravity="top"
             android:lineSpacingExtra="-1sp"
             android:textAppearance="@style/TextAppearance.GestureTutorial.MainTitle"
@@ -161,6 +162,8 @@
             android:layout_marginTop="24dp"
             android:lineSpacingExtra="4sp"
             android:textAppearance="@style/TextAppearance.GestureTutorial.MainSubtitle"
+            android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_feedback_title"
+            android:accessibilityTraversalBefore="@id/gesture_tutorial_fragment_action_button"
 
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_title" />
@@ -224,6 +227,10 @@
         android:layout_marginBottom="@dimen/gesture_tutorial_done_button_bottom_margin"
         android:layout_alignParentBottom="true"
         android:layout_alignParentEnd="true"
+        android:clickable="true"
+        android:focusableInTouchMode="true"
+        android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_feedback_subtitle"
+        android:contentDescription="@string/gesture_tutorial_action_button_label"
         android:background="@drawable/gesture_tutorial_action_button_background"
         android:stateListAnimator="@null"
         android:text="@string/gesture_tutorial_action_button_label"
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index a11974c..b433a59 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -29,9 +29,9 @@
         android:id="@+id/split_instructions_text"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
-        android:maxWidth="@dimen/split_instructions_view_max_width"
         android:textColor="?androidprv:attr/textColorOnAccent"
-        android:text="@string/toast_split_select_app" />
+        android:text="@string/toast_split_select_app"
+        android:layout_weight="1" />
 
     <androidx.appcompat.widget.AppCompatTextView
         android:id="@+id/split_instructions_text_cancel"
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-sw600dp/dimens.xml b/quickstep/res/values-sw600dp/dimens.xml
index bb42648..4996582 100644
--- a/quickstep/res/values-sw600dp/dimens.xml
+++ b/quickstep/res/values-sw600dp/dimens.xml
@@ -40,9 +40,4 @@
     <dimen name="allset_page_margin_horizontal">120dp</dimen>
     <dimen name="allset_page_allset_text_size">38sp</dimen>
     <dimen name="allset_page_swipe_up_text_size">15sp</dimen>
-
-    <!-- Splitscreen -->
-    <!-- Max width of the split instructions view -->
-    <dimen name="split_instructions_view_max_width">300dp</dimen>
-
 </resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index fc671a6..493a5b8 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -81,6 +81,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>
@@ -462,6 +468,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>
@@ -519,9 +526,4 @@
 
     <!-- Digital Wellbeing -->
     <dimen name="digital_wellbeing_toast_height">48dp</dimen>
-
-    <!-- Splitscreen -->
-    <!-- Max width of the split instructions view -->
-    <dimen name="split_instructions_view_max_width">220dp</dimen>
-
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index df949c3..324ea31 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -49,9 +49,6 @@
     <!-- Accessibility title for the list of recent apps [CHAR_LIMIT=none] -->
     <string name="accessibility_recent_apps">Recent apps</string>
 
-    <!-- Accessibility confirmation for task closed -->
-    <string name="task_view_closed">Task Closed</string>
-
     <!-- Accessibility title for an app card in Recents for apps that have time limit set
      [CHAR_LIMIT=none] -->
     <string name="task_contents_description_with_remaining_time"><xliff:g id="task_description" example="GMail">%1$s</xliff:g>, <xliff:g id="remaining_time" example="7 minutes left today">%2$s</xliff:g></string>
@@ -360,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/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index cd38e5e..48cb911 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -269,7 +269,7 @@
         DesktopTask desktopTask = findDesktopTask(tasks);
 
         if (desktopTask != null) {
-            mTasks = desktopTask.tasks.stream()
+            mTasks = desktopTask.getTasks().stream()
                     .map(GroupTask::new)
                     .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
                     .collect(Collectors.toList());
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/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 1905561..9a9575d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -393,10 +393,7 @@
      * @param launcherState The current state launcher is in
      */
     private void updateOverviewDragState(LauncherState launcherState) {
-        boolean disallowLongClick =
-                FeatureFlags.enableSplitContextually()
-                        ? mLauncher.isSplitSelectionActive() || mIsAnimatingToLauncher
-                        : launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
+        boolean disallowLongClick = mLauncher.isSplitSelectionActive() || mIsAnimatingToLauncher;
         com.android.launcher3.taskbar.Utilities.setOverviewDragState(
                 mControllers, launcherState.disallowTaskbarGlobalDrag(),
                 disallowLongClick, launcherState.allowTaskbarInitialSplitSelection());
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/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 3dcf2b4..7d39bf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -342,10 +342,10 @@
             mBubbleBarViewController.showOverflow(update.showOverflow);
         }
 
-        BubbleBarBubble bubbleToSelect = null;
         if (update.addedBubble != null) {
             mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
         }
+        BubbleBarBubble bubbleToSelect = null;
         if (update.selectedBubbleKey != null) {
             if (mSelectedBubble == null
                     || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
@@ -363,7 +363,7 @@
                 && update.removedBubbles.isEmpty()
                 && !mBubbles.isEmpty()) {
             // A bubble was added from the overflow (& now it's empty / not showing)
-            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble, bubbleToSelect);
         } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
             // we're adding and removing a bubble at the same time. handle this as a single update.
             RemovedBubble removedBubble = update.removedBubbles.get(0);
@@ -371,7 +371,8 @@
             boolean showOverflow = update.showOverflowChanged && update.showOverflow;
             if (bubbleToRemove != null) {
                 mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
-                        bubbleToRemove, isExpanding, suppressAnimation, showOverflow);
+                        bubbleToRemove, bubbleToSelect, isExpanding, suppressAnimation,
+                        showOverflow);
             } else {
                 mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                         suppressAnimation, bubbleToSelect);
@@ -387,7 +388,7 @@
                     if (bubble != null && overflowNeedsToBeAdded) {
                         // First removal, show the overflow
                         overflowNeedsToBeAdded = false;
-                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble, bubbleToSelect);
                     } else if (bubble != null) {
                         mBubbleBarViewController.removeBubble(bubble);
                     } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index aa6ad25..c001123 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -741,35 +741,32 @@
 
     /** Add a new bubble and remove an old bubble from the bubble bar. */
     public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble,
-            Runnable onEndRunnable) {
+            @Nullable BubbleView bubbleToSelect, Runnable onEndRunnable) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
-        boolean isOverflowSelected =
-                mSelectedBubbleView != null && mSelectedBubbleView.isOverflow();
-        boolean removingOverflow = removedBubble.isOverflow();
-        boolean addingOverflow = addedBubble.isOverflow();
-
+        int addedIndex = addedBubble.isOverflow() ? getChildCount() : 0;
         if (!isExpanded()) {
             removeView(removedBubble);
-            int index = addingOverflow ? getChildCount() : 0;
-            addView(addedBubble, index, lp);
+            addView(addedBubble, addedIndex, lp);
             if (onEndRunnable != null) {
                 onEndRunnable.run();
             }
             return;
         }
-        int index = addingOverflow ? getChildCount() : 0;
         addedBubble.setScaleX(0f);
         addedBubble.setScaleY(0f);
-        addView(addedBubble, index, lp);
-
-        if (isOverflowSelected && removingOverflow) {
-            // The added bubble will be selected
-            mSelectedBubbleView = addedBubble;
-        }
-        int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
+        addView(addedBubble, addedIndex, lp);
+        int indexOfCurrentSelectedBubble = indexOfChild(mSelectedBubbleView);
         int indexOfBubbleToRemove = indexOfChild(removedBubble);
-
+        int indexOfNewlySelectedBubble = bubbleToSelect == null
+                ? indexOfCurrentSelectedBubble : indexOfChild(bubbleToSelect);
+        // Since removed bubble is kept till the end of the animation we should check if there are
+        // more than one bubble. In such a case the bar will remain open without the selected bubble
+        if (mSelectedBubbleView == removedBubble
+                && bubbleToSelect == null
+                && getBubbleChildCount() > 1) {
+            Log.w(TAG, "Remove the currently selected bubble without selecting a new one.");
+        }
         mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
                 getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
         BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
@@ -802,8 +799,8 @@
                 invalidate();
             }
         };
-        mBubbleAnimator.animateNewAndRemoveOld(indexOfSelectedBubble, indexOfBubbleToRemove,
-                listener);
+        mBubbleAnimator.animateNewAndRemoveOld(indexOfCurrentSelectedBubble,
+                indexOfNewlySelectedBubble, indexOfBubbleToRemove, addedIndex, listener);
     }
 
     @Override
@@ -1342,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 5685093..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);
     }
 
@@ -481,7 +483,7 @@
 
     /** Whether the bubble bar has bubbles. */
     public boolean hasBubbles() {
-        return mBubbleBarController.getSelectedBubbleKey() != null;
+        return mBarView.getBubbleChildCount() > 0;
     }
 
     /**
@@ -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.
     //
@@ -843,11 +884,12 @@
     }
 
     /** Adds a new bubble and removes an old bubble at the same time. */
-    public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble,
-            BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation,
-            boolean addOverflowToo) {
+    public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, BubbleBarBubble removedBubble,
+            @Nullable BubbleBarBubble bubbleToSelect, boolean isExpanding,
+            boolean suppressAnimation, boolean addOverflowToo) {
+        BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(),
-                addOverflowToo ? () -> showOverflow(true) : null);
+                bubbleToSelectView, addOverflowToo ? () -> showOverflow(true) : null);
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
         addedBubble.getView().setController(mBubbleViewController);
         removedBubble.getView().setController(null);
@@ -878,22 +920,26 @@
     }
 
     /** Adds the overflow view to the bubble bar while animating a view away. */
-    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble,
+            @Nullable BubbleBarBubble bubbleToSelect) {
         if (mOverflowAdded) return;
         mOverflowAdded = true;
+        BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
         mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(),
-                null /* onEndRunnable */);
+                bubbleToSelectView, null /* onEndRunnable */);
         mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
         mOverflowBubble.getView().setController(mBubbleViewController);
         removedBubble.getView().setController(null);
     }
 
     /** Removes the overflow view to the bubble bar while animating a view in. */
-    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble,
+            @Nullable BubbleBarBubble bubbleToSelect) {
         if (!mOverflowAdded) return;
         mOverflowAdded = false;
+        BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(),
-                null /* onEndRunnable */);
+                bubbleToSelectView, null /* onEndRunnable */);
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
         addedBubble.getView().setController(mBubbleViewController);
         mOverflowBubble.getView().setController(null);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
index 944e806..26d6ccc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -18,6 +18,8 @@
 
 import androidx.core.animation.Animator
 import androidx.core.animation.ValueAnimator
+import kotlin.math.max
+import kotlin.math.min
 
 /**
  * Animates individual bubbles within the bubble bar while the bubble bar is expanded.
@@ -70,14 +72,18 @@
 
     fun animateNewAndRemoveOld(
         selectedBubbleIndex: Int,
+        newlySelectedBubbleIndex: Int,
         removedBubbleIndex: Int,
+        addedBubbleIndex: Int,
         listener: Listener,
     ) {
         animator = createAnimator(listener)
         state =
             State.AddingAndRemoving(
                 selectedBubbleIndex = selectedBubbleIndex,
+                newlySelectedBubbleIndex = newlySelectedBubbleIndex,
                 removedBubbleIndex = removedBubbleIndex,
+                addedBubbleIndex = addedBubbleIndex,
             )
         animator.start()
     }
@@ -137,6 +143,7 @@
                 getBubbleTranslationXWhileAddingBubbleAtLimit(
                     bubbleIndex = bubbleIndex,
                     removedBubbleIndex = state.removedBubbleIndex,
+                    addedBubbleIndex = state.addedBubbleIndex,
                     addedBubbleScale = animator.animatedFraction,
                     removedBubbleScale = 1 - animator.animatedFraction,
                 )
@@ -187,34 +194,25 @@
             State.Idle -> 0f
             is State.AddingBubble -> getArrowPositionWhenAddingBubble(state)
             is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
-            is State.AddingAndRemoving -> {
-                // we never remove the selected bubble, so the arrow stays pointing to its center
-                val tx =
-                    getBubbleTranslationXWhileAddingBubbleAtLimit(
-                        bubbleIndex = state.selectedBubbleIndex,
-                        removedBubbleIndex = state.removedBubbleIndex,
-                        addedBubbleScale = animator.animatedFraction,
-                        removedBubbleScale = 1 - animator.animatedFraction,
-                    )
-                tx + iconSize / 2f
-            }
+            is State.AddingAndRemoving -> getArrowPositionWhenAddingAndRemovingBubble(state)
         }
     }
 
     private fun getArrowPositionWhenAddingBubble(state: State.AddingBubble): Float {
         val scale = animator.animatedFraction
-        var tx = getBubbleTranslationXWhileScalingBubble(
-            bubbleIndex = state.selectedBubbleIndex,
-            scalingBubbleIndex = 0,
-            bubbleScale = scale
-        ) + iconSize / 2f
+        var tx =
+            getBubbleTranslationXWhileScalingBubble(
+                bubbleIndex = state.selectedBubbleIndex,
+                scalingBubbleIndex = 0,
+                bubbleScale = scale,
+            ) + iconSize / 2f
         if (state.newlySelectedBubbleIndex != null) {
             val selectedBubbleScale = if (state.newlySelectedBubbleIndex == 0) scale else 1f
             val finalTx =
                 getBubbleTranslationXWhileScalingBubble(
                     bubbleIndex = state.newlySelectedBubbleIndex,
                     scalingBubbleIndex = 0,
-                    bubbleScale = 1f,
+                    bubbleScale = scale,
                 ) + iconSize * selectedBubbleScale / 2f
             tx += (finalTx - tx) * animator.animatedFraction
         }
@@ -266,6 +264,46 @@
             }
         }
 
+    private fun getArrowPositionWhenAddingAndRemovingBubble(state: State.AddingAndRemoving): Float {
+        // The bubble bar keeps constant width while adding and removing bubble. So we just need to
+        // find selected bubble arrow position on the animation start and newly selected bubble
+        // arrow position on the animation end interpolating the arrow between these positions
+        // during the animation.
+        // The indexes in the state are provided for the bubble bar containing all bubbles. So for
+        // certain circumstances indexes should be adjusted.
+        // When animation is started added bubble has zero scale as well as removed bubble when the
+        // animation is ended, so for both cases we should compute translation as it is one less
+        // bubble.
+        val bubbleCountOnEnd = bubbleCount - 1
+        var selectedIndex = state.selectedBubbleIndex
+        // We only need to adjust the selected index if added bubble was added before the selected.
+        if (selectedIndex > state.addedBubbleIndex) {
+            // If the selectedIndex is higher index than the added bubble index, we need to reduce
+            // selectedIndex by one because the added bubble  has zero scale when animation is
+            // started.
+            selectedIndex--
+        }
+        var newlySelectedIndex = state.newlySelectedBubbleIndex
+        // We only need to adjust newlySelectedIndex if removed bubble was removed before the newly
+        // selected bubble.
+        if (newlySelectedIndex > state.removedBubbleIndex) {
+            // If the newlySelectedIndex is higher index than the removed bubble index, we need to
+            // reduce newlySelectedIndex by one because the removed bubble has zero scale when
+            // animation is ended.
+            newlySelectedIndex--
+        }
+        val iconAndSpacing: Float = iconSize + expandedBarIconSpacing
+        val startTx = getBubblesToTheLeft(selectedIndex, bubbleCountOnEnd) * iconAndSpacing
+        val endTx = getBubblesToTheLeft(newlySelectedIndex, bubbleCountOnEnd) * iconAndSpacing
+        val tx = startTx + (endTx - startTx) * animator.animatedFraction
+        return tx + iconSize / 2f
+    }
+
+    private fun getBubblesToTheLeft(bubbleIndex: Int, bubbleCount: Int = this.bubbleCount): Int =
+        // when bar is on left the index - 0 corresponds to the right - most bubble and when the
+        // bubble bar is on the right - 0 corresponds to the left - most bubble.
+        if (onLeft) bubbleCount - bubbleIndex - 1 else bubbleIndex
+
     /**
      * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
      * expanded and a bubble is animating in or out.
@@ -290,6 +328,7 @@
                     // the bar is on the left and the current bubble is to the right of the scaling
                     // bubble so account for its scale
                     (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing
+
                 bubbleIndex == scalingBubbleIndex -> {
                     // the bar is on the left and this is the scaling bubble
                     val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize
@@ -299,6 +338,7 @@
                     val scaledSpace = bubbleScale * expandedBarIconSpacing
                     totalIconSize + totalSpacing + scaledSpace + pivotAdjustment
                 }
+
                 else ->
                     // the bar is on the left and the scaling bubble is on the right. the current
                     // bubble is unaffected by the scaling bubble
@@ -310,10 +350,12 @@
                     // the bar is on the right and the scaling bubble is on the right. the current
                     // bubble is unaffected by the scaling bubble
                     iconAndSpacing * bubbleIndex
+
                 bubbleIndex == scalingBubbleIndex ->
                     // the bar is on the right, and this is the animating bubble. it only needs to
                     // be adjusted for the scaling pivot.
                     iconAndSpacing * bubbleIndex + pivotAdjustment
+
                 else ->
                     // the bar is on the right and the scaling bubble is on the left so account for
                     // its scale
@@ -325,61 +367,67 @@
     private fun getBubbleTranslationXWhileAddingBubbleAtLimit(
         bubbleIndex: Int,
         removedBubbleIndex: Int,
+        addedBubbleIndex: Int,
         addedBubbleScale: Float,
         removedBubbleScale: Float,
     ): Float {
         val iconAndSpacing = iconSize + expandedBarIconSpacing
         // the bubbles are scaling from the center, so we need to adjust their translation so
         // that the distance to the adjacent bubble scales at the same rate.
-        val addedBubblePivotAdjustment = -(1 - addedBubbleScale) * iconSize / 2f
-        val removedBubblePivotAdjustment = -(1 - removedBubbleScale) * iconSize / 2f
+        val addedBubblePivotAdjustment = (addedBubbleScale - 1) * iconSize / 2f
+        val removedBubblePivotAdjustment = (removedBubbleScale - 1) * iconSize / 2f
 
-        return if (onLeft) {
-            // this is how many bubbles there are to the left of the current bubble.
-            // when the bubble bar is on the right the added bubble is the right-most bubble so it
-            // doesn't affect the translation of any other bubble.
-            // when the removed bubble is to the left of the current bubble, we need to subtract it
-            // from bubblesToLeft and use removedBubbleScale instead when calculating the
-            // translation.
-            val bubblesToLeft = bubbleCount - bubbleIndex - 1
-            when {
-                bubbleIndex == 0 ->
-                    // this is the added bubble and it's the right-most bubble. account for all the
-                    // other bubbles -- including the removed bubble -- and adjust for the added
-                    // bubble pivot.
-                    (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing +
-                        addedBubblePivotAdjustment
-                bubbleIndex < removedBubbleIndex ->
+        val minAddedRemovedIndex = min(addedBubbleIndex, removedBubbleIndex)
+        val maxAddedRemovedIndex = max(addedBubbleIndex, removedBubbleIndex)
+        val isBetweenAddedAndRemoved =
+            bubbleIndex in (minAddedRemovedIndex + 1)..<maxAddedRemovedIndex
+        val isRemovedBubbleToLeftOfAddedBubble = onLeft == addedBubbleIndex < removedBubbleIndex
+        val bubblesToLeft = getBubblesToTheLeft(bubbleIndex)
+        return when {
+            isBetweenAddedAndRemoved -> {
+                if (isRemovedBubbleToLeftOfAddedBubble) {
                     // the removed bubble is to the left so account for it
                     (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
-                bubbleIndex == removedBubbleIndex -> {
-                    // this is the removed bubble. all the bubbles to the left are at full scale
-                    // but we need to scale the spacing between the removed bubble and the bubble to
-                    // its left because the removed bubble disappears towards the left side
+                } else {
+                    // the added bubble is to the left so account for it
+                    (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing
+                }
+            }
+
+            bubbleIndex == addedBubbleIndex -> {
+                if (isRemovedBubbleToLeftOfAddedBubble) {
+                    // the removed bubble is to the left so account for it
+                    (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
+                } else {
+                    // it's the left-most scaling bubble, all bubbles on the left are at full scale
+                    bubblesToLeft * iconAndSpacing
+                } + addedBubblePivotAdjustment
+            }
+
+            bubbleIndex == removedBubbleIndex -> {
+                if (isRemovedBubbleToLeftOfAddedBubble) {
+                    // All the bubbles to the left are at full scale, but we need to scale the
+                    // spacing between the removed bubble and the bubble next to it
                     val totalIconSize = bubblesToLeft * iconSize
                     val totalSpacing =
                         (bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing
-                    totalIconSize + totalSpacing + removedBubblePivotAdjustment
-                }
-                else ->
-                    // both added and removed bubbles are to the right so they don't affect the tx
-                    bubblesToLeft * iconAndSpacing
+                    totalIconSize + totalSpacing
+                } else {
+                    // The added bubble is to the left, so account for it
+                    (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing
+                } + removedBubblePivotAdjustment
             }
-        } else {
-            when {
-                bubbleIndex == 0 -> addedBubblePivotAdjustment // we always add bubbles at index 0
-                bubbleIndex < removedBubbleIndex ->
-                    // the bar is on the right and the removed bubble is on the right. the current
-                    // bubble is unaffected by the removed bubble. only need to factor in the added
-                    // bubble's scale.
-                    iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale)
-                bubbleIndex == removedBubbleIndex ->
-                    // the bar is on the right, and this is the animating bubble.
-                    iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) +
-                        removedBubblePivotAdjustment
-                else ->
-                    // both the added and the removed bubbles are to the left of the current bubble
-                    iconAndSpacing * (bubbleIndex - 2 + addedBubbleScale + removedBubbleScale)
+
+            else -> {
+                // if bubble index is on the right side of the animated bubbles, we need to deduct
+                // one, since both the added and the removed bubbles takes a single place
+                val onTheRightOfAnimatedBubbles =
+                    if (onLeft) {
+                        bubbleIndex < minAddedRemovedIndex
+                    } else {
+                        bubbleIndex > maxAddedRemovedIndex
+                    }
+                (bubblesToLeft - if (onTheRightOfAnimatedBubbles) 1 else 0) * iconAndSpacing
             }
         }
     }
@@ -413,10 +461,17 @@
             val removingLastRemainingBubble: Boolean,
         ) : State
 
-        // TODO add index where bubble is being added, and index for newly selected bubble
         /** A new bubble is being added and an old bubble is being removed from the bubble bar. */
-        data class AddingAndRemoving(val selectedBubbleIndex: Int, val removedBubbleIndex: Int) :
-            State
+        data class AddingAndRemoving(
+            /** The index of the selected bubble. */
+            val selectedBubbleIndex: Int,
+            /** The index of the newly selected bubble. */
+            val newlySelectedBubbleIndex: Int,
+            /** The index of the bubble being removed. */
+            val removedBubbleIndex: Int,
+            /** The index of the added bubble. */
+            val addedBubbleIndex: Int,
+        ) : State
     }
 
     /** Callbacks for the animation. */
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/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 3469e2f..810325c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -39,7 +39,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.Utilities.isRtl;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
-import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
@@ -562,6 +561,11 @@
             mSplitSelectStateController.onDestroy();
         }
 
+        RecentsView recentsView = getOverviewPanel();
+        if (recentsView != null) {
+            recentsView.destroy();
+        }
+
         super.onDestroy();
         mHotseatPredictionController.destroy();
         if (mViewCapture != null) mViewCapture.close();
@@ -718,11 +722,7 @@
                     splitSelectSource.alreadyRunningTaskId = taskWasFound
                             ? foundTask.key.id
                             : INVALID_TASK_ID;
-                    if (enableSplitContextually()) {
-                        startSplitToHome(splitSelectSource);
-                    } else {
-                        recentsView.initiateSplitSelect(splitSelectSource);
-                    }
+                    startSplitToHome(splitSelectSource);
                 }
         );
     }
@@ -812,15 +812,13 @@
 
         super.onPause();
 
-        if (enableSplitContextually()) {
-            // If Launcher pauses before both split apps are selected, exit split screen.
-            if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
-                    !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
-                mSplitSelectStateController
-                        .logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
-                mSplitSelectStateController.getSplitAnimationController()
-                        .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
-            }
+        // If Launcher pauses before both split apps are selected, exit split screen.
+        if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
+                !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
+            mSplitSelectStateController
+                    .logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
+            mSplitSelectStateController.getSplitAnimationController()
+                    .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
         }
 
         if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
@@ -1419,7 +1417,7 @@
 
     @Override
     public boolean handleIncorrectSplitTargetSelection() {
-        if (!enableSplitContextually() || !mSplitSelectStateController.isSplitSelectActive()) {
+        if (!mSplitSelectStateController.isSplitSelectActive()) {
             return false;
         }
         mSplitSelectStateController.getSplitInstructionsView().goBoing();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
index f196548..c8f46a9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -26,7 +26,6 @@
 import com.android.launcher3.anim.AnimatorListeners.forSuccessCallback
 import com.android.launcher3.anim.PendingAnimation
 import com.android.launcher3.anim.PropertySetter
-import com.android.launcher3.config.FeatureFlags.enableSplitContextually
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
 import com.android.launcher3.statemanager.StateManager.StateHandler
 import com.android.launcher3.states.StateAnimationConfig
@@ -53,7 +52,6 @@
 import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION
 import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA
 import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL
-import com.android.wm.shell.Flags.enableSplitContextual
 
 /**
  * State handler for handling UI changes for [com.android.quickstep.views.LauncherRecentsView]. In
@@ -129,23 +127,6 @@
             config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR),
         )
 
-        val exitingOverview = !enableSplitContextually() && !toState.isRecentsViewVisible
-        if (recentsView.isSplitSelectionActive && exitingOverview) {
-            builder.add(
-                recentsView.splitSelectController.splitAnimationController
-                    .createPlaceholderDismissAnim(
-                        launcher,
-                        LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
-                        builder.duration,
-                    )
-            )
-            builder.setViewAlpha(
-                recentsView.splitInstructionsView,
-                0f,
-                config.getInterpolator(ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE, LINEAR),
-            )
-        }
-
         builder.setFloat(
             recentsView,
             CONTENT_ALPHA,
@@ -227,9 +208,7 @@
         builder: PendingAnimation,
         animate: Boolean,
     ) {
-        val goingToOverviewFromWorkspaceContextual =
-            enableSplitContextual() &&
-                toState == LauncherState.OVERVIEW &&
+        val goingToOverviewFromWorkspaceContextual = toState == LauncherState.OVERVIEW &&
                 launcher.isSplitSelectionActive
         if (
             toState != LauncherState.OVERVIEW_SPLIT_SELECT &&
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 417bb74..7c09e9a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -17,8 +17,6 @@
 package com.android.launcher3.uioverrides.flags
 
 import android.app.PendingIntent
-import android.app.blob.BlobHandle.createWithSha256
-import android.app.blob.BlobStoreManager
 import android.content.Context
 import android.content.IIntentReceiver
 import android.content.IIntentSender.Stub
@@ -29,10 +27,8 @@
 import android.net.Uri
 import android.os.Bundle
 import android.os.IBinder
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream
 import android.provider.DeviceConfig
 import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
-import android.provider.Settings.Secure
 import android.text.Html
 import android.util.AttributeSet
 import android.view.inputmethod.EditorInfo
@@ -44,33 +40,16 @@
 import androidx.preference.PreferenceGroup
 import androidx.preference.PreferenceViewHolder
 import androidx.preference.SwitchPreference
-import com.android.launcher3.AutoInstallsLayout
 import com.android.launcher3.ExtendedEditText
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherPrefs
-import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
-import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
-import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
 import com.android.launcher3.R
-import com.android.launcher3.model.data.FolderInfo
-import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.LauncherAppWidgetInfo
-import com.android.launcher3.pm.UserCache
 import com.android.launcher3.proxy.ProxyActivityStarter
 import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher
-import com.android.launcher3.shortcuts.ShortcutKey
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
-import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
-import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LayoutImportExportHelper
 import com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN
@@ -80,14 +59,12 @@
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.StartActivityParams
-import com.android.launcher3.util.UserIconInfo
 import com.android.quickstep.util.DeviceConfigHelper
 import com.android.quickstep.util.DeviceConfigHelper.Companion.NAMESPACE_LAUNCHER
 import com.android.quickstep.util.DeviceConfigHelper.DebugInfo
 import com.android.systemui.shared.plugins.PluginEnabler
 import com.android.systemui.shared.plugins.PluginPrefs
-import java.io.OutputStreamWriter
-import java.security.MessageDigest
+import java.nio.charset.StandardCharsets
 import java.util.Locale
 import java.util.concurrent.Executor
 
@@ -421,26 +398,12 @@
             title = "Export"
             intent =
                 createUriPickerIntent(ACTION_CREATE_DOCUMENT, MAIN_EXECUTOR) { uri ->
-                    model.enqueueModelUpdateTask { _, dataModel, _ ->
-                        val builder = LauncherLayoutBuilder()
-                        dataModel.workspaceItems.forEach { info ->
-                            val loc =
-                                when (info.container) {
-                                    CONTAINER_DESKTOP ->
-                                        builder.atWorkspace(info.cellX, info.cellY, info.screenId)
-                                    CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
-                                    else -> return@forEach
-                                }
-                            loc.addItem(info)
-                        }
-                        dataModel.appWidgets.forEach { info ->
-                            builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(info)
-                        }
-
+                    LayoutImportExportHelper.exportModelDbAsXml(context) { layoutXml ->
                         context.contentResolver.openOutputStream(uri).use { os ->
-                            builder.build(OutputStreamWriter(os))
+                            val bytes: ByteArray =
+                                layoutXml.toByteArray(StandardCharsets.UTF_8) // Encode to UTF-8
+                            os?.write(bytes)
                         }
-
                         MAIN_EXECUTOR.execute {
                             Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show()
                         }
@@ -458,66 +421,12 @@
                         resolver.openInputStream(uri).use { stream ->
                             stream?.readAllBytes() ?: return@createUriPickerIntent
                         }
-
-                    val digest = MessageDigest.getInstance("SHA-256").digest(data)
-                    val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
-                    val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
-
-                    blobManager.openSession(blobManager.createSession(handle)).use { session ->
-                        AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
-                        session.allowPublicAccess()
-
-                        session.commit(ORDERED_BG_EXECUTOR) {
-                            Secure.putString(
-                                resolver,
-                                LAYOUT_PROVIDER_KEY,
-                                createBlobProviderKey(digest),
-                            )
-
-                            MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
-                            MAIN_EXECUTOR.submit { model.forceReload() }.get()
-                            MODEL_EXECUTOR.submit {}.get()
-                            Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
-                        }
-                    }
+                    LayoutImportExportHelper.importModelFromXml(context, data)
                 }
             category.addPreference(this)
         }
     }
 
-    private fun LauncherLayoutBuilder.ItemTarget.addItem(info: ItemInfo) {
-        val userType: String? =
-            when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
-                UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
-                UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
-                else -> null
-            }
-        when (info.itemType) {
-            ITEM_TYPE_APPLICATION ->
-                info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
-            ITEM_TYPE_DEEP_SHORTCUT ->
-                ShortcutKey.fromItemInfo(info).let { key ->
-                    putShortcut(key.packageName, key.id, userType)
-                }
-            ITEM_TYPE_FOLDER ->
-                (info as FolderInfo).let { folderInfo ->
-                    putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
-                        folderInfo.getContents().forEach { folderContent ->
-                            folderBuilder.addItem(folderContent)
-                        }
-                    }
-                }
-            ITEM_TYPE_APPWIDGET ->
-                putWidget(
-                    (info as LauncherAppWidgetInfo).providerName.packageName,
-                    info.providerName.className,
-                    info.spanX,
-                    info.spanY,
-                    userType,
-                )
-        }
-    }
-
     private fun createUriPickerIntent(
         action: String,
         executor: Executor,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index c48ba4f..5c16a62 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -18,7 +18,6 @@
 import static com.android.app.animation.Interpolators.DECELERATE_2;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
-import static com.android.wm.shell.Flags.enableSplitContextual;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -123,7 +122,7 @@
         if (showFloatingSearch) {
             elements |= FLOATING_SEARCH_BAR;
         }
-        if (enableSplitContextual() && launcher.isSplitSelectionActive()) {
+        if (launcher.isSplitSelectionActive()) {
             elements &= ~CLEAR_ALL_BUTTON;
         }
         return elements;
@@ -131,7 +130,7 @@
 
     @Override
     public float getSplitSelectTranslation(Launcher launcher) {
-        if (!enableSplitContextual() || !launcher.isSplitSelectionActive()) {
+        if (!launcher.isSplitSelectionActive()) {
             return 0f;
         }
         RecentsView recentsView = launcher.getOverviewPanel();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 1d9e492..d203f28 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -210,10 +210,7 @@
                     mLauncher.getStateManager().addStateListener(listener);
                     onSwipeInteractionCompleted(mEndState);
                 };
-                new OverviewToHomeAnim(mLauncher, onReachedHome,
-                        FeatureFlags.enableSplitContextually()
-                                ? mCancelSplitRunnable
-                                : null)
+                new OverviewToHomeAnim(mLauncher, onReachedHome, mCancelSplitRunnable)
                         .animateWithVelocity(velocity);
             } else {
                 mLauncher.getStateManager().goToState(mEndState, true,
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 64c1129..1f95c41 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -268,7 +268,7 @@
      *
      * @return the overview intent
      */
-    Intent getOverviewIntentIgnoreSysUiState() {
+    public Intent getOverviewIntentIgnoreSysUiState() {
         return mIsDefaultHome ? mMyHomeIntent : mOverviewIntent;
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 9c4d9bf..5e8ea37 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -452,6 +452,10 @@
 
     @Override
     protected void onDestroy() {
+        RecentsView recentsView = getOverviewPanel();
+        if (recentsView != null) {
+            recentsView.destroy();
+        }
         super.onDestroy();
         ACTIVITY_TRACKER.onContextDestroyed(this);
         mActivityLaunchAnimationRunner = null;
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index 5128451..233f0a9 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -503,6 +503,11 @@
                 }
             }
 
+            if (Float.isNaN(scale)) {
+                Log.e(TAG, "Scale is NaN: starting dimensions=[" + startWidth + ", " + startHeight
+                        + "], current dimensions=[" + currentWidth + ", " + currentHeight + "]");
+            }
+
             mTargetTaskView.setScaleX(scale);
             mTargetTaskView.setScaleY(scale);
             mTargetTaskView.setTranslationX(
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index e0d4ddd..731c256 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
@@ -53,7 +52,6 @@
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -110,16 +108,6 @@
         return SystemUiProxy.INSTANCE.get(mCtx);
     }
 
-    /**
-     * Preloads the recents animation.
-     */
-    public void preloadRecentsAnimation(Intent intent) {
-        // Pass null animation handler to indicate this start is for preloading
-        UI_HELPER_EXECUTOR.execute(() -> {
-            ActivityManagerWrapper.getInstance().preloadRecentsActivity(intent);
-        });
-    }
-
     boolean shouldIgnoreMotionEvents() {
         return mShouldIgnoreMotionEvents;
     }
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 210065a..bfd6107 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -37,13 +37,16 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitStageInfo;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import com.android.launcher3.util.SplitConfigurationOptions.StageType;
 import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -60,15 +63,17 @@
 import java.util.LinkedList;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /**
  * This class tracked the top-most task and  some 'approximate' task history to allow faster
  * system state estimation during touch interaction
  */
-public class TopTaskTracker extends ISplitScreenListener.Stub
-        implements TaskStackChangeListener, SafeCloseable {
+@LauncherAppSingleton
+public class TopTaskTracker extends ISplitScreenListener.Stub implements TaskStackChangeListener {
     private static final String TAG = "TopTaskTracker";
-    public static MainThreadInitializedObject<TopTaskTracker> INSTANCE =
-            new MainThreadInitializedObject<>(TopTaskTracker::new);
+    public static DaggerSingletonObject<TopTaskTracker> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getTopTaskTracker);
 
     private static final int HISTORY_SIZE = 5;
 
@@ -86,7 +91,9 @@
     // bottom most.
     private ArrayMap<Integer, ArrayList<GroupedTaskInfo>> mVisibleTasks = new ArrayMap<>();
 
-    private TopTaskTracker(Context context) {
+    @Inject
+    public TopTaskTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker,
+            SystemUiProxy systemUiProxy) {
         mContext = context;
 
         if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
@@ -98,18 +105,17 @@
             mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
 
             TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
-            SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this);
-        }
-    }
-
-    @Override
-    public void close() {
-        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
-            return;
+            systemUiProxy.registerSplitScreenListener(this);
         }
 
-        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
-        SystemUiProxy.INSTANCE.get(mContext).unregisterSplitScreenListener(this);
+        tracker.addCloseable(() -> {
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                return;
+            }
+
+            TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
+            systemUiProxy.unregisterSplitScreenListener(this);
+        });
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a06029b..efd9a56 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -49,7 +49,6 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.SystemClock;
-import android.os.Trace;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.InputDevice;
@@ -68,7 +67,6 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
-import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -96,6 +94,7 @@
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.ActiveTrackpadList;
+import com.android.quickstep.util.ActivityPreloadUtil;
 import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -183,7 +182,7 @@
                         recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode,
                         unfoldTransition, dragAndDrop);
                 tis.initInputMonitor("TISBinder#onInitialize()");
-                tis.preloadOverview(true /* fromInit */);
+                ActivityPreloadUtil.preloadOverviewForTIS(tis, true /* fromInit */);
             }));
         }
 
@@ -350,16 +349,6 @@
             ));
         }
 
-        /**
-         * Preloads the Overview activity.
-         * <p>
-         * This method should only be used when the All Set page of the SUW is reached to safely
-         * preload the Launcher for the SUW first reveal.
-         */
-        public void preloadOverviewForSUWAllSet() {
-            executeForTouchInteractionService(tis -> tis.preloadOverview(false, true));
-        }
-
         @Override
         public void onRotationProposal(int rotation, boolean isValid) {
             executeForTaskbarManager(taskbarManager ->
@@ -1035,47 +1024,6 @@
         }
     }
 
-    private void preloadOverview(boolean fromInit) {
-        Trace.beginSection("preloadOverview(fromInit=" + fromInit + ")");
-        preloadOverview(fromInit, false);
-        Trace.endSection();
-    }
-
-    private void preloadOverview(boolean fromInit, boolean forSUWAllSet) {
-        if (!LockedUserState.get(this).isUserUnlocked()) {
-            return;
-        }
-
-        if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
-            // Prevent the overview from being started before the real home on first boot.
-            return;
-        }
-
-        if ((RestoreDbTask.isPending(this) && !forSUWAllSet)
-                || !mDeviceState.isUserSetupComplete()) {
-            // Preloading while a restore is pending may cause launcher to start the restore
-            // too early.
-            return;
-        }
-
-        final BaseContainerInterface containerInterface =
-                mOverviewComponentObserver.getContainerInterface();
-        final Intent overviewIntent = new Intent(
-                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
-        if (containerInterface.getCreatedContainer() != null && fromInit) {
-            // The activity has been created before the initialization of overview service. It is
-            // usually happens when booting or launcher is the top activity, so we should already
-            // have the latest state.
-            return;
-        }
-
-        // TODO(b/258022658): Remove temporary logging.
-        Log.i(TAG, "preloadOverview: forSUWAllSet=" + forSUWAllSet
-                + ", isHomeAndOverviewSame=" + mOverviewComponentObserver.isHomeAndOverviewSame());
-        ActiveGestureProtoLogProxy.logPreloadRecentsAnimation();
-        mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
-    }
-
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         if (!LockedUserState.get(this).isUserUnlocked()) {
@@ -1103,7 +1051,7 @@
             return;
         }
 
-        preloadOverview(false /* fromInit */);
+        ActivityPreloadUtil.preloadOverviewForTIS(this, false /* fromInit */);
     }
 
     private static boolean isTablet(Configuration config) {
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index 549c15b..1d40d76 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -22,6 +22,7 @@
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.util.AsyncClockEventDelegate;
 
@@ -46,4 +47,6 @@
     OverviewComponentObserver getOverviewComponentObserver();
 
     DesktopVisibilityController getDesktopVisibilityController();
+
+    TopTaskTracker getTopTaskTracker();
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 76da4af..d9209bf 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -284,11 +284,7 @@
         }
 
         if (finalState != OVERVIEW_SPLIT_SELECT) {
-            if (FeatureFlags.enableSplitContextually()) {
-                mSplitSelectStateController.resetState();
-            } else {
-                resetFromSplitSelectionState();
-            }
+            mSplitSelectStateController.resetState();
         }
 
         // disabling this so app icons aren't drawn on top of recent tasks.
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index a9259d9..e7e9f51 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -17,13 +17,14 @@
 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
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.Executors
 import com.android.quickstep.DisplayModel
 import com.android.quickstep.FallbackWindowInterface
 import com.android.quickstep.dagger.QuickstepBaseAppComponent
@@ -31,7 +32,9 @@
 import javax.inject.Inject
 
 @LauncherAppSingleton
-class RecentsDisplayModel @Inject constructor(@ApplicationContext context: Context) :
+class RecentsDisplayModel
+@Inject
+constructor(@ApplicationContext context: Context, tracker: DaggerSingletonTracker) :
     DisplayModel<RecentsDisplayResource>(context) {
 
     companion object {
@@ -47,17 +50,36 @@
 
     init {
         if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
-            displayManager.registerDisplayListener(displayListener, Handler.getMain())
-            createDisplayResource(Display.DEFAULT_DISPLAY)
+            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() }
         }
     }
 
     override fun createDisplayResource(displayId: Int) {
-        if (DEBUG) Log.d(TAG, "create: displayId=$displayId")
+        if (DEBUG) Log.d(TAG, "createDisplayResource: displayId=$displayId")
         getDisplayResource(displayId)?.let {
             return
         }
         val display = displayManager.getDisplay(displayId)
+        if (display == null) {
+            if (DEBUG)
+                Log.w(
+                    TAG,
+                    "createDisplayResource: could not create display for displayId=$displayId",
+                    Exception(),
+                )
+            return
+        }
+        storeRecentsDisplayResource(displayId, display)
+    }
+
+    private fun storeRecentsDisplayResource(displayId: Int, display: Display) {
         displayResourceArray[displayId] =
             RecentsDisplayResource(displayId, context.createDisplayContext(display))
     }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index e22736c..5d99aec 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -163,6 +163,7 @@
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
         callbacks?.removeListener(recentsAnimationListener)
         recentsWindowTracker.onContextDestroyed(this)
+        recentsView?.destroy()
     }
 
     override fun startHome() {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 390d097..b2e7015 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -57,6 +57,7 @@
     private final int mTouchSlop;
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
+    private long mDownTime;
     private final long mTimeForLongPress;
     private int mActivePointerId = INVALID_POINTER_ID;
 
@@ -81,6 +82,7 @@
         final int action = ev.getAction();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                mDownTime = System.currentTimeMillis();
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
@@ -120,13 +122,15 @@
                 }
                 break;
             case MotionEvent.ACTION_UP:
+                long tapTime = System.currentTimeMillis() - mDownTime;
                 boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
                         && mBubbleBarSwipeController.isSwipeGesture();
                 // Anything less than a long-press is a tap
-                boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForLongPress;
+                boolean isWithinTapTime = tapTime <= mTimeForLongPress;
                 Log.d(TAG, "ACTION_UP swipeUp=" + swipeUpOnBubbleHandle + " isInTapTime="
-                        + isWithinTapTime + " passedTouchSlop=" + mPassedTouchSlop
-                        + " stashedOrCollapsedOnDown=" + mStashedOrCollapsedOnDown);
+                        + isWithinTapTime + " tapTime=" + tapTime + " passedTouchSlop="
+                        + mPassedTouchSlop + " stashedOrCollapsedOnDown="
+                        + mStashedOrCollapsedOnDown);
                 if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
                         && mStashedOrCollapsedOnDown) {
                     Log.d(TAG, "ACTION_UP showing bubble bar");
@@ -153,6 +157,7 @@
         }
         mPassedTouchSlop = false;
         mPilfered = false;
+        mDownTime = 0;
     }
 
     private boolean isCollapsed() {
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/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 4995e77..c986b88 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -73,6 +73,7 @@
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
 import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.ActivityPreloadUtil;
 import com.android.quickstep.util.LottieAnimationColorUtils;
 import com.android.quickstep.util.TISBindHelper;
 
@@ -202,6 +203,7 @@
 
         OverviewComponentObserver.INSTANCE.get(this)
                 .addOverviewChangeListener(mOverviewChangeListener);
+        ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
     }
 
     private InvariantDeviceProfile getIDP() {
@@ -291,7 +293,6 @@
     private void onTISConnected(TISBinder binder) {
         setSetupUIVisible(isResumed());
         binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
-        binder.preloadOverviewForSUWAllSet();
         TaskbarManager taskbarManager = binder.getTaskbarManager();
         if (taskbarManager != null) {
             mLauncherStartAnim = taskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION);
@@ -299,10 +300,7 @@
     }
 
     private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
-        TISBinder binder = mTISBindHelper.getBinder();
-        if (binder != null) {
-            binder.preloadOverviewForSUWAllSet();
-        }
+        ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 7d14a3e..0fc95e2 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -80,8 +80,8 @@
     private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips";
 
     private static final int FEEDBACK_ANIMATION_MS = 133;
-    private static final int RIPPLE_VISIBLE_MS = 300;
-    private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
+    private static final int SUBTITLE_ANNOUNCE_DELAY_MS = 3000;
+    private static final int DONE_BUTTON_ANNOUNCE_DELAY_MS = 4000;
     private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 3000;
     private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000;
     protected float mExitingAppEndingCornerRadius;
@@ -124,10 +124,12 @@
     // These runnables  should be used when posting callbacks to their views and cleared from their
     // views before posting new callbacks.
     private final Runnable mTitleViewCallback;
+    private final Runnable mSubtitleViewCallback;
     @Nullable private Runnable mFeedbackViewCallback;
     @Nullable private Runnable mFakeTaskViewCallback;
     @Nullable private Runnable mFakeTaskbarViewCallback;
     private final Runnable mShowFeedbackRunnable;
+    private final AccessibilityManager mAccessibilityManager;
 
     TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
         mTutorialFragment = tutorialFragment;
@@ -175,6 +177,7 @@
 
         mFeedbackTitleView.setText(getIntroductionTitle());
         mFeedbackSubtitleView.setText(getIntroductionSubtitle());
+
         mExitingAppView.setClipToOutline(true);
         mExitingAppView.setOutlineProvider(new ViewOutlineProvider() {
             @Override
@@ -183,8 +186,16 @@
             }
         });
 
-        mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
-                AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+        mTitleViewCallback = () -> {
+            mFeedbackTitleView.requestFocus();
+            mFeedbackTitleView.sendAccessibilityEvent(
+                    AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        };
+        mSubtitleViewCallback = () -> {
+            mFeedbackSubtitleView.requestFocus();
+            mFeedbackSubtitleView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        };
         mShowFeedbackRunnable = () -> {
             mFeedbackView.setAlpha(0f);
             mFeedbackView.setScaleX(0.95f);
@@ -203,10 +214,10 @@
                             mFeedbackViewCallback = mTutorialFragment::continueTutorial;
                             mFeedbackView.postDelayed(
                                     mFeedbackViewCallback,
-                                    AccessibilityManager.getInstance(mContext)
-                                            .getRecommendedTimeoutMillis(
-                                                    ADVANCE_TUTORIAL_TIMEOUT_MS,
-                                                    AccessibilityManager.FLAG_CONTENT_TEXT));
+                                    mAccessibilityManager.getRecommendedTimeoutMillis(
+                                            ADVANCE_TUTORIAL_TIMEOUT_MS,
+                                            AccessibilityManager.FLAG_CONTENT_TEXT
+                                                    | AccessibilityManager.FLAG_CONTENT_CONTROLS));
                         }
                     })
                     .start();
@@ -404,6 +415,7 @@
             int subtitleResId,
             boolean isGestureSuccessful) {
         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
+        mFeedbackSubtitleView.removeCallbacks(mSubtitleViewCallback);
         if (mFeedbackViewCallback != null) {
             mFeedbackView.removeCallbacks(mFeedbackViewCallback);
             mFeedbackViewCallback = null;
@@ -411,6 +423,15 @@
 
         mFeedbackTitleView.setText(titleResId);
         mFeedbackSubtitleView.setText(subtitleResId);
+        mFeedbackTitleView.postDelayed(mTitleViewCallback, mAccessibilityManager
+                .getRecommendedTimeoutMillis(
+                        FEEDBACK_ANIMATION_MS,
+                        AccessibilityManager.FLAG_CONTENT_TEXT));
+        mFeedbackSubtitleView.postDelayed(mSubtitleViewCallback, mAccessibilityManager
+                .getRecommendedTimeoutMillis(
+                        SUBTITLE_ANNOUNCE_DELAY_MS,
+                        AccessibilityManager.FLAG_CONTENT_TEXT));
+
         if (isGestureSuccessful) {
             if (mTutorialFragment.isAtFinalStep()) {
                 showActionButton();
@@ -467,6 +488,7 @@
             mFakeTaskbarViewCallback = null;
         }
         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
+        mFeedbackSubtitleView.removeCallbacks(mSubtitleViewCallback);
     }
 
     private void playFeedbackAnimation() {
@@ -542,6 +564,13 @@
         mSkipButton.setVisibility(GONE);
         mDoneButton.setVisibility(View.VISIBLE);
         mDoneButton.setOnClickListener(this::onActionButtonClicked);
+        mDoneButton.postDelayed(() -> {
+            mDoneButton.requestFocus();
+            mDoneButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+        }, mAccessibilityManager
+                .getRecommendedTimeoutMillis(
+                        DONE_BUTTON_ANNOUNCE_DELAY_MS,
+                        AccessibilityManager.FLAG_CONTENT_CONTROLS));
     }
 
     void hideFakeTaskbar(boolean animateToHotseat) {
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..639d3a7 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,11 +41,14 @@
 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
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -66,6 +71,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 +93,12 @@
         defStyleAttr: Int,
     ) : super(context, attrs, defStyleAttr)
 
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+
+        maybeCreateHeader()
+    }
+
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
         viewAttachedScope =
@@ -96,22 +109,28 @@
         updateViewDataValues()
         viewModel = RecentsDependencies.get(this)
         viewModel.uiState
+            .dropWhile { it == Uninitialized }
+            .flowOn(dispatcherProvider.background)
             .onEach { viewModelUiState ->
                 Log.d(TAG, "viewModelUiState changed from: $uiState to: $viewModelUiState")
                 uiState = viewModelUiState
                 resetViews()
                 when (viewModelUiState) {
                     is Uninitialized -> {}
-                    is LiveTile -> drawLiveWindow()
+                    is LiveTile -> drawLiveWindow(viewModelUiState)
                     is SnapshotSplash -> drawSnapshotSplash(viewModelUiState)
                     is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
                 }
             }
             .launchIn(viewAttachedScope)
         viewModel.dimProgress
+            .dropWhile { it == 0f }
+            .flowOn(dispatcherProvider.background)
             .onEach { dimProgress -> scrimView.alpha = dimProgress }
             .launchIn(viewAttachedScope)
         viewModel.splashAlpha
+            .dropWhile { it == 0f }
+            .flowOn(dispatcherProvider.background)
             .onEach { splashAlpha ->
                 splashBackground.alpha = splashAlpha
                 splashIcon.alpha = splashAlpha
@@ -137,6 +156,7 @@
 
     override fun onRecycle() {
         uiState = Uninitialized
+        resetViews()
     }
 
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
@@ -179,14 +199,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 +223,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 +241,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/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index 677875c..f51660b 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -32,6 +32,8 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -79,6 +81,8 @@
                 dispatcherProvider = RecentsDependencies.get(),
             )
         viewModel.overlayState
+            .dropWhile { it == Disabled }
+            .flowOn(dispatcherProvider.background)
             .onEach {
                 uiState = it
                 if (it is Enabled) {
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/util/ActivityPreloadUtil.kt b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt
new file mode 100644
index 0000000..47b39db
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.Intent
+import android.os.Trace
+import com.android.launcher3.provider.RestoreDbTask
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LockedUserState
+import com.android.quickstep.OverviewComponentObserver
+import com.android.quickstep.RecentsAnimationDeviceState
+import com.android.systemui.shared.system.ActivityManagerWrapper
+
+/** Utility class for preloading overview */
+object ActivityPreloadUtil {
+
+    @JvmStatic
+    fun preloadOverviewForSUWAllSet(ctx: Context) {
+        preloadOverview(ctx, fromInit = false, forSUWAllSet = true)
+    }
+
+    @JvmStatic
+    fun preloadOverviewForTIS(ctx: Context, fromInit: Boolean) {
+        preloadOverview(ctx, fromInit = fromInit, forSUWAllSet = false)
+    }
+
+    private fun preloadOverview(ctx: Context, fromInit: Boolean, forSUWAllSet: Boolean) {
+        Trace.beginSection("preloadOverview(fromInit=$fromInit, forSUWAllSet=$forSUWAllSet)")
+
+        try {
+            if (!LockedUserState.get(ctx).isUserUnlocked) return
+
+            val deviceState = RecentsAnimationDeviceState.INSTANCE[ctx]
+            val overviewCompObserver = OverviewComponentObserver.INSTANCE[ctx]
+
+            // Prevent the overview from being started before the real home on first boot
+            if (deviceState.isButtonNavMode && !overviewCompObserver.isHomeAndOverviewSame) return
+
+            // Preloading while a restore is pending may cause launcher to start the restore too
+            // early
+            if ((RestoreDbTask.isPending(ctx) && !forSUWAllSet) || !deviceState.isUserSetupComplete)
+                return
+
+            // The activity has been created before the initialization of overview service. It is
+            // usually happens when booting or launcher is the top activity, so we should already
+            // have the latest state.
+            if (fromInit && overviewCompObserver.containerInterface.createdContainer != null) return
+
+            ActiveGestureProtoLogProxy.logPreloadRecentsAnimation()
+            val overviewIntent = Intent(overviewCompObserver.overviewIntentIgnoreSysUiState)
+            Executors.UI_HELPER_EXECUTOR.execute {
+                ActivityManagerWrapper.getInstance().preloadRecentsActivity(overviewIntent)
+            }
+        } finally {
+            Trace.endSection()
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
deleted file mode 100644
index fc4fc4d..0000000
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.util;
-
-import androidx.annotation.NonNull;
-
-import com.android.quickstep.views.TaskViewType;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Task} container that can contain N number of tasks that are part of the desktop in
- * recent tasks list.
- */
-public class DesktopTask extends GroupTask {
-
-    @NonNull
-    public final List<Task> tasks;
-
-    public DesktopTask(@NonNull List<Task> tasks) {
-        super(tasks.get(0), null, null, TaskViewType.DESKTOP);
-        this.tasks = tasks;
-    }
-
-    @Override
-    public boolean containsTask(int taskId) {
-        for (Task task : tasks) {
-            if (task.key.id == taskId) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public boolean hasMultipleTasks() {
-        return tasks.size() > 1;
-    }
-
-    @Override
-    public boolean supportsMultipleTasks() {
-        return true;
-    }
-
-    @Override
-    @NonNull
-    public List<Task> getTasks() {
-        return tasks;
-    }
-
-    @Override
-    public DesktopTask copy() {
-        return new DesktopTask(tasks);
-    }
-
-    @Override
-    public String toString() {
-        return "type=" + taskViewType + " tasks=" + tasks;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof DesktopTask that)) return false;
-        if (!super.equals(o)) return false;
-        return Objects.equals(tasks, that.tasks);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(super.hashCode(), tasks);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.kt b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
new file mode 100644
index 0000000..1cee2d2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util
+
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import java.util.Objects
+
+/**
+ * A [Task] container that can contain N number of tasks that are part of the desktop in recent
+ * tasks list.
+ */
+class DesktopTask(override val tasks: List<Task>) :
+    GroupTask(tasks[0], null, null, TaskViewType.DESKTOP) {
+
+    override fun containsTask(taskId: Int) = tasks.any { it.key.id == taskId }
+
+    override fun hasMultipleTasks() = tasks.size > 1
+
+    override fun supportsMultipleTasks() = true
+
+    override fun copy() = DesktopTask(tasks)
+
+    override fun toString() = "type=$taskViewType tasks=$tasks"
+
+    override fun equals(o: Any?): Boolean {
+        if (this === o) return true
+        if (o !is DesktopTask) return false
+        if (!super.equals(o)) return false
+        return tasks == o.tasks
+    }
+
+    override fun hashCode() = Objects.hash(super.hashCode(), tasks)
+}
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
deleted file mode 100644
index 7aeeb2f..0000000
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.util;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.views.TaskViewType;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Task} container that can contain one or two tasks, depending on if the two tasks
- * are represented as an app-pair in the recents task list.
- */
-public class GroupTask {
-    @NonNull
-    public final Task task1;
-    @Nullable
-    public final Task task2;
-    @Nullable
-    public final SplitBounds mSplitBounds;
-    public final TaskViewType taskViewType;
-
-    public GroupTask(@NonNull Task task) {
-        this(task, null, null);
-    }
-
-    public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) {
-        this(t1, t2, splitBounds, t2 != null ? TaskViewType.GROUPED : TaskViewType.SINGLE);
-    }
-
-    protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds,
-            TaskViewType taskViewType) {
-        task1 = t1;
-        task2 = t2;
-        mSplitBounds = splitBounds;
-        this.taskViewType = taskViewType;
-    }
-
-    public boolean containsTask(int taskId) {
-        return task1.key.id == taskId || (task2 != null && task2.key.id == taskId);
-    }
-
-    public boolean hasMultipleTasks() {
-        return task2 != null;
-    }
-
-    /**
-     * Returns whether this task supports multiple tasks or not.
-     */
-    public boolean supportsMultipleTasks() {
-        return taskViewType == TaskViewType.GROUPED;
-    }
-
-    /**
-     * Returns a List of all the Tasks in this GroupTask
-     */
-    public List<Task> getTasks() {
-        if (task2 == null) {
-            return Collections.singletonList(task1);
-        } else {
-            return Arrays.asList(task1, task2);
-        }
-    }
-
-    /**
-     * Create a copy of this instance
-     */
-    public GroupTask copy() {
-        return new GroupTask(
-                new Task(task1),
-                task2 != null ? new Task(task2) : null,
-                mSplitBounds);
-    }
-
-    @Override
-    public String toString() {
-        return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof GroupTask that)) return false;
-        return taskViewType == that.taskViewType && Objects.equals(task1,
-                that.task1) && Objects.equals(task2, that.task2)
-                && Objects.equals(mSplitBounds, that.mSplitBounds);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(task1, task2, mSplitBounds, taskViewType);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.kt b/quickstep/src/com/android/quickstep/util/GroupTask.kt
new file mode 100644
index 0000000..1dd8d18
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util
+
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import java.util.Objects
+
+/**
+ * A [Task] container that can contain one or two tasks, depending on if the two tasks are
+ * represented as an app-pair in the recents task list.
+ */
+open class GroupTask
+@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+constructor(
+    @JvmField val task1: Task,
+    @JvmField val task2: Task?,
+    @JvmField val mSplitBounds: SplitConfigurationOptions.SplitBounds?,
+    @JvmField val taskViewType: TaskViewType,
+) {
+    constructor(task: Task) : this(task, null, null)
+
+    constructor(
+        t1: Task,
+        t2: Task?,
+        splitBounds: SplitConfigurationOptions.SplitBounds?,
+    ) : this(t1, t2, splitBounds, if (t2 != null) TaskViewType.GROUPED else TaskViewType.SINGLE)
+
+    open fun containsTask(taskId: Int) =
+        task1.key.id == taskId || (task2 != null && task2.key.id == taskId)
+
+    open fun hasMultipleTasks() = task2 != null
+
+    /** Returns whether this task supports multiple tasks or not. */
+    open fun supportsMultipleTasks() = taskViewType == TaskViewType.GROUPED
+
+    /** Returns a List of all the Tasks in this GroupTask */
+    open val tasks: List<Task>
+        get() = listOfNotNull(task1, task2)
+
+    /** Creates a copy of this instance */
+    open fun copy() = GroupTask(Task(task1), if (task2 != null) Task(task2) else null, mSplitBounds)
+
+    override fun toString() = "type=$taskViewType task1=$task1 task2=$task2"
+
+    override fun equals(o: Any?): Boolean {
+        if (this === o) return true
+        if (o !is GroupTask) return false
+        return taskViewType == o.taskViewType &&
+            task1 == o.task1 &&
+            task2 == o.task2 &&
+            mSplitBounds == o.mSplitBounds
+    }
+
+    override fun hashCode() = Objects.hash(task1, task2, mSplitBounds, taskViewType)
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index f708f4b..9b4c772 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -486,11 +486,7 @@
 
         pendingAnimation.addEndListener {
             splitSelectStateController.launchInitialAppFullscreen {
-                if (FeatureFlags.enableSplitContextually()) {
-                    splitSelectStateController.resetState()
-                } else if (resetCallback.isPresent) {
-                    resetCallback.get().run()
-                }
+                splitSelectStateController.resetState()
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index c524286..6b59c1b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -169,7 +169,7 @@
     private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
         @Override
         public boolean canHandleBack() {
-            return FeatureFlags.enableSplitContextually() && isSplitSelectActive();
+            return isSplitSelectActive();
         }
 
         @Override
@@ -739,8 +739,7 @@
     }
 
     /**
-     * To be called whenever we exit split selection state. If
-     * {@link FeatureFlags#enableSplitContextually()} is set, this should be the
+     * To be called whenever we exit split selection state. This should be the
      * central way split is getting reset, which should then go through the callbacks to reset
      * other state.
      */
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 86090d5..d3390b4 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -219,6 +219,6 @@
     }
 
     private boolean shouldIgnoreSecondSplitLaunch() {
-        return !FeatureFlags.enableSplitContextually() || !mController.isSplitSelectActive();
+        return !mController.isSplitSelectActive();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 425c4fe..6ba733a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -73,9 +72,8 @@
 
     @BinderThread
     public void enterStageSplit(boolean leftOrTop) {
-        if (!enableSplitContextually() ||
-                // Do not enter stage split from keyboard shortcuts if the user is already in split
-                TopTaskTracker.INSTANCE.get(mLauncher).getRunningSplitTaskIds().length == 2) {
+        if (TopTaskTracker.INSTANCE.get(mLauncher).getRunningSplitTaskIds().length == 2) {
+            // Do not enter stage split from keyboard shortcuts if the user is already in split
             return;
         }
         RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index d6fe049..9be462c 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -255,11 +255,7 @@
 
     @Override
     public boolean canLaunchFullscreenTask() {
-        if (FeatureFlags.enableSplitContextually()) {
-            return !mSplitSelectStateController.isSplitSelectActive();
-        } else {
-            return !mContainer.isInState(OVERVIEW_SPLIT_SELECT);
-        }
+        return !mSplitSelectStateController.isSplitSelectActive();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9b91027..e621d0c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -209,7 +209,6 @@
 import com.android.quickstep.recents.viewmodel.RecentsViewModel;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AnimUtils;
-import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
@@ -550,6 +549,8 @@
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
+    private final Rect mTopRowDeadZoneRect = new Rect();
+    private final Rect mBottomRowDeadZoneRect = new Rect();
     /**
      * Reflects if Recents is currently in the middle of a gesture, and if so, which tasks are
      * running. If a gesture is not in progress, this will be null.
@@ -848,6 +849,8 @@
 
     private final Matrix mTmpMatrix = new Matrix();
 
+    private int mTaskViewCount = 0;
+
     @Nullable
     public TaskView getFirstTaskView() {
         return mUtils.getFirstTaskView();
@@ -924,8 +927,11 @@
         mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
         mEmptyMessagePaint.setTextSize(getResources()
                 .getDimension(R.dimen.recents_empty_message_text_size));
-        mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
-                Typeface.NORMAL));
+        Typeface typeface = Typeface.create(
+                Typeface.create(Themes.getDefaultBodyFont(context), Typeface.NORMAL),
+                getFontWeight(),
+                false);
+        mEmptyMessagePaint.setTypeface(typeface);
         mEmptyMessagePaint.setAntiAlias(true);
         mEmptyMessagePadding = getResources()
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
@@ -1211,9 +1217,7 @@
                 mIPipAnimationListener);
         mOrientationState.initListeners();
         mTaskOverlayFactory.initListeners();
-        if (FeatureFlags.enableSplitContextually()) {
-            mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
-        }
+        mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
     }
 
     @Override
@@ -1233,12 +1237,17 @@
         mIPipAnimationListener.setActivityAndRecentsView(null, null);
         mOrientationState.destroyListeners();
         mTaskOverlayFactory.removeListeners();
-        if (FeatureFlags.enableSplitContextually()) {
-            mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
-        }
+        mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
         reset();
+    }
+
+    /**
+     * Execute clean-up logic needed when the view is destroyed.
+     */
+    public void destroy() {
+        Log.d(TAG, "destroy");
         if (enableRefactorTaskThumbnail()) {
-            mHelper.onDetachedFromWindow();
+            mHelper.onDestroy();
             RecentsDependencies.destroy();
         }
     }
@@ -1250,9 +1259,11 @@
         // - It's the initial taskview for entering split screen, we only pretend to dismiss the
         // task
         // - It's the focused task to be moved to the front, we immediately re-add the task
-        if (child instanceof TaskView && child != mSplitHiddenTaskView
-                && child != mMovingTaskView) {
-            clearAndRecycleTaskView((TaskView) child);
+        if (child instanceof TaskView) {
+            mTaskViewCount = Math.max(0, --mTaskViewCount);
+            if (child != mSplitHiddenTaskView && child != mMovingTaskView) {
+                clearAndRecycleTaskView((TaskView) child);
+            }
         }
     }
 
@@ -1273,6 +1284,9 @@
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
+        if (child instanceof TaskView) {
+            mTaskViewCount++;
+        }
         child.setAlpha(mContentAlpha);
         // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
         // child direction back to match system settings.
@@ -1704,8 +1718,11 @@
                                 mClearAllButton.getAlpha() == 1
                                         && mClearAllButtonDeadZoneRect.contains(x, y);
                         final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+                        int adjustedX = x + getScrollX();
                         if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
-                                && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
+                                && !mTaskViewDeadZoneRect.contains(adjustedX, y)
+                                && !mTopRowDeadZoneRect.contains(adjustedX, y)
+                                && !mBottomRowDeadZoneRect.contains(adjustedX, y)) {
                             mTouchDownToStartHome = true;
                         }
                     }
@@ -1953,7 +1970,7 @@
             } else if (taskView instanceof DesktopTaskView) {
                 // Minimized tasks should not be shown in Overview
                 List<Task> nonMinimizedTasks =
-                        ((DesktopTask) groupTask).tasks.stream()
+                        groupTask.getTasks().stream()
                                 .filter(task -> !task.isMinimized)
                                 .toList();
                 ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
@@ -2083,15 +2100,11 @@
 
     /** Returns true if there are at least one TaskView has been added to the RecentsView. */
     public boolean hasTaskViews() {
-        return CollectionsKt.any(getTaskViews());
+        return mUtils.hasTaskViews();
     }
 
     public int getTaskViewCount() {
-        int taskViewCount = getChildCount();
-        if (indexOfChild(mClearAllButton) != -1) {
-            taskViewCount--;
-        }
-        return taskViewCount;
+        return mTaskViewCount;
     }
 
     /**
@@ -2634,9 +2647,6 @@
         }
         runActionOnRemoteHandles(remoteTargetHandle ->
                 remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
-        if (!FeatureFlags.enableSplitContextually()) {
-            resetFromSplitSelectionState();
-        }
 
         // These are relatively expensive and don't need to be done this frame (RecentsView isn't
         // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
@@ -2683,7 +2693,7 @@
     }
 
     @Nullable
-    private TaskView getTaskViewFromTaskViewId(int taskViewId) {
+    TaskView getTaskViewFromTaskViewId(int taskViewId) {
         if (taskViewId == -1) {
             return null;
         }
@@ -3609,11 +3619,7 @@
                 InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
             } else {
                 // If transition to split select was interrupted, clean up to prevent glitches
-                if (FeatureFlags.enableSplitContextually()) {
-                    mSplitSelectStateController.resetState();
-                } else {
-                    resetFromSplitSelectionState();
-                }
+                mSplitSelectStateController.resetState();
                 InteractionJankMonitorWrapper.cancel(Cuj.CUJ_SPLIT_SCREEN_ENTER);
             }
 
@@ -4026,8 +4032,6 @@
                         } else {
                             removeTaskInternal(dismissedTaskView);
                         }
-                        announceForAccessibility(
-                                getResources().getString(R.string.task_view_closed));
                         mContainer.getStatsLogManager().logger()
                                 .withItemInfo(dismissedTaskView.getFirstItemInfo())
                                 .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
@@ -4211,6 +4215,12 @@
                 onDismissAnimationEnds();
                 mPendingAnimation = null;
                 mTaskViewsDismissPrimaryTranslations.clear();
+
+                if (!dismissingForSplitSelection && success) {
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS);
+                } else if (!dismissingForSplitSelection) {
+                    InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS);
+                }
             }
         });
     }
@@ -4336,7 +4346,7 @@
     /**
      * Returns all the tasks in the top row, without the focused task
      */
-    private IntArray getTopRowIdArray() {
+    IntArray getTopRowIdArray() {
         if (mTopRowIdSet.isEmpty()) {
             return new IntArray(0);
         }
@@ -4353,7 +4363,7 @@
     /**
      * Returns all the tasks in the bottom row, without the focused task
      */
-    private IntArray getBottomRowIdArray() {
+    IntArray getBottomRowIdArray() {
         int bottomRowIdArraySize = getBottomRowTaskCountForTablet();
         if (bottomRowIdArraySize <= 0) {
             return new IntArray(0);
@@ -4514,6 +4524,7 @@
     }
 
     public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
+        InteractionJankMonitorWrapper.begin(this, Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS);
         PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION);
         createTaskDismissAnimation(pa, taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION,
                 false /* dismissingForSplitSelection*/);
@@ -5333,11 +5344,7 @@
             mSplitSelectStateController.launchSplitTasks(
                     aBoolean1 -> {
                         InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
-                        if (FeatureFlags.enableSplitContextually()) {
-                            mSplitSelectStateController.resetState();
-                        } else {
-                            resetFromSplitSelectionState();
-                        }
+                        mSplitSelectStateController.resetState();
                     });
         });
 
@@ -5359,17 +5366,14 @@
 
     @SuppressLint("WrongCall")
     protected void resetFromSplitSelectionState() {
-        if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1 ||
-                FeatureFlags.enableSplitContextually()) {
-            safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
-            safeRemoveDragLayerView(mSecondFloatingTaskView);
-            safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
-            safeRemoveDragLayerView(mSplitScrim);
-            mSecondFloatingTaskView = null;
-            mSplitSelectSource = null;
-            mSplitSelectStateController.getSplitAnimationController()
-                    .removeSplitInstructionsView(mContainer);
-        }
+        safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
+        safeRemoveDragLayerView(mSecondFloatingTaskView);
+        safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
+        safeRemoveDragLayerView(mSplitScrim);
+        mSecondFloatingTaskView = null;
+        mSplitSelectSource = null;
+        mSplitSelectStateController.getSplitAnimationController()
+                .removeSplitInstructionsView(mContainer);
 
         if (mSecondSplitHiddenView != null) {
             mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
@@ -5381,11 +5385,6 @@
         setTaskViewsPrimarySplitTranslation(0);
         setTaskViewsSecondarySplitTranslation(0);
 
-        if (!FeatureFlags.enableSplitContextually()) {
-            // When flag is on, this method gets called from resetState() call below, let's avoid
-            // infinite recursion today
-            mSplitSelectStateController.resetState();
-        }
         if (mSplitHiddenTaskViewIndex == -1) {
             return;
         }
@@ -5484,15 +5483,8 @@
             mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
         }
 
-        // Get the deadzone rect between the task views
-        mTaskViewDeadZoneRect.setEmpty();
-        if (hasTaskViews()) {
-            final View firstTaskView = getFirstTaskView();
-            mUtils.getLastTaskView().getHitRect(mTaskViewDeadZoneRect);
-            mTaskViewDeadZoneRect.union(firstTaskView.getLeft(), firstTaskView.getTop(),
-                    firstTaskView.getRight(),
-                    firstTaskView.getBottom());
-        }
+        mUtils.updateTaskViewDeadZoneRect(mTaskViewDeadZoneRect, mTopRowDeadZoneRect,
+                mBottomRowDeadZoneRect);
     }
 
     private void updateEmptyStateUi(boolean sizeChanged) {
@@ -6816,6 +6808,14 @@
         }
     }
 
+    private int getFontWeight() {
+        int fontWeightAdjustment = getResources().getConfiguration().fontWeightAdjustment;
+        if (fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+            return Typeface.Builder.NORMAL_WEIGHT + fontWeightAdjustment;
+        }
+        return Typeface.Builder.NORMAL_WEIGHT;
+    }
+
     public interface TaskLaunchListener {
         void onTaskLaunched();
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index d92c4d0..ff711da 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -32,8 +32,8 @@
     private val recentsCoroutineScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
 ) {
-    fun onDetachedFromWindow() {
-        recentsCoroutineScope.cancel("RecentsView detaching from window")
+    fun onDestroy() {
+        recentsCoroutineScope.cancel("RecentsView is being destroyed")
     }
 
     fun switchToScreenshot(
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 3ac3349..dcb954a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -16,9 +16,11 @@
 
 package com.android.quickstep.views
 
+import android.graphics.Rect
 import android.view.View
 import androidx.core.view.children
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
+import com.android.launcher3.util.IntArray
 import com.android.quickstep.util.GroupTask
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
@@ -169,4 +171,78 @@
         FULL_SCREEN,
         DESKTOP,
     }
+
+    /** Returns true if there are at least one TaskView has been added to the RecentsView. */
+    fun hasTaskViews() = taskViews.any()
+
+    private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) {
+        outRowRect.setEmpty()
+        firstView?.let {
+            it.getHitRect(TEMP_RECT)
+            outRowRect.union(TEMP_RECT)
+        }
+        lastView?.let {
+            it.getHitRect(TEMP_RECT)
+            outRowRect.union(TEMP_RECT)
+        }
+    }
+
+    private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) {
+        if (rowTaskViewIds.isEmpty) {
+            outRowRect.setEmpty()
+            return
+        }
+        getRowRect(
+            recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)),
+            recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)),
+            outRowRect,
+        )
+    }
+
+    fun updateTaskViewDeadZoneRect(
+        outTaskViewRowRect: Rect,
+        outTopRowRect: Rect,
+        outBottomRowRect: Rect,
+    ) {
+        if (!(recentsView.mContainer as RecentsViewContainer).deviceProfile.isTablet) {
+            getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect)
+            return
+        }
+        getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect)
+        getRowRect(recentsView.getTopRowIdArray(), outTopRowRect)
+        getRowRect(recentsView.getBottomRowIdArray(), outBottomRowRect)
+
+        // Expand large tile Rect to include space between top/bottom row.
+        val nonEmptyRowRect =
+            when {
+                !outTopRowRect.isEmpty -> outTopRowRect
+                !outBottomRowRect.isEmpty -> outBottomRowRect
+                else -> return
+            }
+        if (recentsView.isRtl) {
+            if (outTaskViewRowRect.left > nonEmptyRowRect.right) {
+                outTaskViewRowRect.left = nonEmptyRowRect.right
+            }
+        } else {
+            if (outTaskViewRowRect.right < nonEmptyRowRect.left) {
+                outTaskViewRowRect.right = nonEmptyRowRect.left
+            }
+        }
+
+        // Expand the shorter row Rect to include the space between the 2 rows.
+        if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return
+        if (outTopRowRect.width() <= outBottomRowRect.width()) {
+            if (outTopRowRect.bottom < outBottomRowRect.top) {
+                outTopRowRect.bottom = outBottomRowRect.top
+            }
+        } else {
+            if (outBottomRowRect.top > outTopRowRect.bottom) {
+                outBottomRowRect.top = outTopRowRect.bottom
+            }
+        }
+    }
+
+    companion object {
+        val TEMP_RECT = Rect()
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index f6393e4..0d80f6a 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -126,33 +126,31 @@
         TextView cancelTextView = findViewById(R.id.split_instructions_text_cancel);
         TextView instructionTextView = findViewById(R.id.split_instructions_text);
 
-        if (FeatureFlags.enableSplitContextually()) {
-            cancelTextView.setVisibility(VISIBLE);
-            cancelTextView.setOnClickListener((v) -> exitSplitSelection());
-            instructionTextView.setText(R.string.toast_contextual_split_select_app);
+        cancelTextView.setVisibility(VISIBLE);
+        cancelTextView.setOnClickListener((v) -> exitSplitSelection());
+        instructionTextView.setText(R.string.toast_contextual_split_select_app);
 
-            // After layout, expand touch target of cancel button to meet minimum a11y measurements.
-            post(() -> {
-                int minTouchSize = getResources()
-                        .getDimensionPixelSize(settingslib_preferred_minimum_touch_target);
-                Rect r = new Rect();
-                cancelTextView.getHitRect(r);
+        // After layout, expand touch target of cancel button to meet minimum a11y measurements.
+        post(() -> {
+            int minTouchSize = getResources()
+                    .getDimensionPixelSize(settingslib_preferred_minimum_touch_target);
+            Rect r = new Rect();
+            cancelTextView.getHitRect(r);
 
-                if (r.width() < minTouchSize) {
-                    // add 1 to ensure ceiling on int division
-                    int expandAmount = (minTouchSize + 1 - r.width()) / 2;
-                    r.left -= expandAmount;
-                    r.right += expandAmount;
-                }
-                if (r.height() < minTouchSize) {
-                    int expandAmount = (minTouchSize + 1 - r.height()) / 2;
-                    r.top -= expandAmount;
-                    r.bottom += expandAmount;
-                }
+            if (r.width() < minTouchSize) {
+                // add 1 to ensure ceiling on int division
+                int expandAmount = (minTouchSize + 1 - r.width()) / 2;
+                r.left -= expandAmount;
+                r.right += expandAmount;
+            }
+            if (r.height() < minTouchSize) {
+                int expandAmount = (minTouchSize + 1 - r.height()) / 2;
+                r.top -= expandAmount;
+                r.bottom += expandAmount;
+            }
 
-                setTouchDelegate(new TouchDelegate(r, cancelTextView));
-            });
-        }
+            setTouchDelegate(new TouchDelegate(r, cancelTextView));
+        });
 
         // Set accessibility title, will be announced by a11y tools.
         instructionTextView.setAccessibilityPaneTitle(instructionTextView.getText());
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/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index f94eb14..dc849f3 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -624,7 +624,11 @@
         with(info) {
             // Only make actions available if the app icon menu is visible to the user.
             // When modalness is >0, the user is in select mode and the icon menu is hidden.
-            if (modalness == 0f) {
+            // When split selection is active, they should only be able to select the app and not
+            // take any other action.
+            val shouldPopulateAccessibilityMenu =
+                modalness == 0f && recentsView?.isSplitSelectionActive == false
+            if (shouldPopulateAccessibilityMenu) {
                 addAction(
                     AccessibilityAction(
                         R.id.action_close,
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index 49fe614..a76f83c 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -20,6 +20,8 @@
 import android.view.LayoutInflater
 import com.android.launcher3.R
 import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
 import org.junit.Rule
@@ -46,7 +48,7 @@
     private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
 
     @Test
-    fun taskThumbnailView_uninitialized() {
+    fun taskThumbnailView_uninitializedByDefault() {
         screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
             activity.actionBar?.hide()
             createTaskThumbnailView(activity)
@@ -54,10 +56,39 @@
     }
 
     @Test
+    fun taskThumbnailView_resetsToUninitialized() {
+        screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
+            activity.actionBar?.hide()
+            val taskThumbnailView = createTaskThumbnailView(activity)
+            taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
+            taskThumbnailViewModel.uiState.value = Uninitialized
+            taskThumbnailView
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_recyclesToUninitialized() {
+        screenshotRule.screenshotTest(
+            "taskThumbnailView_uninitialized",
+            viewProvider = { activity ->
+                activity.actionBar?.hide()
+                val taskThumbnailView = createTaskThumbnailView(activity)
+                taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
+                taskThumbnailView
+            },
+            checkView = { _, taskThumbnailView ->
+                // Call onRecycle() after View is attached (end of block above)
+                (taskThumbnailView as TaskThumbnailView).onRecycle()
+                false
+            },
+        )
+    }
+
+    @Test
     fun taskThumbnailView_backgroundOnly() {
         screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity ->
             activity.actionBar?.hide()
-            taskThumbnailViewModel.uiState.value = TaskThumbnailUiState.BackgroundOnly(Color.YELLOW)
+            taskThumbnailViewModel.uiState.value = BackgroundOnly(Color.YELLOW)
             createTaskThumbnailView(activity)
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
index 3ca36ec..da362bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -94,7 +94,9 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             bubbleAnimator.animateNewAndRemoveOld(
                 selectedBubbleIndex = 3,
-                removedBubbleIndex = 2,
+                newlySelectedBubbleIndex = 2,
+                removedBubbleIndex = 1,
+                addedBubbleIndex = 3,
                 listener,
             )
         }
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 ffb1f23..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
@@ -25,7 +31,6 @@
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.MSDLPlayerWrapper
-import com.android.quickstep.fallback.window.RecentsDisplayModel
 import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.shared.system.InputConsumerController
 import dagger.BindsInstance
@@ -40,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)
@@ -64,19 +70,32 @@
     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)
-                .bindRecentsDisplayModel(RecentsDisplayModel(sandboxContext))
+            DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
         )
         sandboxContext.putObject(
             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))
 
@@ -122,8 +141,6 @@
     interface Builder : LauncherAppComponent.Builder {
         @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
 
-        @BindsInstance fun bindRecentsDisplayModel(model: RecentsDisplayModel): Builder
-
         override fun build(): TestComponent
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 98a3607..8879a01 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -45,6 +45,9 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppModule;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.quickstep.DeviceConfigWrapper;
@@ -56,6 +59,9 @@
 import com.android.quickstep.util.TestExtensions;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
+import dagger.BindsInstance;
+import dagger.Component;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -423,7 +429,10 @@
             mContext.onDestroy();
         }
         mContext = new SandboxContext(getApplicationContext());
-        mContext.putObject(TopTaskTracker.INSTANCE, mTopTaskTracker);
+        mContext.initDaggerComponent(
+                DaggerNavHandleLongPressInputConsumerTest_TopTaskTrackerComponent
+                        .builder()
+                        .bindTopTaskTracker(mTopTaskTracker));
         mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x;
         mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
                 mDeviceState, mNavHandle, mGestureState);
@@ -450,4 +459,17 @@
                 value,
                 () -> DeviceConfigWrapper.get().getEnableLpnhTwoStages());
     }
+
+    @LauncherAppSingleton
+    @Component(modules = LauncherAppModule.class)
+    public interface TopTaskTrackerComponent extends LauncherAppComponent {
+        @Component.Builder
+        interface Builder extends LauncherAppComponent.Builder {
+            @BindsInstance
+            Builder bindTopTaskTracker(TopTaskTracker topTaskTracker);
+
+            @Override
+            TopTaskTrackerComponent build();
+        }
+    }
 }
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/recents/window/RecentsDisplayModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
index d2aa6ac..44ea73e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/window/RecentsDisplayModelTest.kt
@@ -28,14 +28,9 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW
 import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW
-import com.android.launcher3.dagger.LauncherAppComponent
-import com.android.launcher3.dagger.LauncherAppModule
-import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.quickstep.fallback.window.RecentsDisplayModel
-import dagger.BindsInstance
-import dagger.Component
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Rule
@@ -75,10 +70,6 @@
         whenever(displayManager.getDisplay(anyInt())).thenReturn(display)
 
         runOnMainSync { recentsDisplayModel = RecentsDisplayModel.INSTANCE.get(context) }
-        context.initDaggerComponent(
-            DaggerRecentsDisplayModelComponent.builder()
-                .bindRecentsDisplayModel(recentsDisplayModel)
-        )
     }
 
     @Test
@@ -125,14 +116,3 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync { f.run() }
     }
 }
-
-@LauncherAppSingleton
-@Component(modules = [LauncherAppModule::class])
-interface RecentsDisplayModelComponent : LauncherAppComponent {
-    @Component.Builder
-    interface Builder : LauncherAppComponent.Builder {
-        @BindsInstance fun bindRecentsDisplayModel(model: RecentsDisplayModel): Builder
-
-        override fun build(): RecentsDisplayModelComponent
-    }
-}
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/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index e065dba..88be752 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -165,7 +165,6 @@
     }
 
     @Test
-    @ScreenRecordRule.ScreenRecord // b/355466672
     public void testPrivateSpaceLockingBehaviour() throws IOException {
         assumeFalse(mLauncher.isTablet()); // b/367258373
         // Scroll to the bottom of All Apps
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index daa4ec3..afe6368 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -16,8 +16,6 @@
 package com.android.quickstep;
 
 
-import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
-
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
@@ -89,18 +87,10 @@
                 .getSplitScreenMenuItem()
                 .click();
 
-        if (enableSplitContextually()) {
-            // We're staying in all apps, use same instance
-            mLauncher.getAllApps()
-                    .getAppIcon(CALCULATOR_APP_NAME)
-                    .launchIntoSplitScreen();
-        } else {
-            // We're in overview, use taskbar instance
-            mLauncher.getLaunchedAppState()
-                    .getTaskbar()
-                    .getAppIcon(CALCULATOR_APP_NAME)
-                    .launchIntoSplitScreen();
-        }
+        // We're staying in all apps, use same instance
+        mLauncher.getAllApps()
+                .getAppIcon(CALCULATOR_APP_NAME)
+                .launchIntoSplitScreen();
     }
 
     @Test
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/res/values/strings.xml b/res/values/strings.xml
index 501e650..a02516a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -408,9 +408,6 @@
     <!-- Accessibility action to move item to the current location. [CHAR_LIMIT=30] -->
     <string name="action_move_here">Move item here</string>
 
-    <!-- Accessibility confirmation for item added to workspace. -->
-    <string name="item_added_to_workspace">Item added to home screen</string>
-
     <!-- Accessibility confirmation for item removed. [CHAR_LIMIT=50]-->
     <string name="item_removed">Item removed</string>
 
diff --git a/res/xml/folder_shapes.xml b/res/xml/folder_shapes.xml
deleted file mode 100644
index e60d333..0000000
--- a/res/xml/folder_shapes.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2019 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.
--->
-<shapes xmlns:launcher="http://schemas.android.com/apk/res-auto" >
-
-    <Circle launcher:folderIconRadius="1" />
-
-    <!-- Default icon for AOSP -->
-    <RoundedSquare launcher:folderIconRadius="0.16" />
-
-    <!-- Rounded icon from RRO -->
-    <RoundedSquare launcher:folderIconRadius="0.6" />
-
-    <!-- Square icon -->
-    <RoundedSquare launcher:folderIconRadius="0" />
-
-    <TearDrop launcher:folderIconRadius="0.3" />
-    <Squircle launcher:folderIconRadius="0.2" />
-
-</shapes>
\ No newline at end of file
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index f68c8e0..7df4014 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1657,9 +1657,7 @@
                 }
             }
 
-            if (FeatureFlags.enableSplitContextually()) {
-                handleSplitAnimationGoingToHome(LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
-            }
+            handleSplitAnimationGoingToHome(LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
             mOverlayManager.hideOverlay(isStarted());
             handleGestureContract(intent);
         } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 6e2d357..a526b89 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -24,25 +24,38 @@
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Process;
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.util.LayoutImportExportHelper;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.function.ToIntFunction;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
 
+    // Method API For Provider#call method.
+    private static final String METHOD_EXPORT_LAYOUT_XML = "EXPORT_LAYOUT_XML";
+    private static final String METHOD_IMPORT_LAYOUT_XML = "IMPORT_LAYOUT_XML";
+    private static final String KEY_RESULT = "KEY_RESULT";
+    private static final String KEY_LAYOUT = "KEY_LAYOUT";
+    private static final String SUCCESS = "success";
+    private static final String FAILURE = "failure";
+
     /**
      * $ adb shell dumpsys activity provider com.android.launcher3
      */
@@ -142,6 +155,43 @@
         return executeControllerTask(c -> c.update(args.table, values, args.where, args.args));
     }
 
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        Bundle b = new Bundle();
+
+        // The caller must have the read or write permission for this content provider to
+        // access the "call" method at all. We also enforce the appropriate per-method permissions.
+        switch(method) {
+            case METHOD_EXPORT_LAYOUT_XML:
+                if (getContext().checkCallingOrSelfPermission(getReadPermission())
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller doesn't have read permission");
+                }
+
+                CompletableFuture<String> resultFuture = LayoutImportExportHelper.INSTANCE
+                        .exportModelDbAsXmlFuture(getContext());
+                try {
+                    b.putString(KEY_LAYOUT, resultFuture.get());
+                    b.putString(KEY_RESULT, SUCCESS);
+                } catch (ExecutionException | InterruptedException e) {
+                    b.putString(KEY_RESULT, FAILURE);
+                }
+                return b;
+
+            case METHOD_IMPORT_LAYOUT_XML:
+                if (getContext().checkCallingOrSelfPermission(getWritePermission())
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller doesn't have write permission");
+                }
+
+                LayoutImportExportHelper.INSTANCE.importModelFromXml(getContext(), arg);
+                b.putString(KEY_RESULT, SUCCESS);
+                return b;
+            default:
+                return null;
+        }
+    }
+
     private int executeControllerTask(ToIntFunction<ModelDbController> task) {
         if (Binder.getCallingPid() == Process.myPid()) {
             throw new IllegalArgumentException("Same process should call model directly");
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 0ec3b79..5072e37 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -718,12 +718,14 @@
     }
 
     /**
-     * Queues the given callback to be run once {@code mPageScrolls} has been initialized.
+     * Run the given `callback` immediately once {@code mPageScrolls} has been initialized,
+     * otherwise queue the callback to `mOnPageScrollsInitializedCallbacks`.
      */
     public void runOnPageScrollsInitialized(Runnable callback) {
-        mOnPageScrollsInitializedCallbacks.add(callback);
         if (isPageScrollsInitialized()) {
-            onPageScrollsInitialized();
+            callback.run();
+        } else {
+            mOnPageScrollsInitializedCallbacks.add(callback);
         }
     }
 
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/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index d115f9f..c91e783 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -24,7 +24,6 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -67,7 +66,6 @@
                         screenId, coordinates[0], coordinates[1]);
                 mContext.bindItems(Collections.singletonList(info), true);
                 AbstractFloatingView.closeAllOpenViews(mContext);
-                announceConfirmation(R.string.item_added_to_workspace);
             }));
             return true;
         }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 9e38824..1fe42f7 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -201,15 +201,6 @@
             "USE_LOCAL_ICON_OVERRIDES", ENABLED,
             "Use inbuilt monochrome icons if app doesn't provide one");
 
-    // Aconfig migration complete for ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.
-    public static final BooleanFlag ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE = getDebugFlag(
-            270393453, "ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE", DISABLED,
-            "Enable initiating split screen from workspace to workspace.");
-    public static boolean enableSplitContextually() {
-        return ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get() ||
-                com.android.wm.shell.Flags.enableSplitContextual();
-    }
-
     // TODO(Block 29): Clean up flags
     // Aconfig migration complete for ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.
     public static final BooleanFlag ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = getDebugFlag(270393897,
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/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 42556ca..b76e098 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -697,15 +697,23 @@
             }
         }
 
+        Log.d("b/383526431", "animateOpen: content child count before: "
+                + mContent.getTotalChildCount());
+
         mContent.completePendingPageChanges();
         mContent.setCurrentPage(pageNo);
 
+        Log.d("b/383526431", "animateOpen: content child count after pending page"
+                + " changes: " + mContent.getTotalChildCount());
+
         // This is set to true in close(), but isn't reset to false until onDropCompleted(). This
         // leads to an inconsistent state if you drag out of the folder and drag back in without
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
         cancelRunningAnimations();
+        Log.d("b/383526431", "animateOpen: content child count after cancelling"
+                + " animation: " + mContent.getTotalChildCount());
         FolderAnimationManager fam = new FolderAnimationManager(this, true /* isOpening */);
         AnimatorSet anim = fam.getAnimator();
         anim.addListener(new AnimatorListenerAdapter() {
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index a7ab7b9..06286d6 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 
 import android.graphics.Point;
+import android.util.Log;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.model.data.FolderInfo;
@@ -178,6 +179,14 @@
                 break;
             }
         }
+
+        if (result.isEmpty()) {
+            // Log specifics since we are getting empty result
+            Log.d("b/383526431", "previewItemsForPage: "
+                    + "mCountX = " + mCountX
+                    + ", mCountY = " + mCountY
+                    + ", content size = " + contents.size());
+        }
         return result;
     }
 
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 22f1164..bebe1a4 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -58,6 +58,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.ToIntFunction;
 import java.util.stream.Collectors;
 
@@ -531,6 +532,16 @@
         verifyVisibleHighResIcons(getCurrentPage() + 1);
     }
 
+    int getTotalChildCount() {
+        AtomicInteger count = new AtomicInteger();
+        iterateOverItems((i, v) -> {
+            count.getAndIncrement();
+            return false;
+        });
+
+        return count.get();
+    }
+
     /**
      * Ensures that all the icons on the given page are of high-res
      */
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 5461485..ad176dc 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.graphics;
 
+
+import static com.android.launcher3.graphics.IconShape.PREF_ICON_SHAPE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
@@ -40,9 +42,10 @@
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.model.BgDataModel;
-import com.android.launcher3.shapes.AppShape;
-import com.android.launcher3.shapes.AppShapesProvider;
+import com.android.launcher3.shapes.IconShapeModel;
+import com.android.launcher3.shapes.IconShapesProvider;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
@@ -50,7 +53,6 @@
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Optional;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.concurrent.ExecutionException;
@@ -136,19 +138,26 @@
 
         switch (path) {
             case KEY_SHAPE_OPTIONS: {
-                if (Flags.newCustomizationPickerUi() && Flags.enableLauncherIconShapes()) {
+                if (Flags.newCustomizationPickerUi()) {
                     MatrixCursor cursor = new MatrixCursor(new String[]{
                             KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
-                    List<AppShape> shapes =  AppShapesProvider.INSTANCE.getShapes();
+                    List<IconShapeModel> shapes = IconShapesProvider.INSTANCE.getShapes()
+                            .values()
+                            .stream()
+                            .toList();
+                    String currentPath = LauncherPrefs.get(context).get(PREF_ICON_SHAPE);
+                    IconShapeModel currentShape = shapes.stream()
+                            .filter(shape -> currentPath.equals(shape.getPathString()))
+                            .findFirst()
+                            .orElse(IconShapesProvider.INSTANCE.getShapes().get("circle"));
+
                     for (int i = 0; i < shapes.size(); i++) {
-                        AppShape shape = shapes.get(i);
+                        IconShapeModel shape = shapes.get(i);
                         cursor.newRow()
                                 .add(KEY_SHAPE_KEY, shape.getKey())
                                 .add(KEY_SHAPE_TITLE, shape.getTitle())
-                                .add(KEY_PATH, shape.getPath())
-                                // TODO (b/348664593): We should fetch the currently-set shape
-                                //  option from the preferences.
-                                .add(KEY_IS_DEFAULT, i == 0);
+                                .add(KEY_PATH, shape.getPathString())
+                                .add(KEY_IS_DEFAULT, shape.equals(currentShape));
                     }
                     return cursor;
                 } else  {
@@ -210,11 +219,8 @@
             case KEY_DEFAULT_GRID: {
                 if (Flags.newCustomizationPickerUi()) {
                     String shapeKey = values.getAsString(KEY_SHAPE_KEY);
-                    Optional<AppShape> optionalShape = AppShapesProvider.INSTANCE.getShapes()
-                            .stream().filter(shape -> shape.getKey().equals(shapeKey)).findFirst();
-                    String pathToSet = optionalShape.map(AppShape::getPath).orElse(null);
-                    // TODO (b/348664593): Apply shapeName to the system. This needs to be a
-                    //  synchronous call.
+                    IconShapeModel shape = IconShapesProvider.INSTANCE.getShapes().get(shapeKey);
+                    IconShape.INSTANCE.get(context).setShapeOverride(shape);
                 }
                 String gridName = values.getAsString(KEY_NAME);
                 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
@@ -353,14 +359,12 @@
                     renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
                     break;
                 case MESSAGE_ID_UPDATE_SHAPE:
-                    if (Flags.newCustomizationPickerUi() && Flags.enableLauncherIconShapes()) {
+                    if (Flags.newCustomizationPickerUi()
+                            && com.android.launcher3.Flags.enableLauncherIconShapes()) {
                         String shapeKey = message.getData().getString(KEY_SHAPE_KEY);
-                        Optional<AppShape> optionalShape = AppShapesProvider.INSTANCE.getShapes()
-                                .stream()
-                                .filter(shape -> shape.getKey().equals(shapeKey))
-                                .findFirst();
-                        String pathToSet = optionalShape.map(AppShape::getPath).orElse(null);
-                        // TODO (b/348664593): Update launcher preview with the given shape
+                        IconShapeModel shape =
+                                IconShapesProvider.INSTANCE.getShapes().get(shapeKey);
+                        renderer.updateShape(shape);
                     }
                     break;
                 case MESSAGE_ID_UPDATE_GRID:
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/IconShape.kt
index c64d4da..2d2ee30 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/IconShape.kt
@@ -17,100 +17,90 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
-import android.animation.FloatArrayEvaluator
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Matrix.ScaleToFit.FILL
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.Rect
+import android.graphics.RectF
 import android.graphics.Region
 import android.graphics.drawable.AdaptiveIconDrawable
 import android.graphics.drawable.ColorDrawable
-import android.util.Xml
+import android.util.Log
 import android.view.View
 import android.view.ViewOutlineProvider
-import com.android.launcher3.R
+import androidx.annotation.VisibleForTesting
+import androidx.graphics.shapes.CornerRounding
+import androidx.graphics.shapes.Morph
+import androidx.graphics.shapes.RoundedPolygon
+import androidx.graphics.shapes.SvgPathParser
+import androidx.graphics.shapes.toPath
+import androidx.graphics.shapes.transformed
+import com.android.launcher3.EncryptionType
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext
 import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
 import com.android.launcher3.icons.GraphicsUtils
 import com.android.launcher3.icons.IconNormalizer
+import com.android.launcher3.shapes.IconShapeModel
+import com.android.launcher3.shapes.IconShapesProvider
 import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.views.ClipPathView
-import java.io.IOException
 import javax.inject.Inject
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
 
 /** Abstract representation of the shape of an icon shape */
 @LauncherAppSingleton
 class IconShape
 @Inject
 constructor(
-    @ApplicationContext context: Context,
+    @ApplicationContext private val context: Context,
+    private val prefs: LauncherPrefs,
     themeManager: ThemeManager,
     lifeCycle: DaggerSingletonTracker,
 ) {
-    var shape: ShapeDelegate = Circle()
-        private set
+
+    var shapeOverride: IconShapeModel? = getShapeFromPathString(prefs.get(PREF_ICON_SHAPE))
+        set(value) {
+            field = value
+            if (context !is PreviewContext) {
+                value?.let { prefs.put(PREF_ICON_SHAPE, value.pathString) }
+            }
+        }
 
     var normalizationScale: Float = IconNormalizer.ICON_VISIBLE_AREA_FACTOR
         private set
 
-    init {
-        pickBestShape(context)
+    var shape: ShapeDelegate = pickBestShape(themeManager)
+        private set
 
-        val changeListener = ThemeChangeListener { pickBestShape(context) }
+    init {
+        val changeListener = ThemeChangeListener { shape = pickBestShape(themeManager) }
         themeManager.addChangeListener(changeListener)
         lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) }
     }
 
     /** Initializes the shape which is closest to the [AdaptiveIconDrawable] */
-    fun pickBestShape(context: Context) {
-        // Pick any large size
-        val size = 200
-        val full = Region(0, 0, size, size)
-        val shapePath = Path()
-        val shapeR = Region()
-        val iconR = Region()
-        val drawable = AdaptiveIconDrawable(ColorDrawable(Color.BLACK), ColorDrawable(Color.BLACK))
-        drawable.setBounds(0, 0, size, size)
-        iconR.setPath(drawable.iconMask, full)
-
-        // Find the shape with minimum area of divergent region.
-        var minArea = Int.MAX_VALUE
-        var closestShape: ShapeDelegate? = null
-        for (shape in getAllShapes(context)) {
-            shapePath.reset()
-            shape.addToPath(shapePath, 0f, 0f, size / 2f)
-            shapeR.setPath(shapePath, full)
-            shapeR.op(iconR, Region.Op.XOR)
-
-            val area = GraphicsUtils.getArea(shapeR)
-            if (area < minArea) {
-                minArea = area
-                closestShape = shape
+    private fun pickBestShape(themeManager: ThemeManager): ShapeDelegate {
+        val drawable =
+            AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)).apply {
+                setBounds(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
             }
-        }
 
-        if (closestShape != null) {
-            shape = closestShape
-        }
-
-        // Initialize shape properties
-        normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null)
+        normalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, AREA_CALC_SIZE)
+        return pickBestShape(drawable.iconMask, themeManager.iconState.iconMask)
     }
 
     interface ShapeDelegate {
-        fun enableShapeDetection(): Boolean {
-            return false
-        }
-
         fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
 
         fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
@@ -121,38 +111,11 @@
             endRect: Rect,
             endRadius: Float,
             isReversed: Boolean,
-        ): ValueAnimator where T : View?, T : ClipPathView?
+        ): ValueAnimator where T : View, T : ClipPathView
     }
 
-    /** Abstract shape where the reveal animation is a derivative of a round rect animation */
-    private abstract class SimpleRectShape : ShapeDelegate {
-        override fun <T> createRevealAnimator(
-            target: T,
-            startRect: Rect,
-            endRect: Rect,
-            endRadius: Float,
-            isReversed: Boolean,
-        ): ValueAnimator where T : View?, T : ClipPathView? {
-            return object :
-                    RoundedRectRevealOutlineProvider(
-                        getStartRadius(startRect),
-                        endRadius,
-                        startRect,
-                        endRect,
-                    ) {
-                    override fun shouldRemoveElevationDuringAnimation(): Boolean {
-                        return true
-                    }
-                }
-                .createRevealAnimator(target, isReversed)
-        }
-
-        protected abstract fun getStartRadius(startRect: Rect): Float
-    }
-
-    /** Abstract shape which draws using [Path] */
-    abstract class PathShape : ShapeDelegate {
-        private val mTmpPath = Path()
+    @VisibleForTesting
+    class Circle : RoundedSquare(1f) {
 
         override fun drawShape(
             canvas: Canvas,
@@ -160,138 +123,16 @@
             offsetY: Float,
             radius: Float,
             paint: Paint,
-        ) {
-            mTmpPath.reset()
-            addToPath(mTmpPath, offsetX, offsetY, radius)
-            canvas.drawPath(mTmpPath, paint)
-        }
+        ) = canvas.drawCircle(radius + offsetX, radius + offsetY, radius, paint)
 
-        protected abstract fun newUpdateListener(
-            startRect: Rect,
-            endRect: Rect,
-            endRadius: Float,
-            outPath: Path,
-        ): ValueAnimator.AnimatorUpdateListener
-
-        override fun <T> createRevealAnimator(
-            target: T,
-            startRect: Rect,
-            endRect: Rect,
-            endRadius: Float,
-            isReversed: Boolean,
-        ): ValueAnimator where T : View?, T : ClipPathView? {
-            val path = Path()
-            val listener = newUpdateListener(startRect, endRect, endRadius, path)
-
-            val va =
-                if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f)
-            va.addListener(
-                object : AnimatorListenerAdapter() {
-                    private var mOldOutlineProvider: ViewOutlineProvider? = null
-
-                    override fun onAnimationStart(animation: Animator) {
-                        target?.apply {
-                            mOldOutlineProvider = outlineProvider
-                            outlineProvider = null
-                            translationZ = -target.elevation
-                        }
-                    }
-
-                    override fun onAnimationEnd(animation: Animator) {
-                        target?.apply {
-                            translationZ = 0f
-                            setClipPath(null)
-                            outlineProvider = mOldOutlineProvider
-                        }
-                    }
-                }
-            )
-
-            va.addUpdateListener { anim: ValueAnimator ->
-                path.reset()
-                listener.onAnimationUpdate(anim)
-                target?.setClipPath(path)
-            }
-
-            return va
-        }
-    }
-
-    open class Circle : PathShape() {
-        private val mTempRadii = FloatArray(8)
-
-        override fun newUpdateListener(
-            startRect: Rect,
-            endRect: Rect,
-            endRadius: Float,
-            outPath: Path,
-        ): ValueAnimator.AnimatorUpdateListener {
-            val r1 = getStartRadius(startRect)
-
-            val startValues =
-                floatArrayOf(
-                    startRect.left.toFloat(),
-                    startRect.top.toFloat(),
-                    startRect.right.toFloat(),
-                    startRect.bottom.toFloat(),
-                    r1,
-                    r1,
-                )
-            val endValues =
-                floatArrayOf(
-                    endRect.left.toFloat(),
-                    endRect.top.toFloat(),
-                    endRect.right.toFloat(),
-                    endRect.bottom.toFloat(),
-                    endRadius,
-                    endRadius,
-                )
-
-            val evaluator = FloatArrayEvaluator(FloatArray(6))
-
-            return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
-                val progress = anim.animatedValue as Float
-                val values = evaluator.evaluate(progress, startValues, endValues)
-                outPath.addRoundRect(
-                    values[0],
-                    values[1],
-                    values[2],
-                    values[3],
-                    getRadiiArray(values[4], values[5]),
-                    Path.Direction.CW,
-                )
-            }
-        }
-
-        private fun getRadiiArray(r1: Float, r2: Float): FloatArray {
-            mTempRadii[7] = r1
-            mTempRadii[6] = mTempRadii[7]
-            mTempRadii[3] = mTempRadii[6]
-            mTempRadii[2] = mTempRadii[3]
-            mTempRadii[1] = mTempRadii[2]
-            mTempRadii[0] = mTempRadii[1]
-            mTempRadii[5] = r2
-            mTempRadii[4] = mTempRadii[5]
-            return mTempRadii
-        }
-
-        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
+        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) =
             path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW)
-        }
-
-        private fun getStartRadius(startRect: Rect): Float {
-            return startRect.width() / 2f
-        }
-
-        override fun enableShapeDetection(): Boolean {
-            return true
-        }
     }
 
-    private class RoundedSquare(
-        /** Ratio of corner radius to half size. */
-        private val mRadiusRatio: Float
-    ) : SimpleRectShape() {
+    /** Rounded square with [radiusRatio] as a ratio of its half edge size */
+    @VisibleForTesting
+    open class RoundedSquare(val radiusRatio: Float) : ShapeDelegate {
+
         override fun drawShape(
             canvas: Canvas,
             offsetX: Float,
@@ -301,14 +142,14 @@
         ) {
             val cx = radius + offsetX
             val cy = radius + offsetY
-            val cr = radius * mRadiusRatio
+            val cr = radius * radiusRatio
             canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, paint)
         }
 
         override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
             val cx = radius + offsetX
             val cy = radius + offsetY
-            val cr = radius * mRadiusRatio
+            val cr = radius * radiusRatio
             path.addRoundRect(
                 cx - radius,
                 cy - radius,
@@ -320,213 +161,218 @@
             )
         }
 
-        override fun getStartRadius(startRect: Rect): Float {
-            return (startRect.width() / 2f) * mRadiusRatio
+        override fun <T> createRevealAnimator(
+            target: T,
+            startRect: Rect,
+            endRect: Rect,
+            endRadius: Float,
+            isReversed: Boolean,
+        ): ValueAnimator where T : View, T : ClipPathView {
+            return object :
+                    RoundedRectRevealOutlineProvider(
+                        (startRect.width() / 2f) * radiusRatio,
+                        endRadius,
+                        startRect,
+                        endRect,
+                    ) {
+                    override fun shouldRemoveElevationDuringAnimation() = true
+                }
+                .createRevealAnimator(target, isReversed)
         }
     }
 
-    private class TearDrop(
-        /**
-         * Radio of short radius to large radius, based on the shape options defined in the config.
-         */
-        private val mRadiusRatio: Float
-    ) : PathShape() {
-        private val mTempRadii = FloatArray(8)
-
-        override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
-            val r2 = radius * mRadiusRatio
-            val cx = radius + offsetX
-            val cy = radius + offsetY
-
-            path.addRoundRect(
-                cx - radius,
-                cy - radius,
-                cx + radius,
-                cy + radius,
-                getRadiiArray(radius, r2),
-                Path.Direction.CW,
+    /** Generic shape delegate with pathString in bounds [0, 0, 100, 100] */
+    class GenericPathShape(pathString: String) : ShapeDelegate {
+        private val poly =
+            RoundedPolygon(
+                features = SvgPathParser.parseFeatures(pathString),
+                centerX = 50f,
+                centerY = 50f,
             )
-        }
-
-        fun getRadiiArray(r1: Float, r2: Float): FloatArray {
-            mTempRadii[7] = r1
-            mTempRadii[6] = mTempRadii[7]
-            mTempRadii[3] = mTempRadii[6]
-            mTempRadii[2] = mTempRadii[3]
-            mTempRadii[1] = mTempRadii[2]
-            mTempRadii[0] = mTempRadii[1]
-            mTempRadii[5] = r2
-            mTempRadii[4] = mTempRadii[5]
-            return mTempRadii
-        }
-
-        override fun newUpdateListener(
-            startRect: Rect,
-            endRect: Rect,
-            endRadius: Float,
-            outPath: Path,
-        ): ValueAnimator.AnimatorUpdateListener {
-            val r1 = startRect.width() / 2f
-            val r2 = r1 * mRadiusRatio
-
-            val startValues =
-                floatArrayOf(
-                    startRect.left.toFloat(),
-                    startRect.top.toFloat(),
-                    startRect.right.toFloat(),
-                    startRect.bottom.toFloat(),
-                    r1,
-                    r2,
-                )
-            val endValues =
-                floatArrayOf(
-                    endRect.left.toFloat(),
-                    endRect.top.toFloat(),
-                    endRect.right.toFloat(),
-                    endRect.bottom.toFloat(),
-                    endRadius,
-                    endRadius,
-                )
-
-            val evaluator = FloatArrayEvaluator(FloatArray(6))
-
-            return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
-                val progress = anim.animatedValue as Float
-                val values = evaluator.evaluate(progress, startValues, endValues)
-                outPath.addRoundRect(
-                    values[0],
-                    values[1],
-                    values[2],
-                    values[3],
-                    getRadiiArray(values[4], values[5]),
-                    Path.Direction.CW,
-                )
+        // This ensures that a valid morph is possible from the provided path
+        private val basePath =
+            Path().apply {
+                Morph(poly, createRoundedRect(0f, 0f, 100f, 100f, 25f)).toPath(0f, this)
             }
-        }
-    }
+        private val tmpPath = Path()
+        private val tmpMatrix = Matrix()
 
-    private class Squircle(
-        /** Radio of radius to circle radius, based on the shape options defined in the config. */
-        private val mRadiusRatio: Float
-    ) : PathShape() {
+        override fun drawShape(
+            canvas: Canvas,
+            offsetX: Float,
+            offsetY: Float,
+            radius: Float,
+            paint: Paint,
+        ) {
+            tmpPath.reset()
+            addToPath(tmpPath, offsetX, offsetY, radius)
+            canvas.drawPath(tmpPath, paint)
+        }
+
         override fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float) {
-            val cx = radius + offsetX
-            val cy = radius + offsetY
-            val control = radius - radius * mRadiusRatio
-
-            path.moveTo(cx, cy - radius)
-            addLeftCurve(cx, cy, radius, control, path)
-            addRightCurve(cx, cy, radius, control, path)
-            addLeftCurve(cx, cy, -radius, -control, path)
-            addRightCurve(cx, cy, -radius, -control, path)
-            path.close()
+            tmpMatrix.setScale(radius / 50, radius / 50)
+            tmpMatrix.postTranslate(offsetX, offsetY)
+            basePath.transform(tmpMatrix, path)
         }
 
-        fun addLeftCurve(cx: Float, cy: Float, r: Float, control: Float, path: Path) {
-            path.cubicTo(cx - control, cy - r, cx - r, cy - control, cx - r, cy)
-        }
-
-        fun addRightCurve(cx: Float, cy: Float, r: Float, control: Float, path: Path) {
-            path.cubicTo(cx - r, cy + control, cx - control, cy + r, cx, cy + r)
-        }
-
-        override fun newUpdateListener(
+        override fun <T> createRevealAnimator(
+            target: T,
             startRect: Rect,
             endRect: Rect,
             endRadius: Float,
-            outPath: Path,
-        ): ValueAnimator.AnimatorUpdateListener {
-            val startCX = startRect.exactCenterX()
-            val startCY = startRect.exactCenterY()
-            val startR = startRect.width() / 2f
-            val startControl = startR - startR * mRadiusRatio
-            val startHShift = 0f
-            val startVShift = 0f
+            isReversed: Boolean,
+        ): ValueAnimator where T : View, T : ClipPathView {
+            // End poly is defined as a rectangle starting at top/center so that the
+            // transformation has minimum motion
+            val morph =
+                Morph(
+                    start =
+                        poly.transformed(
+                            Matrix().apply {
+                                setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(startRect), FILL)
+                            }
+                        ),
+                    end =
+                        createRoundedRect(
+                            left = endRect.left.toFloat(),
+                            top = endRect.top.toFloat(),
+                            right = endRect.right.toFloat(),
+                            bottom = endRect.bottom.toFloat(),
+                            cornerR = endRadius,
+                        ),
+                )
 
-            val endCX = endRect.exactCenterX()
-            val endCY = endRect.exactCenterY()
-            // Approximate corner circle using bezier curves
-            // http://spencermortensen.com/articles/bezier-circle/
-            val endControl = endRadius * 0.551915024494f
-            val endHShift = endRect.width() / 2f - endRadius
-            val endVShift = endRect.height() / 2f - endRadius
+            val va =
+                if (isReversed) ValueAnimator.ofFloat(1f, 0f) else ValueAnimator.ofFloat(0f, 1f)
+            va.addListener(
+                object : AnimatorListenerAdapter() {
+                    private var oldOutlineProvider: ViewOutlineProvider? = null
 
-            return ValueAnimator.AnimatorUpdateListener { anim: ValueAnimator ->
-                val progress = anim.animatedValue as Float
-                val cx = (1 - progress) * startCX + progress * endCX
-                val cy = (1 - progress) * startCY + progress * endCY
-                val r = (1 - progress) * startR + progress * endRadius
-                val control = (1 - progress) * startControl + progress * endControl
-                val hShift = (1 - progress) * startHShift + progress * endHShift
-                val vShift = (1 - progress) * startVShift + progress * endVShift
+                    override fun onAnimationStart(animation: Animator) {
+                        target.apply {
+                            oldOutlineProvider = outlineProvider
+                            outlineProvider = null
+                            translationZ = -target.elevation
+                        }
+                    }
 
-                outPath.moveTo(cx, cy - vShift - r)
-                outPath.rLineTo(-hShift, 0f)
+                    override fun onAnimationEnd(animation: Animator) {
+                        target.apply {
+                            translationZ = 0f
+                            setClipPath(null)
+                            outlineProvider = oldOutlineProvider
+                        }
+                    }
+                }
+            )
 
-                addLeftCurve(cx - hShift, cy - vShift, r, control, outPath)
-                outPath.rLineTo(0f, vShift + vShift)
-
-                addRightCurve(cx - hShift, cy + vShift, r, control, outPath)
-                outPath.rLineTo(hShift + hShift, 0f)
-
-                addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath)
-                outPath.rLineTo(0f, -vShift - vShift)
-
-                addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath)
-                outPath.close()
+            val path = Path()
+            va.addUpdateListener { anim: ValueAnimator ->
+                path.reset()
+                morph.toPath(anim.animatedValue as Float, path)
+                target.setClipPath(path)
             }
+            return va
         }
     }
 
     companion object {
         @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
 
-        private fun getShapeDefinition(type: String, radius: Float): ShapeDelegate {
-            return when (type) {
-                "Circle" -> Circle()
-                "RoundedSquare" -> RoundedSquare(radius)
-                "TearDrop" -> TearDrop(radius)
-                "Squircle" -> Squircle(radius)
-                else -> throw IllegalArgumentException("Invalid shape type: $type")
+        const val TAG = "IconShape"
+        const val KEY_ICON_SHAPE = "icon_shape"
+        const val AREA_CALC_SIZE = 1000
+        // .1% error margin
+        const val AREA_DIFF_THRESHOLD = AREA_CALC_SIZE * AREA_CALC_SIZE / 1000
+
+        @JvmField val PREF_ICON_SHAPE = backedUpItem(KEY_ICON_SHAPE, "", EncryptionType.ENCRYPTED)
+
+        private fun getShapeFromPathString(pathString: String): IconShapeModel? {
+            return IconShapesProvider.shapes.values.firstOrNull { shape: IconShapeModel ->
+                shape.pathString == pathString
             }
         }
 
-        private fun getAllShapes(context: Context): List<ShapeDelegate> {
-            val result = ArrayList<ShapeDelegate>()
-            try {
-                context.resources.getXml(R.xml.folder_shapes).use { parser ->
-                    // Find the root tag
-                    var type: Int = parser.next()
-                    while (
-                        type != XmlPullParser.END_TAG &&
-                            type != XmlPullParser.END_DOCUMENT &&
-                            "shapes" != parser.name
-                    ) {
-                        type = parser.next()
-                    }
-                    val depth = parser.depth
-                    val radiusAttr = intArrayOf(R.attr.folderIconRadius)
-                    type = parser.next()
-                    while (
-                        (type != XmlPullParser.END_TAG || parser.depth > depth) &&
-                            type != XmlPullParser.END_DOCUMENT
-                    ) {
-                        if (type == XmlPullParser.START_TAG) {
-                            val attrs = Xml.asAttributeSet(parser)
-                            val arr = context.obtainStyledAttributes(attrs, radiusAttr)
-                            val shape = getShapeDefinition(parser.name, arr.getFloat(0, 1f))
-                            arr.recycle()
-                            result.add(shape)
-                        }
-                        type = parser.next()
-                    }
-                }
-            } catch (e: IOException) {
-                throw RuntimeException(e)
-            } catch (e: XmlPullParserException) {
-                throw RuntimeException(e)
+        /** Returns a function to calculate area diff from [base] */
+        @VisibleForTesting
+        fun areaDiffCalculator(base: Path): (ShapeDelegate) -> Int {
+            val fullRegion = Region(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
+            val iconRegion = Region().apply { setPath(base, fullRegion) }
+
+            val shapePath = Path()
+            val shapeRegion = Region()
+            return fun(shape: ShapeDelegate): Int {
+                shapePath.reset()
+                shape.addToPath(shapePath, 0f, 0f, AREA_CALC_SIZE / 2f)
+                shapeRegion.setPath(shapePath, fullRegion)
+                shapeRegion.op(iconRegion, Region.Op.XOR)
+                return GraphicsUtils.getArea(shapeRegion)
             }
-            return result
         }
+
+        @VisibleForTesting
+        fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
+            val calcAreaDiff = areaDiffCalculator(baseShape)
+
+            // Find the shape with minimum area of divergent region.
+            var closestShape: ShapeDelegate = Circle()
+            var minAreaDiff = calcAreaDiff(closestShape)
+
+            // Try some common rounded rect edges
+            for (f in 0..20) {
+                val rectShape = RoundedSquare(f.toFloat() / 20)
+                val rectArea = calcAreaDiff(rectShape)
+                if (rectArea < minAreaDiff) {
+                    minAreaDiff = rectArea
+                    closestShape = rectShape
+                }
+            }
+
+            // Use the generic shape only if we have more than .1% error
+            if (shapeStr.isNotEmpty() && minAreaDiff > AREA_DIFF_THRESHOLD) {
+                try {
+                    val generic = GenericPathShape(shapeStr)
+                    closestShape = generic
+                } catch (e: Exception) {
+                    Log.e(TAG, "Error converting mask to generic shape", e)
+                }
+            }
+            return closestShape
+        }
+
+        /**
+         * Creates a rounded rect with the start point at the center of the top edge. This ensures a
+         * better animation since our shape paths also start at top-center of the bounding box.
+         */
+        fun createRoundedRect(
+            left: Float,
+            top: Float,
+            right: Float,
+            bottom: Float,
+            cornerR: Float,
+        ) =
+            RoundedPolygon(
+                vertices =
+                    floatArrayOf(
+                        // x1, y1
+                        (left + right) / 2,
+                        top,
+                        // x2, y2
+                        right,
+                        top,
+                        // x3, y3
+                        right,
+                        bottom,
+                        // x4, y4
+                        left,
+                        bottom,
+                        // x5, y5
+                        left,
+                        top,
+                    ),
+                centerX = (left + right) / 2,
+                centerY = (top + bottom) / 2,
+                rounding = CornerRounding(cornerR),
+            )
     }
 }
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 3000b25..6afac71 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.provider.LauncherDbUtils;
+import com.android.launcher3.shapes.IconShapeModel;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
@@ -77,9 +78,7 @@
 public class PreviewSurfaceRenderer {
 
     private static final String TAG = "PreviewSurfaceRenderer";
-
     private static final int FADE_IN_ANIMATION_DURATION = 200;
-
     private static final String KEY_HOST_TOKEN = "host_token";
     private static final String KEY_VIEW_WIDTH = "width";
     private static final String KEY_VIEW_HEIGHT = "height";
@@ -90,25 +89,25 @@
     private static final String KEY_DARK_MODE = "use_dark_mode";
 
     private Context mContext;
-    private final IBinder mHostToken;
-    private final int mWidth;
-    private final int mHeight;
-    private String mGridName;
-
-    private final int mDisplayId;
-    private final Display mDisplay;
-    private final WallpaperColors mWallpaperColors;
     private SparseIntArray mPreviewColorOverride;
+    private String mGridName;
+    private IconShapeModel mShape;
     @Nullable private Boolean mDarkMode;
-    private final RunnableList mLifeCycleTracker;
-
-    private final SurfaceControlViewHost mSurfaceControlViewHost;
-
     private boolean mDestroyed = false;
     private LauncherPreviewRenderer mRenderer;
     private boolean mHideQsb;
     @Nullable private FrameLayout mViewRoot = null;
 
+    private final IBinder mHostToken;
+    private final int mWidth;
+    private final int mHeight;
+    private final int mDisplayId;
+    private final Display mDisplay;
+    private final WallpaperColors mWallpaperColors;
+    private final RunnableList mLifeCycleTracker;
+    private final SurfaceControlViewHost mSurfaceControlViewHost;
+
+
     public PreviewSurfaceRenderer(
             Context context, RunnableList lifecycleTracker, Bundle bundle) throws Exception {
         mContext = context;
@@ -135,10 +134,10 @@
         }
 
         mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new MySurfaceControlViewHost(
-                mContext,
-                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
-                mHostToken,
-                mLifeCycleTracker))
+                        mContext,
+                        context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
+                        mHostToken,
+                        mLifeCycleTracker))
                 .get(5, TimeUnit.SECONDS);
         mLifeCycleTracker.add(this::destroy);
     }
@@ -219,6 +218,20 @@
     }
 
     /**
+     * Update the shapes of the launcher preview
+     *
+     * @param shape path for shapes
+     */
+    public void updateShape(@NonNull IconShapeModel shape) {
+        if (shape.equals(mShape)) {
+            Log.w(TAG, "Preview shape already set, skipping. shape=" + shape);
+            return;
+        }
+        mShape = shape;
+        loadAsync();
+    }
+
+    /**
      * Hides the components in the bottom row.
      *
      * @param hide True to hide and false to show.
@@ -303,9 +316,13 @@
     private void loadModelData() {
         final Context inflationContext = getPreviewContext();
         final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
-        if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp)) {
+        if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp)
+                || mShape != null) {
             // Start the migration
             PreviewContext previewContext = new PreviewContext(inflationContext, idp);
+            if (mShape != null) {
+                IconShape.INSTANCE.get(previewContext).setShapeOverride(mShape);
+            }
             // Copy existing data to preview DB
             LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext)
                             .getModel().getModelDbController().getDb(),
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 991edf7..989471f 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -25,6 +25,8 @@
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.graphics.IconShape.Companion.KEY_ICON_SHAPE
+import com.android.launcher3.graphics.IconShape.Companion.PREF_ICON_SHAPE
 import com.android.launcher3.icons.IconThemeController
 import com.android.launcher3.icons.mono.MonoIconThemeController
 import com.android.launcher3.util.DaggerSingletonObject
@@ -63,9 +65,12 @@
         receiver.registerPkgActions(context, "android", ACTION_OVERLAY_CHANGED)
 
         val prefListener = LauncherPrefChangeListener { key ->
-            if (key == THEMED_ICONS.sharedPrefKey) verifyIconState()
+            when (key) {
+                KEY_THEMED_ICONS,
+                KEY_ICON_SHAPE -> verifyIconState()
+            }
         }
-        prefs.addListener(prefListener, THEMED_ICONS)
+        prefs.addListener(prefListener, THEMED_ICONS, PREF_ICON_SHAPE)
 
         lifecycle.addCloseable {
             receiver.unregisterReceiverSafely(context)
@@ -87,13 +92,18 @@
 
     fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener)
 
-    private fun parseIconState() =
-        IconState(
+    private fun parseIconState(): IconState {
+        val shapeOverride = prefs.get(PREF_ICON_SHAPE)
+        return IconState(
             iconMask =
-                if (CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL) ""
-                else context.resources.getString(CONFIG_ICON_MASK_RES_ID),
+                when {
+                    shapeOverride.isNotEmpty() -> shapeOverride
+                    CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> ""
+                    else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID)
+                },
             isMonoTheme = isMonoThemeEnabled,
         )
+    }
 
     data class IconState(
         val iconMask: String,
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 04d88b0..2ffbeb8 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -17,14 +17,22 @@
 package com.android.launcher3.icons;
 
 import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
+import androidx.core.graphics.PathParser;
 
+import com.android.launcher3.Flags;
 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.shapes.IconShapeModel;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.UserIconInfo;
@@ -56,8 +64,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;
     }
@@ -76,6 +83,28 @@
         return UserCache.INSTANCE.get(mContext).getUserInfo(user);
     }
 
+    @NonNull
+    @Override
+    public Path getShapePath(AdaptiveIconDrawable drawable, Rect iconBounds) {
+        if (!Flags.enableLauncherIconShapes()) return drawable.getIconMask();
+
+        IconShapeModel shapeOverride = IconShape.INSTANCE.get(mContext).getShapeOverride();
+        if (shapeOverride != null) {
+            Path maskPath = PathParser.createPathFromPathData(shapeOverride.getPathString());
+            Matrix matrix = new Matrix();
+            // Assuming Path is in [0, 0, 100, 100] coordinate space.
+            matrix.setRectToRect(
+                    new RectF(0, 0, 100, 100),
+                    new RectF(iconBounds),
+                    Matrix.ScaleToFit.CENTER // Todo: CENTER or FILL?
+            );
+            maskPath.transform(matrix);
+            return maskPath;
+        } else {
+            return drawable.getIconMask();
+        }
+    }
+
     @Override
     public void close() {
         recycle();
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index bfa00bd..0732379 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -120,39 +120,50 @@
             @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
             @NonNull SQLiteDatabase source,
-            boolean isDestNewDb) {
+            boolean isDestNewDb,
+            ModelDelegate modelDelegate) {
 
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
             return true;
         }
 
-        if (isDestNewDb
+        boolean shouldMigrateToStrictlyTallerGrid = isDestNewDb
                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
-                && srcDeviceState.getRows() < destDeviceState.getRows()) {
-            // Only use this strategy when comparing the previous grid to the new grid and the
-            // columns are the same and the destination has more rows
+                && srcDeviceState.getRows() < destDeviceState.getRows();
+        if (shouldMigrateToStrictlyTallerGrid) {
             copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
-
-            if (oneGridSpecs()) {
-                DbReader destReader = new DbReader(
-                        target.getWritableDatabase(), TABLE_NAME, context);
-                boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows());
-                if (shouldShiftCells) {
-                    shiftTableByXCells(
-                            target.getWritableDatabase(),
-                            (destDeviceState.getRows() - srcDeviceState.getRows()),
-                            TABLE_NAME);
-                }
-            }
-
-            // Save current configuration, so that the migration does not run again.
-            destDeviceState.writeToPrefs(context);
-            return true;
+        } else {
+            copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
         }
-        copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
 
         long migrationStartTime = System.currentTimeMillis();
         try (SQLiteTransaction t = new SQLiteTransaction(target.getWritableDatabase())) {
+
+            if (shouldMigrateToStrictlyTallerGrid) {
+                // This is a special case where if the grid is the same amount of columns but a
+                // larger amount of rows we simply copy over the source grid to the destination
+                // grid, rather than undergoing the general grid migration. If there are more icons
+                // on the bottom of the first page then we shift the icons down to the bottom of the
+                // grid so that the icons remain bottom-anchored.
+                if (oneGridSpecs()) {
+                    DbReader destReader = new DbReader(
+                            target.getWritableDatabase(), TABLE_NAME, context);
+                    boolean shouldShiftCells =
+                            shouldShiftCells(destReader, srcDeviceState.getRows());
+                    if (shouldShiftCells) {
+                        shiftTableByXCells(
+                                target.getWritableDatabase(),
+                                (destDeviceState.getRows() - srcDeviceState.getRows()),
+                                TABLE_NAME);
+                    }
+                }
+
+                // Save current configuration, so that the migration does not run again.
+                destDeviceState.writeToPrefs(context);
+                t.commit();
+                return true;
+            }
+
             DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context);
             DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context);
 
@@ -164,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 "
@@ -172,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 6f86ae0..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
@@ -65,33 +66,42 @@
                 "$srcDeviceState\ndestDeviceState: $destDeviceState\nisDestNewDb: $isDestNewDb",
         )
 
-        // This is a special case where if the grid is the same amount of columns but a larger
-        // amount of rows we simply copy over the source grid to the destination grid, rather
-        // than undergoing the general grid migration.
-        if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
-            Log.d(TAG, "Migrating to strictly taller grid")
+        val shouldMigrateToStrtictlyTallerGrid =
+            shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)
+        if (shouldMigrateToStrtictlyTallerGrid) {
             copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
-            if (oneGridSpecs()) {
-                val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
-                val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
-                if (shouldShiftCells) {
-                    shiftTableByXCells(
-                        target.writableDatabase,
-                        (destDeviceState.rows - srcDeviceState.rows),
-                        TABLE_NAME,
-                    )
-                }
-            }
-            // Save current configuration, so that the migration does not run again.
-            destDeviceState.writeToPrefs(context)
-            return
+        } else {
+            copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
         }
 
-        copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
-
         val migrationStartTime = System.currentTimeMillis()
         try {
             SQLiteTransaction(target.writableDatabase).use { t ->
+                // This is a special case where if the grid is the same amount of columns but a
+                // larger amount of rows we simply copy over the source grid to the destination
+                // grid, rather than undergoing the general grid migration. If there are more icons
+                // on the bottom of the first page then we shift the icons down to the bottom of the
+                // grid so that the icons remain bottom-anchored.
+                if (shouldMigrateToStrtictlyTallerGrid) {
+                    Log.d(TAG, "Migrating to strictly taller grid")
+                    if (oneGridSpecs()) {
+                        val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
+                        val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
+                        if (shouldShiftCells) {
+                            Log.i("TAGTAG", "should shift cells")
+                            shiftTableByXCells(
+                                target.writableDatabase,
+                                (destDeviceState.rows - srcDeviceState.rows),
+                                TABLE_NAME,
+                            )
+                        }
+                    }
+                    // Save current configuration, so that the migration does not run again.
+                    destDeviceState.writeToPrefs(context)
+                    t.commit()
+                    return
+                }
+
                 val srcReader = DbReader(t.db, TMP_TABLE, context)
                 val destReader = DbReader(t.db, TABLE_NAME, context)
 
@@ -123,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/shapes/AppShapesProvider.kt b/src/com/android/launcher3/shapes/AppShapesProvider.kt
deleted file mode 100644
index 8c2f181..0000000
--- a/src/com/android/launcher3/shapes/AppShapesProvider.kt
+++ /dev/null
@@ -1,58 +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.launcher3.shapes
-
-import com.android.systemui.shared.Flags
-
-object AppShapesProvider {
-
-    val shapes =
-        if (Flags.newCustomizationPickerUi())
-            listOf(
-                AppShape(
-                    "arch",
-                    "arch",
-                    "M100 83.46C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116.884 93.916.1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0 77.614 0 100 22.386 100 50V83.46Z",
-                ),
-                AppShape(
-                    "4_sided_cookie",
-                    "4 sided cookie",
-                    "M63.605 3C84.733-6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176-6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C-6.176 15.268 15.267-6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z",
-                ),
-                AppShape(
-                    "seven_sided_cookie",
-                    "7 sided cookie",
-                    "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82-2.742 55.18-2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24.273 66.266-2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
-                ),
-                AppShape(
-                    "sunny",
-                    "sunny",
-                    "M42.846 4.873C46.084-.531 53.916-.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C-.531 53.916-.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
-                ),
-                AppShape(
-                    "circle",
-                    "circle",
-                    "M99.18 50C99.18 77.162 77.162 99.18 50 99.18 22.838 99.18.82 77.162.82 50 .82 22.839 22.838.82 50 .82 77.162.82 99.18 22.839 99.18 50Z",
-                ),
-                AppShape(
-                    "square",
-                    "square",
-                    "M99.18 53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758.82 74.306.82 67.434.82 53.689L.82 46.311C.82 32.566.82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694.82 32.566.82 46.311.82L53.689.82C67.434.82 74.306.82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689Z\n",
-                ),
-            )
-        else emptyList()
-}
diff --git a/src/com/android/launcher3/shapes/AppShape.kt b/src/com/android/launcher3/shapes/IconShapeModel.kt
similarity index 88%
rename from src/com/android/launcher3/shapes/AppShape.kt
rename to src/com/android/launcher3/shapes/IconShapeModel.kt
index 68200a0..10b7f91 100644
--- a/src/com/android/launcher3/shapes/AppShape.kt
+++ b/src/com/android/launcher3/shapes/IconShapeModel.kt
@@ -16,4 +16,4 @@
 
 package com.android.launcher3.shapes
 
-class AppShape(val key: String, val title: String, val path: String)
+data class IconShapeModel(val key: String, val title: String, val pathString: String)
diff --git a/src/com/android/launcher3/shapes/IconShapesProvider.kt b/src/com/android/launcher3/shapes/IconShapesProvider.kt
new file mode 100644
index 0000000..a190e22
--- /dev/null
+++ b/src/com/android/launcher3/shapes/IconShapesProvider.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shapes
+
+import com.android.launcher3.Flags as LauncherFlags
+import com.android.systemui.shared.Flags
+
+object IconShapesProvider {
+    val shapes =
+        if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
+            mapOf(
+                "arch" to
+                    IconShapeModel(
+                        key = "arch",
+                        title = "arch",
+                        pathString =
+                            "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116.884 93.916.1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
+                    ),
+                "4_sided_cookie" to
+                    IconShapeModel(
+                        key = "4_sided_cookie",
+                        title = "4 sided cookie",
+                        pathString =
+                            "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
+                    ),
+                "seven_sided_cookie" to
+                    IconShapeModel(
+                        key = "seven_sided_cookie",
+                        title = "7 sided cookie",
+                        pathString =
+                            "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24.273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
+                    ),
+                "sunny" to
+                    IconShapeModel(
+                        key = "sunny",
+                        title = "sunny",
+                        pathString =
+                            "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
+                    ),
+                "circle" to
+                    IconShapeModel(
+                        key = "circle",
+                        title = "circle",
+                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+                    ),
+                "square" to
+                    IconShapeModel(
+                        key = "square",
+                        title = "square",
+                        pathString =
+                            "M53.689 0.82L53.689.82C67.434.82 74.306.82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758.82 74.306.82 67.434.82 53.689L.82 46.311C.82 32.566.82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694.82 32.566.82 46.311.82Z",
+                    ),
+            )
+        } else {
+            mapOf(
+                "circle" to
+                    IconShapeModel(
+                        key = "circle",
+                        title = "circle",
+                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+                    )
+            )
+        }
+}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index cde72c1..daa6e67 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.enableAppPairs;
-import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -245,8 +244,8 @@
             }
 
             case TestProtocol.REQUEST_GET_SPLIT_SELECTION_ACTIVE:
-                response.putBoolean(TEST_INFO_RESPONSE_FIELD, enableSplitContextually()
-                        && Launcher.ACTIVITY_TRACKER.getCreatedContext().isSplitSelectionActive());
+                response.putBoolean(TEST_INFO_RESPONSE_FIELD,
+                        Launcher.ACTIVITY_TRACKER.getCreatedContext().isSplitSelectionActive());
                 return response;
 
             case TestProtocol.REQUEST_ENABLE_ROTATION:
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 89057a2..98ba8c7 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -188,7 +188,7 @@
         // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts)
         if (launcher.getDragController().isDragging()) return false;
         // Return early if user is in the middle of selecting split-screen apps
-        if (FeatureFlags.enableSplitContextually() && launcher.isSplitSelectionActive()) {
+        if (launcher.isSplitSelectionActive()) {
             return false;
         }
 
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 0ff10c2..b69bc17 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -207,7 +207,7 @@
                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                 mLauncher.getStatsLogManager().logger().log(LAUNCHER_WORKSPACE_LONGPRESS);
                 mLauncher.showDefaultOptions(mTouchDownPoint.x, mTouchDownPoint.y);
-                if (FeatureFlags.enableSplitContextually() && mLauncher.isSplitSelectionActive()) {
+                if (mLauncher.isSplitSelectionActive()) {
                     mLauncher.dismissSplitSelection(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
                 }
             } else {
diff --git a/src/com/android/launcher3/util/LayoutImportExportHelper.kt b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
new file mode 100644
index 0000000..4033f60
--- /dev/null
+++ b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util
+
+import android.app.blob.BlobHandle.createWithSha256
+import android.app.blob.BlobStoreManager
+import android.content.Context
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import android.provider.Settings.Secure
+import com.android.launcher3.AutoInstallsLayout
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
+import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.util.concurrent.CompletableFuture
+
+object LayoutImportExportHelper {
+    fun exportModelDbAsXmlFuture(context: Context): CompletableFuture<String> {
+        val future = CompletableFuture<String>()
+        exportModelDbAsXml(context) { xmlString -> future.complete(xmlString) }
+        return future
+    }
+
+    fun exportModelDbAsXml(context: Context, callback: (String) -> Unit) {
+        val model = LauncherAppState.getInstance(context).model
+
+        model.enqueueModelUpdateTask { _, dataModel, _ ->
+            val builder = LauncherLayoutBuilder()
+            dataModel.workspaceItems.forEach { info ->
+                val loc =
+                    when (info.container) {
+                        CONTAINER_DESKTOP ->
+                            builder.atWorkspace(info.cellX, info.cellY, info.screenId)
+
+                        CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
+                        else -> return@forEach
+                    }
+                loc.addItem(context, info)
+            }
+            dataModel.appWidgets.forEach { info ->
+                builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(context, info)
+            }
+
+            val layoutXml = builder.build()
+            callback(layoutXml)
+        }
+    }
+
+    fun importModelFromXml(context: Context, xmlString: String) {
+        importModelFromXml(context, xmlString.toByteArray(StandardCharsets.UTF_8))
+    }
+
+    fun importModelFromXml(context: Context, data: ByteArray) {
+        val model = LauncherAppState.getInstance(context).model
+
+        val digest = MessageDigest.getInstance("SHA-256").digest(data)
+        val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
+        val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
+
+        val resolver = context.contentResolver
+
+        blobManager.openSession(blobManager.createSession(handle)).use { session ->
+            AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
+            session.allowPublicAccess()
+
+            session.commit(ORDERED_BG_EXECUTOR) {
+                Secure.putString(resolver, LAYOUT_PROVIDER_KEY, createBlobProviderKey(digest))
+
+                MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
+                MAIN_EXECUTOR.submit { model.forceReload() }.get()
+                MODEL_EXECUTOR.submit {}.get()
+                Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
+            }
+        }
+    }
+
+    private fun LauncherLayoutBuilder.ItemTarget.addItem(context: Context, info: ItemInfo) {
+        val userType: String? =
+            when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
+                UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
+                UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
+                else -> null
+            }
+        when (info.itemType) {
+            ITEM_TYPE_APPLICATION ->
+                info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
+            ITEM_TYPE_DEEP_SHORTCUT ->
+                ShortcutKey.fromItemInfo(info).let { key ->
+                    putShortcut(key.packageName, key.id, userType)
+                }
+            ITEM_TYPE_FOLDER ->
+                (info as FolderInfo).let { folderInfo ->
+                    putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
+                        folderInfo.getContents().forEach { folderContent ->
+                            folderBuilder.addItem(context, folderContent)
+                        }
+                    }
+                }
+            ITEM_TYPE_APPWIDGET ->
+                putWidget(
+                    (info as LauncherAppWidgetInfo).providerName.packageName,
+                    info.providerName.className,
+                    info.spanX,
+                    info.spanY,
+                    userType,
+                )
+        }
+    }
+}
\ No newline at end of file
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/Android.bp b/tests/Android.bp
index f666bba..4bc654c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -226,6 +226,5 @@
     ],
     instrumentation_for: "Launcher3",
     plugins: ["dagger2-compiler"],
-    upstream: true,
     strict_mode: false,
 }
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
index a876860..56dd6a4 100644
--- a/tests/Launcher3Tests.xml
+++ b/tests/Launcher3Tests.xml
@@ -50,6 +50,8 @@
         <option name="run-command" value="settings put system show_touches 1" />
 
         <option name="run-command" value="setprop pixel_legal_joint_permission_v2 true" />
+
+        <option name="run-command" value="settings put global verifier_verify_adb_installs 0" />
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
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/celllayout/ReorderAlgorithmUnitTest.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index a62258c..bf8e8b1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -17,9 +17,6 @@
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -41,7 +38,6 @@
 import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator;
 import com.android.launcher3.util.ActivityContextWrapper;
 import com.android.launcher3.util.rule.TestStabilityRule;
-import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
 
 import org.junit.Rule;
@@ -82,7 +78,6 @@
      * This test reads existing test cases and makes sure the CellLayout produces the same
      * output for each of them for a given input.
      */
-    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     public void testAllCases() throws IOException {
         List<ReorderAlgorithmUnitTestCase> testCases = getTestCases(
@@ -125,7 +120,6 @@
     /**
      * Same as above but testing the Multipage CellLayout.
      */
-    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     public void generateValidTests_Multi() {
         Random generator = new Random(SEED);
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
new file mode 100644
index 0000000..311676a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.launcher3.graphics
+
+import android.graphics.Matrix
+import android.graphics.Matrix.ScaleToFit.FILL
+import android.graphics.Path
+import android.graphics.Path.Direction
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.Region
+import android.platform.uiautomatorhelpers.DeviceHelpers.context
+import android.view.View
+import androidx.core.graphics.PathParser
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.graphics.IconShape.Circle
+import com.android.launcher3.graphics.IconShape.Companion.AREA_CALC_SIZE
+import com.android.launcher3.graphics.IconShape.Companion.AREA_DIFF_THRESHOLD
+import com.android.launcher3.graphics.IconShape.Companion.areaDiffCalculator
+import com.android.launcher3.graphics.IconShape.Companion.pickBestShape
+import com.android.launcher3.graphics.IconShape.GenericPathShape
+import com.android.launcher3.graphics.IconShape.RoundedSquare
+import com.android.launcher3.icons.GraphicsUtils
+import com.android.launcher3.views.ClipPathView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IconShapeTest {
+
+    @Test
+    fun `areaDiffCalculator increases with outwards shape`() {
+        val diffCalculator =
+            areaDiffCalculator(
+                Path().apply {
+                    addCircle(
+                        AREA_CALC_SIZE / 2f,
+                        AREA_CALC_SIZE / 2f,
+                        AREA_CALC_SIZE / 2f,
+                        Direction.CW,
+                    )
+                }
+            )
+        assertThat(diffCalculator(Circle())).isLessThan(AREA_DIFF_THRESHOLD)
+        assertThat(diffCalculator(Circle())).isLessThan(diffCalculator(RoundedSquare(.9f)))
+        assertThat(diffCalculator(RoundedSquare(.9f)))
+            .isLessThan(diffCalculator(RoundedSquare(.8f)))
+        assertThat(diffCalculator(RoundedSquare(.8f)))
+            .isLessThan(diffCalculator(RoundedSquare(.7f)))
+        assertThat(diffCalculator(RoundedSquare(.7f)))
+            .isLessThan(diffCalculator(RoundedSquare(.6f)))
+        assertThat(diffCalculator(RoundedSquare(.6f)))
+            .isLessThan(diffCalculator(RoundedSquare(.5f)))
+    }
+
+    @Test
+    fun `areaDiffCalculator increases with inwards shape`() {
+        val diffCalculator = areaDiffCalculator(roundedRectPath(0.5f))
+        assertThat(diffCalculator(RoundedSquare(.5f))).isLessThan(AREA_DIFF_THRESHOLD)
+        assertThat(diffCalculator(RoundedSquare(.5f)))
+            .isLessThan(diffCalculator(RoundedSquare(.6f)))
+        assertThat(diffCalculator(RoundedSquare(.5f)))
+            .isLessThan(diffCalculator(RoundedSquare(.4f)))
+    }
+
+    @Test
+    fun `pickBestShape picks circle`() {
+        val r = AREA_CALC_SIZE / 2
+        val pathStr = "M 50 0 a 50 50 0 0 1 0 100 a 50 50 0 0 1 0 -100"
+        val path = Path().apply { addCircle(r.toFloat(), r.toFloat(), r.toFloat(), Direction.CW) }
+        assertThat(pickBestShape(path, pathStr)).isInstanceOf(Circle::class.java)
+    }
+
+    @Test
+    fun `pickBestShape picks rounded rect`() {
+        val factor = 0.5f
+        var shape = pickBestShape(roundedRectPath(factor), roundedRectString(factor))
+        assertThat(shape).isInstanceOf(RoundedSquare::class.java)
+        assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor)
+
+        val factor2 = 0.2f
+        shape = pickBestShape(roundedRectPath(factor2), roundedRectString(factor2))
+        assertThat(shape).isInstanceOf(RoundedSquare::class.java)
+        assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor2)
+    }
+
+    @Test
+    fun `pickBestShape picks generic shape`() {
+        val path = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE))
+        val pathStr = FOUR_SIDED_COOKIE
+        val shape = pickBestShape(path, pathStr)
+        assertThat(shape).isInstanceOf(GenericPathShape::class.java)
+
+        val diffCalculator = areaDiffCalculator(path)
+        assertThat(diffCalculator(shape)).isLessThan(AREA_DIFF_THRESHOLD)
+    }
+
+    @Test
+    fun `generic shape creates smooth animation`() {
+        val shape = GenericPathShape(FOUR_SIDED_COOKIE)
+        val target = TestClipView()
+        val anim =
+            shape.createRevealAnimator(
+                target,
+                Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE),
+                Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE),
+                AREA_CALC_SIZE * .25f,
+                false,
+            )
+
+        // Verify that the start rect is similar to initial path
+        anim.setCurrentFraction(0f)
+        assertThat(
+                getAreaDiff(
+                    target.currentClip!!,
+                    cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)),
+                )
+            )
+            .isLessThan(AREA_CALC_SIZE)
+
+        // Verify that end rect is similar to end path
+        anim.setCurrentFraction(1f)
+        assertThat(getAreaDiff(target.currentClip!!, roundedRectPath(0.5f)))
+            .isLessThan(AREA_CALC_SIZE)
+
+        // Ensure that when running animation, area increases smoothly. We run the animation over
+        // [steps] and verify increase of max 5 times the linear diff increase
+        val steps = 1000
+        val incrementalDiff =
+            getAreaDiff(
+                cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)),
+                roundedRectPath(0.5f),
+            ) * 5 / steps
+        var lastPath = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE))
+        for (progress in 1..steps) {
+            anim.setCurrentFraction(progress / 1000f)
+            val currentPath = Path(target.currentClip!!)
+            assertThat(getAreaDiff(lastPath, currentPath)).isLessThan(incrementalDiff)
+            lastPath = currentPath
+        }
+        assertThat(getAreaDiff(lastPath, roundedRectPath(0.5f))).isLessThan(AREA_CALC_SIZE)
+    }
+
+    private fun roundedRectPath(factor: Float) =
+        Path().apply {
+            val r = factor * AREA_CALC_SIZE / 2
+            addRoundRect(
+                0f,
+                0f,
+                AREA_CALC_SIZE.toFloat(),
+                AREA_CALC_SIZE.toFloat(),
+                r,
+                r,
+                Direction.CW,
+            )
+        }
+
+    private fun roundedRectString(factor: Float): String {
+        val s = 100f
+        val r = (factor * s / 2)
+        val t = s - r
+        return "M $r 0 " +
+            "L $t 0 " +
+            "A $r $r 0 0 1 $s $r " +
+            "L $s $t " +
+            "A $r $r 0 0 1 $t $s " +
+            "L $r $s " +
+            "A $r $r 0 0 1 0 $t " +
+            "L 0 $r " +
+            "A $r $r 0 0 1 $r 0 Z"
+    }
+
+    private fun getAreaDiff(p1: Path, p2: Path): Int {
+        val fullRegion = Region(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
+        val iconRegion = Region().apply { setPath(p1, fullRegion) }
+        val shapeRegion = Region().apply { setPath(p2, fullRegion) }
+        shapeRegion.op(iconRegion, Region.Op.XOR)
+        return GraphicsUtils.getArea(shapeRegion)
+    }
+
+    class TestClipView : View(context), ClipPathView {
+
+        var currentClip: Path? = null
+
+        override fun setClipPath(clipPath: Path?) {
+            currentClip = clipPath
+        }
+    }
+
+    companion object {
+        const val FOUR_SIDED_COOKIE =
+            "M63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z"
+
+        private fun cookiePath(bounds: Rect) =
+            PathParser.createPathFromPathData(FOUR_SIDED_COOKIE).apply {
+                transform(
+                    Matrix().apply { setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(bounds), FILL) }
+                )
+            }
+    }
+}
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/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index e2f9feb9a..2e02eb0 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -64,6 +64,7 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
+    @ScreenRecordRule.ScreenRecord // b/383917141
     public void testDragToFolder() {
         // TODO: add the use case to drag an icon to an existing folder. Currently it either fails
         // on tablets or phones due to difference in resolution.
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 1816030..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);
@@ -110,6 +109,9 @@
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
     public void testUninstallFromAllApps() throws Exception {
+        // Ensure no existing app icons on the workspace cause scroll to all apps interruptions
+        mLauncher.clearLauncherData();
+
         installDummyAppAndWaitForUIUpdate();
         try {
             Workspace workspace = mLauncher.getWorkspace();
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/tablet/TaplIsTabletTest.kt b/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt
deleted file mode 100644
index a6de607..0000000
--- a/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt
+++ /dev/null
@@ -1,48 +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.launcher3.tablet
-
-import android.platform.test.rule.AllowedDevices
-import android.platform.test.rule.DeviceProduct
-import com.android.launcher3.Launcher
-import com.android.launcher3.ui.AbstractLauncherUiTest
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
-import org.junit.Test
-
-class TaplIsTabletTest : AbstractLauncherUiTest<Launcher>() {
-
-    /** Investigating b/366237798 by isolating and seeing flake rate of mLauncher.isTablet */
-    @Test
-    @AllowedDevices(
-        DeviceProduct.CF_FOLDABLE,
-        DeviceProduct.CF_TABLET,
-        DeviceProduct.TANGORPRO,
-        DeviceProduct.FELIX,
-        DeviceProduct.COMET,
-    )
-    fun isTabletShouldBeTrue() {
-        assertTrue(mLauncher.isTablet)
-    }
-
-    /** Investigating b/366237798 by isolating and seeing flake rate of mLauncher.isTablet */
-    @Test
-    @AllowedDevices(DeviceProduct.CF_PHONE, DeviceProduct.CHEETAH)
-    fun isTabletShouldBeFalse() {
-        assertFalse(mLauncher.isTablet)
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index d49168f..124c18f 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -76,8 +76,8 @@
                     mTest.mDevice.setOrientationNatural();
                     mTest.executeOnLauncher(launcher ->
                     {
-                        LauncherPrefs.get(launcher).put(FIXED_LANDSCAPE_MODE, false);
                         if (launcher != null) {
+                            LauncherPrefs.get(launcher).put(FIXED_LANDSCAPE_MODE, false);
                             launcher.getRotationHelper().forceAllowRotationForTesting(false);
                         }
                     });
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 2fb7987..7e8d759 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -51,6 +51,7 @@
 import com.android.launcher3.util.BlockingBroadcastReceiver;
 import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
 import org.junit.Before;
@@ -74,6 +75,9 @@
     @Rule
     public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher();
 
+    @Rule
+    public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+
     private String mCallbackAction;
     private String mShortcutId;
     private int mAppWidgetId;
@@ -87,6 +91,7 @@
     @Test
     public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ }
 
+    @ScreenRecordRule.ScreenRecord // b/386243192
     @Test
     public void testPinWidgetNoConfig() throws Throwable {
         runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
@@ -95,6 +100,7 @@
                         .equals(AppWidgetNoConfig.class.getName()));
     }
 
+    @ScreenRecordRule.ScreenRecord // b/386243192
     @Test
     public void testPinWidgetNoConfig_customPreview() throws Throwable {
         // Command to set custom preview
@@ -108,6 +114,7 @@
                         .equals(AppWidgetNoConfig.class.getName()), command);
     }
 
+    @ScreenRecordRule.ScreenRecord // b/386243192
     @Test
     public void testPinWidgetWithConfig() throws Throwable {
         runTest("pinWidgetWithConfig", true,
@@ -117,6 +124,7 @@
                                 .equals(AppWidgetWithConfig.class.getName()));
     }
 
+    @ScreenRecordRule.ScreenRecord // b/386243192
     @Test
     public void testPinShortcut() throws Throwable {
         // Command to set the shortcut id
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
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index 2edd129..e544b19 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -37,7 +37,6 @@
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.After;
 import org.junit.Before;
@@ -283,7 +282,6 @@
 
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/330232490
     public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() {
         Workspace workspace = mLauncher.getWorkspace();
 
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index c852729..a123170 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -61,8 +61,7 @@
         setThemeEnabled(false);
         new FavoriteItemsTransaction(targetContext()).commit();
         loadLauncherSync();
-        goToState(LauncherState.ALL_APPS);
-        freezeAllApps();
+        switchToAllApps();
 
         scrollToAppIcon(APP_NAME);
         BubbleTextView btv = getFromLauncher(
@@ -76,8 +75,7 @@
         setThemeEnabled(false);
         new FavoriteItemsTransaction(targetContext()).commit();
         loadLauncherSync();
-        goToState(LauncherState.ALL_APPS);
-        freezeAllApps();
+        switchToAllApps();
 
         scrollToAppIcon(TEST_APP_NAME);
         BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
@@ -95,8 +93,7 @@
         setThemeEnabled(true);
         new FavoriteItemsTransaction(targetContext()).commit();
         loadLauncherSync();
-        goToState(LauncherState.ALL_APPS);
-        freezeAllApps();
+        switchToAllApps();
 
         scrollToAppIcon(APP_NAME);
         BubbleTextView btv = getFromLauncher(l ->
@@ -109,8 +106,7 @@
     public void testShortcutIconWithTheme() throws Exception {
         setThemeEnabled(true);
         loadLauncherSync();
-        goToState(LauncherState.ALL_APPS);
-        freezeAllApps();
+        switchToAllApps();
 
         scrollToAppIcon(TEST_APP_NAME);
         BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
@@ -158,6 +154,13 @@
         }
     }
 
+    private void switchToAllApps() {
+        goToState(LauncherState.ALL_APPS);
+        waitForState("Launcher internal state didn't switch to All Apps",
+                () -> LauncherState.ALL_APPS);
+        freezeAllApps();
+    }
+
     private void scrollToAppIcon(String appName) {
         executeOnLauncher(l -> {
             l.hideKeyboard();