Merge "Preserve bubble bar state." into main
diff --git a/Android.bp b/Android.bp
index 9d7aa73..223e2c2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -66,6 +66,8 @@
     srcs: [
         "quickstep/src/**/*.kt",
         "quickstep/src/**/*.java",
+    ],
+    device_common_srcs: [
         ":launcher-quickstep-processed-protolog-src",
     ],
 }
@@ -90,7 +92,7 @@
     ],
 }
 
-genrule {
+java_genrule {
     name: "launcher-quickstep-processed-protolog-src",
     srcs: [
         ":protolog-impl",
@@ -108,7 +110,7 @@
     out: ["launcher.quickstep.protolog.srcjar"],
 }
 
-genrule {
+java_genrule {
     name: "gen-launcher.quickstep.protolog.pb",
     srcs: [
         ":launcher-quickstep-unprocessed-protolog-src",
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 6ff3bb2..949acc1 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -369,6 +369,13 @@
 }
 
 flag {
+    name: "work_scheduler_in_work_profile"
+    namespace: "launcher"
+    description: "Enables work scheduler view above the work pause button in work profile."
+    bug: "361589193"
+}
+
+flag {
     name: "one_grid_specs"
     namespace: "launcher"
     description: "Defines the new specs for grids based on OneGrid"
@@ -494,8 +501,8 @@
 }
 
 flag {
-    name: "enable_launcher_pill"
+    name: "enable_contrast_tiles"
     namespace: "launcher"
-    description: "Enable Workspace Launcher Pill in Simple Mode View."
+    description: "Enable launcher app contrast tiles."
     bug: "341217082"
-}
\ No newline at end of file
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index c59978f..4335f76 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -47,4 +47,11 @@
     metadata {
       purpose: PURPOSE_BUGFIX
     }
+}
+
+flag {
+    name: "enable_desktop_windowing_carousel_detach"
+    namespace: "launcher_overview"
+    description: "Makes the desktop windowing task carousel detaches from fullscreen task carousel during quickswitch."
+    bug: "353947917"
 }
\ No newline at end of file
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index 0d3825f..b699d93 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -22,8 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"تثبيت"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"شكل مجاني"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"الكمبيوتر المكتبي"</string>
-    <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
-    <skip />
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"نقل التطبيق إلى شاشة خارجية"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"كمبيوتر مكتبي"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ما مِن عناصر تم استخدامها مؤخرًا"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"إعدادات استخدام التطبيق"</string>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index 890959a..b30b000 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -22,8 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"固定"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"フリーフォーム"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"デスクトップ"</string>
-    <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
-    <skip />
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"外部ディスプレイに移動する"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"パソコン"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"最近のアイテムはありません"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"アプリの使用状況の設定"</string>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index d5216a5..2634b94 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -22,8 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Закачи"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Работна површина"</string>
-    <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
-    <skip />
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Префрлете се на надворешниот екран"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"За компјутер"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Нема неодамнешни ставки"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Поставки за користење на апликациите"</string>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index 1f93cb6..99b53d9 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -22,8 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ปักหมุด"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"รูปแบบอิสระ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"เดสก์ท็อป"</string>
-    <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
-    <skip />
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ย้ายไปยังจอแสดงผลภายนอก"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"เดสก์ท็อป"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ไม่มีรายการล่าสุด"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"การตั้งค่าการใช้แอป"</string>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 2bf4a13..3a32551 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -22,8 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Ghim"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Dạng tự do"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Máy tính"</string>
-    <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
-    <skip />
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Chuyển sang màn hình ngoài"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Máy tính"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Không có mục gần đây nào"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Cài đặt mức sử dụng ứng dụng"</string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 451ba55..782a705 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -111,6 +111,7 @@
     <dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
     <dimen name="motion_pause_detector_speed_slow">0.15dp</dimen>
     <dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
+    <dimen name="motion_pause_detector_speed_trackpad_somewhat_fast">0.7dp</dimen>
     <dimen name="motion_pause_detector_speed_fast">1.4dp</dimen>
     <dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
     <dimen name="quickstep_fling_threshold_speed">0.5dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index dd2ff2d..6916a1d 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -32,6 +32,8 @@
 import androidx.core.animation.addListener
 import com.android.app.animation.Interpolators
 import com.android.quickstep.RemoteRunnable
+import com.android.wm.shell.shared.animation.MinimizeAnimator
+import com.android.wm.shell.shared.animation.WindowAnimator
 import java.util.concurrent.Executor
 
 /**
@@ -77,7 +79,13 @@
         val launchAnimator =
             createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
         val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
-        val minimizeAnimator = createMinimizeAnimator(minimizeChange, transaction, finishCallback)
+        val minimizeAnimator =
+            MinimizeAnimator.create(
+                context.resources.displayMetrics,
+                minimizeChange,
+                transaction,
+                finishCallback,
+            )
         return listOf(launchAnimator, minimizeAnimator)
     }
 
@@ -96,7 +104,7 @@
     ): Animator {
         val boundsAnimator =
             WindowAnimator.createBoundsAnimator(
-                context,
+                context.resources.displayMetrics,
                 launchBoundsAnimationDef,
                 change,
                 transaction,
@@ -115,32 +123,6 @@
         }
     }
 
-    private fun createMinimizeAnimator(
-        change: Change,
-        transaction: Transaction,
-        onAnimFinish: (Animator) -> Unit,
-    ): Animator {
-        val boundsAnimator =
-            WindowAnimator.createBoundsAnimator(
-                context,
-                minimizeBoundsAnimationDef,
-                change,
-                transaction,
-            )
-        val alphaAnimator =
-            ValueAnimator.ofFloat(1f, 0f).apply {
-                duration = MINIMIZE_ANIM_ALPHA_DURATION_MS
-                interpolator = Interpolators.LINEAR
-                addUpdateListener { animation ->
-                    transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
-                }
-            }
-        return AnimatorSet().apply {
-            playTogether(boundsAnimator, alphaAnimator)
-            addListener(onEnd = { animation -> onAnimFinish(animation) })
-        }
-    }
-
     companion object {
         private val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
 
@@ -154,13 +136,5 @@
                 startScale = 0.97f,
                 interpolator = Interpolators.STANDARD_DECELERATE,
             )
-
-        private val minimizeBoundsAnimationDef =
-            WindowAnimator.BoundsAnimationParams(
-                durationMs = 200,
-                endOffsetYDp = 12f,
-                endScale = 0.97f,
-                interpolator = Interpolators.STANDARD_ACCELERATE,
-            )
     }
 }
diff --git a/quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt b/quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt
deleted file mode 100644
index 1a99a36..0000000
--- a/quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt
+++ /dev/null
@@ -1,100 +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.desktop
-
-import android.animation.RectEvaluator
-import android.animation.ValueAnimator
-import android.content.Context
-import android.graphics.Rect
-import android.util.TypedValue
-import android.view.SurfaceControl
-import android.view.animation.Interpolator
-import android.window.TransitionInfo
-
-/** Creates animations that can be applied to windows/surfaces. */
-object WindowAnimator {
-
-    /** Parameters defining a window bounds animation. */
-    data class BoundsAnimationParams(
-        val durationMs: Long,
-        val startOffsetYDp: Float = 0f,
-        val endOffsetYDp: Float = 0f,
-        val startScale: Float = 1f,
-        val endScale: Float = 1f,
-        val interpolator: Interpolator,
-    )
-
-    /**
-     * Creates an animator to reposition and scale the bounds of the leash of the given change.
-     *
-     * @param boundsAnimDef the parameters for the animation itself (duration, scale, position)
-     * @param change the change to which the animation should be applied
-     * @param transaction the transaction to apply the animation to
-     */
-    fun createBoundsAnimator(
-        context: Context,
-        boundsAnimDef: BoundsAnimationParams,
-        change: TransitionInfo.Change,
-        transaction: SurfaceControl.Transaction,
-    ): ValueAnimator {
-        val startBounds =
-            createBounds(
-                context,
-                change.startAbsBounds,
-                boundsAnimDef.startScale,
-                boundsAnimDef.startOffsetYDp,
-            )
-        val leash = change.leash
-        val endBounds =
-            createBounds(
-                context,
-                change.startAbsBounds,
-                boundsAnimDef.endScale,
-                boundsAnimDef.endOffsetYDp,
-            )
-        return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
-            duration = boundsAnimDef.durationMs
-            interpolator = boundsAnimDef.interpolator
-            addUpdateListener { animation ->
-                val animBounds = animation.animatedValue as Rect
-                val animScale = 1 - (1 - boundsAnimDef.endScale) * animation.animatedFraction
-                transaction
-                    .setPosition(leash, animBounds.left.toFloat(), animBounds.top.toFloat())
-                    .setScale(leash, animScale, animScale)
-                    .apply()
-            }
-        }
-    }
-
-    private fun createBounds(context: Context, origBounds: Rect, scale: Float, offsetYDp: Float) =
-        Rect(origBounds).apply {
-            check(scale in 0.0..1.0)
-            // Scale the  bounds down with an anchor in the center
-            inset(
-                (origBounds.width().toFloat() * (1 - scale) / 2).toInt(),
-                (origBounds.height().toFloat() * (1 - scale) / 2).toInt(),
-            )
-            val offsetYPx =
-                TypedValue.applyDimension(
-                        TypedValue.COMPLEX_UNIT_DIP,
-                        offsetYDp,
-                        context.resources.displayMetrics,
-                    )
-                    .toInt()
-            offset(/* dx= */ 0, offsetYPx)
-        }
-}
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 29e1f4e..2f4c6f6 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -59,7 +59,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
@@ -156,9 +155,6 @@
                         state.containerId);
         FixedContainerItems fci = new FixedContainerItems(state.containerId,
                 state.storage.read(mApp.getContext(), factory, ums.allUsers::get));
-        if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-            bindPredictionItems(callbacks, fci);
-        }
         mDataModel.extraItems.put(state.containerId, fci);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 929e793..e3bcb0d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -25,20 +25,24 @@
 
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.statemanager.StateManager;
-import com.android.quickstep.RecentsActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 
 import java.util.stream.Stream;
 
 /**
  * A data source which integrates with the fallback RecentsActivity instance (for 3P launchers).
+ * @param <T> The type of the RecentsViewContainer that will handle Recents state changes.
  */
-public class FallbackTaskbarUIController extends TaskbarUIController {
+public class FallbackTaskbarUIController
+        <T extends RecentsViewContainer & StatefulContainer<RecentsState>>
+        extends TaskbarUIController {
 
-    private final RecentsActivity mRecentsActivity;
+    private final T mRecentsContainer;
 
     private final StateManager.StateListener<RecentsState> mStateListener =
             new StateManager.StateListener<RecentsState>() {
@@ -63,23 +67,23 @@
                 }
             };
 
-    public FallbackTaskbarUIController(RecentsActivity recentsActivity) {
-        mRecentsActivity = recentsActivity;
+    public FallbackTaskbarUIController(T recentsContainer) {
+        mRecentsContainer = recentsContainer;
     }
 
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
         super.init(taskbarControllers);
-        mRecentsActivity.setTaskbarUIController(this);
-        mRecentsActivity.getStateManager().addStateListener(mStateListener);
+        mRecentsContainer.setTaskbarUIController(this);
+        mRecentsContainer.getStateManager().addStateListener(mStateListener);
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
         getRecentsView().setTaskLaunchListener(null);
-        mRecentsActivity.setTaskbarUIController(null);
-        mRecentsActivity.getStateManager().removeStateListener(mStateListener);
+        mRecentsContainer.setTaskbarUIController(null);
+        mRecentsContainer.getStateManager().removeStateListener(mStateListener);
     }
 
     /**
@@ -109,7 +113,7 @@
 
     @Override
     public RecentsView getRecentsView() {
-        return mRecentsActivity.getOverviewPanel();
+        return mRecentsContainer.getOverviewPanel();
     }
 
     @Override
@@ -131,11 +135,11 @@
     @Nullable
     @Override
     protected TISBindHelper getTISBindHelper() {
-        return mRecentsActivity.getTISBindHelper();
+        return mRecentsContainer.getTISBindHelper();
     }
 
     @Override
     protected String getTaskbarUIControllerName() {
-        return "FallbackTaskbarUIController";
+        return "FallbackTaskbarUIController<" + mRecentsContainer.getClass().getSimpleName() + ">";
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 9912c6c..711a49a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -63,6 +63,11 @@
     private int mTaskListChangeId = -1;
     // Only empty before the recent tasks list has been loaded the first time
     @NonNull private List<GroupTask> mTasks = new ArrayList<>();
+    // Set of task IDs filtered out of tasks in recents model to generate list of tasks to show in
+    // the Keyboard Quick Switch view. Non empty only if the view has been shown in response to
+    // toggling taskbar overflow button.
+    @NonNull private Set<Integer> mExcludedTaskIds = Collections.emptySet();
+
     private int mNumHiddenTasks = 0;
 
     // Initialized in init
@@ -90,10 +95,12 @@
             return;
         }
         int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex();
+        boolean wasOpenedFromTaskbar = mQuickSwitchViewController.wasOpenedFromTaskbar();
         onDestroy();
         if (currentFocusedIndex != -1) {
             mControllers.taskbarActivityContext.getMainThreadHandler().post(
-                    () -> openQuickSwitchView(currentFocusedIndex));
+                    () -> openQuickSwitchView(currentFocusedIndex, mExcludedTaskIds,
+                            wasOpenedFromTaskbar));
         }
     }
 
@@ -102,10 +109,19 @@
     }
 
     /**
-     * Opens the view with a filtered list of tasks.
+     * Opens or closes the view in response to taskbar action. The view shows a filtered list of
+     * tasks.
      * @param taskIdsToExclude A list of tasks to exclude in the opened view.
      */
-    void openQuickSwitchView(@NonNull Set<Integer> taskIdsToExclude) {
+    void toggleQuickSwitchViewForTaskbar(@NonNull Set<Integer> taskIdsToExclude) {
+        // Close the view if its shown, and was opened from the taskbar.
+        if (mQuickSwitchViewController != null
+                && !mQuickSwitchViewController.isCloseAnimationRunning()
+                && mQuickSwitchViewController.wasOpenedFromTaskbar()) {
+            closeQuickSwitchView(true);
+            return;
+        }
+
         openQuickSwitchView(-1, taskIdsToExclude, true);
     }
 
@@ -117,10 +133,16 @@
             @NonNull Set<Integer> taskIdsToExclude,
             boolean wasOpenedFromTaskbar) {
         if (mQuickSwitchViewController != null) {
-            if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
+            if (!mQuickSwitchViewController.isCloseAnimationRunning()
+                    && mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
                 return;
             }
-            // Allow the KQS to be reopened during the close animation to make it more responsive
+
+            // Allow the KQS to be reopened during the close animation to make it more responsive.
+            // Similarly, if KQS was opened in different mode (from taskbar vs. keyboard event),
+            // close it so it can be reopened in the correct mode.
+            // TODO(b/368119679) Consider updating list of shown tasks in place, or at least reopen
+            // the view in the same vertical location.
             closeQuickSwitchView(false);
         }
         mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
@@ -139,9 +161,8 @@
         final boolean onDesktop =
                 mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible();
 
-        // TODO(b/368119679) For now we will re-process the task list every time, but this can be
-        // optimized if we have the same set of task ids to exclude.
-        if (mModel.isTaskListValid(mTaskListChangeId) && !Flags.taskbarOverflow()) {
+        if (mModel.isTaskListValid(mTaskListChangeId)
+                && taskIdsToExclude.equals(mExcludedTaskIds)) {
             // When we are opening the KQS with no focus override, check if the first task is
             // running. If not, focus that first task.
             mQuickSwitchViewController.openQuickSwitchView(
@@ -157,6 +178,7 @@
             return;
         }
 
+        mExcludedTaskIds = taskIdsToExclude;
         mTaskListChangeId = mModel.getTasks((tasks) -> {
             mHasDesktopTask = false;
             mWasDesktopTaskFilteredOut = false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 50a253c..05d34b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -131,6 +131,15 @@
     }
 
     @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mViewCallbacks != null) {
+            mViewCallbacks.onViewDetchedFromWindow();
+        }
+    }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane);
@@ -281,6 +290,10 @@
         return mDesktopTaskIndex;
     }
 
+    void resetViewCallbacks() {
+        mViewCallbacks = null;
+    }
+
     protected Animator getCloseAnimation() {
         AnimatorSet closeAnimation = new AnimatorSet();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 2902d55..7a63f74 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -31,12 +31,14 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.jank.Cuj;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.desktop.DesktopAppLaunchTransition;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
@@ -69,6 +71,9 @@
 
     private boolean mOnDesktop;
     private boolean mWasDesktopTaskFilteredOut;
+    private boolean mWasOpenedFromTaskbar;
+
+    private boolean mDetachingFromWindow = false;
 
     protected KeyboardQuickSwitchViewController(
             @NonNull TaskbarControllers controllers,
@@ -85,6 +90,10 @@
         return mCurrentFocusIndex;
     }
 
+    protected boolean wasOpenedFromTaskbar() {
+        return mWasOpenedFromTaskbar;
+    }
+
     protected void openQuickSwitchView(
             @NonNull List<GroupTask> tasks,
             int numHiddenTasks,
@@ -94,10 +103,20 @@
             boolean hasDesktopTask,
             boolean wasDesktopTaskFilteredOut,
             boolean wasOpenedFromTaskbar) {
-        positionView(wasOpenedFromTaskbar);
+        final boolean isTransientTaskBar = DisplayController.isTransientTaskbar(
+                mControllers.taskbarActivityContext);
+        positionView(wasOpenedFromTaskbar, isTransientTaskBar);
+
+        // Keep the taskbar unstashed if the KQS is opened.
+        if (wasOpenedFromTaskbar && isTransientTaskBar) {
+            mControllers.taskbarStashController.updateTaskbarTimeout(/* isAutohideSuspended= */
+                    true);
+        }
+
         mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
         mOnDesktop = onDesktop;
         mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
+        mWasOpenedFromTaskbar = wasOpenedFromTaskbar;
 
         mKeyboardQuickSwitchView.applyLoadPlan(
                 mOverlayContext,
@@ -109,7 +128,7 @@
                 /* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
     }
 
-    protected void positionView(boolean wasOpenedFromTaskbar) {
+    protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) {
         if (!wasOpenedFromTaskbar) {
             // Keep the default positioning.
             return;
@@ -120,8 +139,16 @@
         final Resources resources = mKeyboardQuickSwitchView.getResources();
         final int marginHorizontal = resources.getDimensionPixelSize(
                 R.dimen.keyboard_quick_switch_margin_ends);
-        final int marginBottom = resources.getDimensionPixelSize(
+
+        final DeviceProfile dp = mControllers.taskbarActivityContext.getDeviceProfile();
+        // Calculate the additional margin space that the KQS should move up for the transient
+        // taskbar. The value of spaceForTaskbar is the distance between the bottom of the KQS
+        // view with 0 bottom margin to the top of the transient taskbar view.
+        final int spaceForTaskbar = isTransientTaskbar ? dp.taskbarHeight + dp.taskbarBottomMargin
+                - dp.stashedTaskbarHeight : 0;
+        final int marginBottom = spaceForTaskbar + resources.getDimensionPixelSize(
                 R.dimen.keyboard_quick_switch_margin_bottom);
+
         lp.setMargins(marginHorizontal, 0, marginHorizontal, marginBottom);
         lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
         lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
@@ -237,7 +264,12 @@
 
     private void onCloseComplete() {
         mCloseAnimation = null;
-        mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+        // Reset the view callbacks to prevent `onDetachedFromWindow` getting called in response to
+        // the `removeView(mKeyboardQuickSwitchView)` call.
+        mKeyboardQuickSwitchView.resetViewCallbacks();
+        if (!mDetachingFromWindow) {
+            mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+        }
         mControllerCallbacks.onCloseComplete();
         InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
     }
@@ -254,6 +286,7 @@
         pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
         pw.println(prefix + "\tmOnDesktop=" + mOnDesktop);
         pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
+        pw.println(prefix + "\tmWasOpenedFromTaskbar=" + mWasOpenedFromTaskbar);
     }
 
     /**
@@ -327,5 +360,11 @@
         boolean isAspectRatioSquare() {
             return mControllerCallbacks.isAspectRatioSquare();
         }
+
+        void onViewDetchedFromWindow() {
+            mDetachingFromWindow = true;
+            closeQuickSwitchView(false);
+            mDetachingFromWindow = false;
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 042bc9a..09dbeb6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.data.ItemInfo;
@@ -69,17 +68,14 @@
     public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1;
     public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2;
     public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3;
-    public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4;
 
-    public static final int DISPLAY_PROGRESS_COUNT = 5;
+    public static final int DISPLAY_PROGRESS_COUNT = 4;
 
     private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat(
             this::onInAppDisplayProgressChanged);
     private final MultiPropertyFactory<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp =
             new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress,
                     AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max);
-    private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat(
-            this::launcherPauseProgressUpdate);
 
     private final QuickstepLauncher mLauncher;
     private final HomeVisibilityState mHomeState;
@@ -195,33 +191,6 @@
     }
 
     /**
-     * Called when Launcher Activity is paused/resumed.
-     * <p>
-     * To avoid UI clash between taskbar & bottom sheet, shift nav buttons down on launcher
-     * pause/resume at home.
-     * @param paused if launcher is currently paused.
-     */
-    public void onLauncherPausedOrResumed(boolean paused) {
-        if (!FeatureFlags.enableHomeTransitionListener()) {
-            onLauncherVisibilityChanged(mLauncher.hasBeenResumed());
-            return;
-        }
-
-        // Animate navbar iff pause/resume from home, NOT to/from app (avoid overriding existing
-        // animations).
-        boolean launcherPauseOrResumeFromHome = mHomeState.isHomeVisible() && mControllers
-                .taskbarAutohideSuspendController.isSuspendedForTransientTaskbarInLauncher();
-        if (launcherPauseOrResumeFromHome) {
-            mLauncherPauseProgress.animateToValue(paused ? 1.0f : 0.0f).start();
-        }
-    }
-
-    private void launcherPauseProgressUpdate() {
-        onTaskbarInAppDisplayProgressUpdate(
-                mLauncherPauseProgress.value, LAUNCHER_PAUSE_PROGRESS_INDEX);
-    }
-
-    /**
      * Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
      */
     @Override
@@ -395,20 +364,18 @@
         }
         if (mControllers.uiController.isIconAlignedWithHotseat()
                 && !mTaskbarLauncherStateController.isAnimatingToLauncher()) {
-            // Only animate nav button position while home and not animating home, otherwise let
+            // Only animate the nav buttons while home and not animating home, otherwise let
             // the TaskbarViewController handle it.
             mControllers.navbarButtonsViewController
-                    .getNavButtonTranslationYForInAppDisplay()
+                    .getTaskbarNavButtonTranslationYForInAppDisplay()
                     .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
                             * mTaskbarInAppDisplayProgress.value);
-            if (!mLauncher.isPaused()) {
-                mControllers.navbarButtonsViewController
-                        .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
-            }
+            mControllers.navbarButtonsViewController
+                    .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
         }
     }
 
-    @Override
+    /** Returns true iff any in-app display progress > 0. */
     public boolean shouldUseInAppLayout() {
         return mTaskbarInAppDisplayProgress.value > 0;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index cfcbd2f..7d8e93c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -183,7 +183,7 @@
 
     private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat(
             this::updateNavButtonTranslationY);
-    private final AnimatedFloat mNavButtonTranslationYForInAppDisplay = new AnimatedFloat(
+    private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat(
             this::updateNavButtonTranslationY);
     private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat(
             this::updateNavButtonTranslationY);
@@ -704,8 +704,8 @@
     }
 
     /** Use to set the translationY for the all nav+contextual buttons when in Launcher */
-    public AnimatedFloat getNavButtonTranslationYForInAppDisplay() {
-        return mNavButtonTranslationYForInAppDisplay;
+    public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() {
+        return mTaskbarNavButtonTranslationYForInAppDisplay;
     }
 
     /** Use to set the dark intensity for the all nav+contextual buttons */
@@ -751,20 +751,18 @@
         if (mContext.isPhoneButtonNavMode()) {
             return;
         }
-        mLastSetNavButtonTranslationY = calculateNavButtonTranslationY();
-        mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY);
-    }
+        final float normalTranslationY = mTaskbarNavButtonTranslationY.value;
+        final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value;
+        TaskbarUIController uiController = mControllers.uiController;
+        final float inAppDisplayAdjustmentTranslationY =
+                (uiController instanceof LauncherTaskbarUIController
+                        && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout())
+                        ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0;
 
-    /**
-     * Calculates the translationY of the nav buttons based on the current device state.
-     */
-    private float calculateNavButtonTranslationY() {
-        float translationY =
-                mTaskbarNavButtonTranslationY.value + mTaskbarNavButtonTranslationYForIme.value;
-        if (mControllers.uiController.shouldUseInAppLayout()) {
-            translationY += mNavButtonTranslationYForInAppDisplay.value;
-        }
-        return translationY;
+        mLastSetNavButtonTranslationY = normalTranslationY
+                + imeAdjustmentTranslationY
+                + inAppDisplayAdjustmentTranslationY;
+        mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY);
     }
 
     /**
@@ -1164,7 +1162,7 @@
         pw.println(prefix + "\t\tmTaskbarNavButtonTranslationY="
                 + mTaskbarNavButtonTranslationY.value);
         pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForInAppDisplay="
-                + mNavButtonTranslationYForInAppDisplay.value);
+                + mTaskbarNavButtonTranslationYForInAppDisplay.value);
         pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme="
                 + mTaskbarNavButtonTranslationYForIme.value);
         pw.println(prefix + "\t\tmTaskbarNavButtonDarkIntensity="
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6f1e96f..e22de06 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -29,7 +29,6 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
-import static com.android.launcher3.Flags.taskbarOverflow;
 import static com.android.launcher3.Utilities.calculateTextHeight;
 import static com.android.launcher3.Utilities.isRunningInTestHarness;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
@@ -1217,9 +1216,7 @@
         boolean shouldCloseAllOpenViews = true;
         Object tag = view.getTag();
 
-        if (taskbarOverflow()) {
-            mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
-        }
+        mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
 
         if (tag instanceof GroupTask groupTask) {
             handleGroupTaskLaunch(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 4a6b6d4..a8ce10f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -21,7 +21,6 @@
 import android.graphics.Paint
 import android.graphics.Rect
 import android.graphics.Region
-import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
 import android.os.Binder
 import android.os.IBinder
 import android.view.DisplayInfo
@@ -150,7 +149,7 @@
         }
         if (
             taskbarStashController.isInApp ||
-                taskbarStashController.isInOverview ||
+                controllers.uiController.isInOverviewUi ||
                 DisplayController.showLockedTaskbarOnHome(context)
         ) {
             // only add the taskbar touch region if not on home
@@ -259,7 +258,7 @@
         // When in gesture nav, report the stashed height to the IME, to allow hiding the
         // IME navigation bar.
         val imeInsetsSize =
-            if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
+            if (context.isGestureNav) {
                 getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity)
             } else {
                 getInsetsForGravity(taskbarHeightForIme, gravity)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index c18cf28..ab4b1b6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -71,7 +71,9 @@
 import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -115,7 +117,6 @@
     private WindowManager mWindowManager;
     private FrameLayout mTaskbarRootLayout;
     private boolean mAddedWindow;
-    private boolean mIsSuspended;
     private final TaskbarNavButtonController mNavButtonController;
     private final ComponentCallbacks mComponentCallbacks;
 
@@ -131,6 +132,8 @@
 
     private TaskbarActivityContext mTaskbarActivityContext;
     private StatefulActivity mActivity;
+    private RecentsViewContainer mRecentsViewContainer;
+
     /**
      * Cache a copy here so we can initialize state whenever taskbar is recreated, since
      * this class does not get re-initialized w/ new taskbars.
@@ -404,9 +407,28 @@
         }
         mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
 
+        if (activity instanceof RecentsViewContainer recentsViewContainer) {
+            setRecentsViewContainer(recentsViewContainer);
+        }
+    }
+
+    /**
+     * Sets the current RecentsViewContainer, from which we create a TaskbarUIController.
+     */
+    public void setRecentsViewContainer(@NonNull RecentsViewContainer recentsViewContainer) {
+        if (mRecentsViewContainer == recentsViewContainer) {
+            return;
+        }
+        if (mRecentsViewContainer == mActivity) {
+            // When switching to RecentsWindowManager (not an Activity), the old mActivity is not
+            // destroyed, nor is there a new Activity to replace it. Thus if we don't clear it here,
+            // it will not get re-set properly if we return to the Activity (e.g. NexusLauncher).
+            mActivityOnDestroyCallback.run();
+        }
+        mRecentsViewContainer = recentsViewContainer;
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+                    createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
         }
     }
 
@@ -429,12 +451,18 @@
     /**
      * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
      */
-    private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) {
-        if (activity instanceof QuickstepLauncher) {
-            return new LauncherTaskbarUIController((QuickstepLauncher) activity);
+    private TaskbarUIController createTaskbarUIControllerForRecentsViewContainer(
+            RecentsViewContainer container) {
+        if (container instanceof QuickstepLauncher quickstepLauncher) {
+            return new LauncherTaskbarUIController(quickstepLauncher);
         }
-        if (activity instanceof RecentsActivity) {
-            return new FallbackTaskbarUIController((RecentsActivity) activity);
+        // If a 3P Launcher is default, always use FallbackTaskbarUIController regardless of
+        // whether the recents container is RecentsActivity or RecentsWindowManager.
+        if (container instanceof RecentsActivity recentsActivity) {
+            return new FallbackTaskbarUIController<>(recentsActivity);
+        }
+        if (container instanceof RecentsWindowManager recentsWindowManager) {
+            return new FallbackTaskbarUIController<>(recentsWindowManager);
         }
         return TaskbarUIController.DEFAULT;
     }
@@ -446,8 +474,6 @@
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
-        if (mIsSuspended) return;
-
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
@@ -484,9 +510,9 @@
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
             mTaskbarActivityContext.init(mSharedState);
 
-            if (mActivity != null) {
+            if (mRecentsViewContainer != null) {
                 mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+                        createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
 
             if (enableTaskbarNoRecreate()) {
@@ -663,16 +689,6 @@
         }
     }
 
-    /**
-     * Removes Taskbar from the window manager and prevents recreation if {@code true}.
-     * <p>
-     * Suspending is for testing purposes only; avoid calling this method in production.
-     */
-    @VisibleForTesting
-    public void setSuspended(boolean isSuspended) {
-        mIsSuspended = isSuspended;
-    }
-
     private void addTaskbarRootViewToWindow() {
         if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
             mWindowManager.addView(mTaskbarRootLayout,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 7030088..f7f5cf6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -91,14 +91,6 @@
     protected void onStashedInAppChanged() { }
 
     /**
-     * Whether the Taskbar should use in-app layout.
-     * @return {@code true} iff in-app display progress > 0 or Launcher Activity paused.
-     */
-    public boolean shouldUseInAppLayout() {
-        return false;
-    }
-
-    /**
      * Called when taskbar icon layout bounds change.
      */
     protected void onIconLayoutBoundsChanged() { }
@@ -126,6 +118,8 @@
      * Manually closes the overlay window.
      */
     public void hideOverlayWindow() {
+        mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
+
         if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext)
                 || mControllers.taskbarAllAppsController.isOpen()) {
             mControllers.taskbarOverlayController.hideWindow();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 6cc03ab..55bcb23 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -198,11 +198,13 @@
     private int calculateMaxNumIcons() {
         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
         int availableWidth = deviceProfile.widthPx;
+        int defaultEdgeMargin =
+                (int) getResources().getDimension(deviceProfile.inv.inlineNavButtonsEndSpacing);
 
         // Reserve space required for edge margins, or for navbar if shown. If task bar needs to be
         // center aligned with nav bar shown, reserve space on both sides.
-        availableWidth -= Math.max(deviceProfile.edgeMarginPx, deviceProfile.hotseatBarEndOffset);
-        availableWidth -= Math.max(deviceProfile.edgeMarginPx,
+        availableWidth -= Math.max(defaultEdgeMargin, deviceProfile.hotseatBarEndOffset);
+        availableWidth -= Math.max(defaultEdgeMargin,
                 mShouldTryStartAlign ? 0 : deviceProfile.hotseatBarEndOffset);
 
         // The space taken by an item icon used during layout.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 4591f9b..5d769d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -148,7 +148,7 @@
         return new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                mControllers.keyboardQuickSwitchController.openQuickSwitchView(
+                mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
                         mControllers.taskbarViewController.getTaskIdsForPinnedApps());
             }
         };
@@ -159,7 +159,7 @@
         return new View.OnLongClickListener() {
             @Override
             public boolean onLongClick(View v) {
-                mControllers.keyboardQuickSwitchController.openQuickSwitchView(
+                mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
                         mControllers.taskbarViewController.getTaskIdsForPinnedApps());
                 return true;
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 6f2d459..87e19be 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -233,7 +233,7 @@
         mTaskbarNavButtonTranslationY =
                 controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
         mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController
-                .getNavButtonTranslationYForInAppDisplay();
+                .getTaskbarNavButtonTranslationYForInAppDisplay();
 
         mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index bbb0b8b..5e6659f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -57,6 +57,7 @@
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
@@ -177,6 +178,9 @@
         mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
         mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
+        if (!Flags.enableOptionalBubbleOverflow()) {
+            showOverflow(true);
+        }
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 78e5dbd..eca2bc8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -55,9 +55,11 @@
             return animatingBubble.state != AnimatingBubble.State.CREATED
         }
 
+    private var interceptedHandleAnimator = false
+
     private companion object {
         /** The time to show the flyout. */
-        const val FLYOUT_DELAY_MS: Long = 3000
+        const val FLYOUT_DELAY_MS: Long = 10000
         /** The initial scale Y value that the new bubble is set to before the animation starts. */
         const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
         /** The minimum alpha value to make the bubble bar touchable. */
@@ -133,10 +135,21 @@
             dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
         )
 
+    private fun cancelAnimationIfPending() {
+        val animatingBubble = animatingBubble ?: return
+        if (animatingBubble.state != AnimatingBubble.State.CREATED) return
+        scheduler.cancel(animatingBubble.showAnimation)
+        scheduler.cancel(animatingBubble.hideAnimation)
+    }
+
     /** Animates a bubble for the state where the bubble bar is stashed. */
     fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
-        // TODO b/346400677: handle animations for the same bubble interrupting each other
-        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
+        if (isAnimating) {
+            interruptAndUpdateAnimatingBubble(b.view, isExpanding)
+            return
+        }
+        cancelAnimationIfPending()
+
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
@@ -165,17 +178,19 @@
      * 3. The third part is the overshoot of the spring animation, where we make the bubble fully
      *    visible which helps avoiding further updates when we re-enter the second part.
      */
-    private fun buildHandleToBubbleBarAnimation() = Runnable {
+    private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable {
         moveToState(AnimatingBubble.State.ANIMATING_IN)
-        // prepare the bubble bar for the animation
-        bubbleBarView.visibility = VISIBLE
-        bubbleBarView.alpha = 0f
-        bubbleBarView.translationY = 0f
-        bubbleBarView.scaleX = 1f
-        bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
-        bubbleBarView.setBackgroundScaleX(1f)
-        bubbleBarView.setBackgroundScaleY(1f)
-        bubbleBarView.relativePivotY = 0.5f
+        // prepare the bubble bar for the animation if we're starting fresh
+        if (initialVelocity == null) {
+            bubbleBarView.visibility = VISIBLE
+            bubbleBarView.alpha = 0f
+            bubbleBarView.translationY = 0f
+            bubbleBarView.scaleX = 1f
+            bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
+            bubbleBarView.setBackgroundScaleX(1f)
+            bubbleBarView.setBackgroundScaleY(1f)
+            bubbleBarView.relativePivotY = 0.5f
+        }
 
         // this is the offset between the center of the bubble bar and the center of the stash
         // handle. when the handle becomes invisible and we start animating in the bubble bar,
@@ -194,7 +209,7 @@
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
         val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
         animator.setDefaultSpringConfig(springConfig)
-        animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
+        animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f)
         animator.addUpdateListener { handle, values ->
             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
             when {
@@ -314,7 +329,19 @@
                 }
             }
         }
-        animator.addEndListener { _, _, _, canceled, _, _, _ ->
+        animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ ->
+            // PhysicsAnimator calls the end listeners when the animation is replaced with a new one
+            // if we're not in ANIMATING_OUT state, then this animation never started and we should
+            // return
+            if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener
+            if (interceptedHandleAnimator) {
+                interceptedHandleAnimator = false
+                // post this to give a PhysicsAnimator a chance to clean up its internal listeners.
+                // otherwise this end listener will be called as soon as we create a new spring
+                // animation
+                scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity))
+                return@addEndListener
+            }
             animatingBubble = null
             if (!canceled) bubbleStashController.stashBubbleBarImmediate()
             bubbleBarView.relativePivotY = 1f
@@ -326,7 +353,7 @@
         val flyout = bubble?.flyoutMessage
         if (flyout != null) {
             bubbleBarFlyoutController.collapseFlyout {
-                onFlyoutRemoved(bubble.view)
+                onFlyoutRemoved()
                 animator.start()
             }
         } else {
@@ -336,8 +363,6 @@
 
     /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
     fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
-        // TODO b/346400677: handle animations for the same bubble interrupting each other
-        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
@@ -350,8 +375,11 @@
                 buildBubbleBarToHandleAnimation()
             } else {
                 Runnable {
-                    bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) }
-                    animatingBubble = null
+                    moveToState(AnimatingBubble.State.ANIMATING_OUT)
+                    bubbleBarFlyoutController.collapseFlyout {
+                        onFlyoutRemoved()
+                        animatingBubble = null
+                    }
                     bubbleStashController.showBubbleBarImmediate()
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
@@ -394,16 +422,23 @@
     }
 
     fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
-        // TODO b/346400677: handle animations for the same bubble interrupting each other
-        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
+        if (isAnimating) {
+            interruptAndUpdateAnimatingBubble(b.view, isExpanding)
+            return
+        }
+        cancelAnimationIfPending()
+
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
         // first bounce the bubble bar and show the flyout. Then hide the flyout.
         val showAnimation = buildBubbleBarBounceAnimation()
         val hideAnimation = Runnable {
-            bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) }
-            animatingBubble = null
+            moveToState(AnimatingBubble.State.ANIMATING_OUT)
+            bubbleBarFlyoutController.collapseFlyout {
+                onFlyoutRemoved()
+                animatingBubble = null
+            }
             bubbleStashController.showBubbleBarImmediate()
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -462,12 +497,11 @@
     }
 
     private fun cancelFlyout() {
-        val bubbleView = animatingBubble?.bubbleView
-        bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved(bubbleView) }
+        bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() }
     }
 
-    private fun onFlyoutRemoved(bubbleView: BubbleView?) {
-        bubbleView?.suppressDotForBubbleUpdate(false)
+    private fun onFlyoutRemoved() {
+        animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false)
         bubbleStashController.updateTaskbarTouchRegion()
     }
 
@@ -507,6 +541,116 @@
         }
     }
 
+    private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) {
+        val animatingBubble = animatingBubble ?: return
+        when (animatingBubble.state) {
+            AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started
+            AnimatingBubble.State.ANIMATING_IN ->
+                updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding)
+            AnimatingBubble.State.IN ->
+                updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding)
+            AnimatingBubble.State.ANIMATING_OUT ->
+                updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding)
+        }
+    }
+
+    private fun updateAnimationWhileAnimatingIn(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+        if (!bubbleBarFlyoutController.hasFlyout()) {
+            // if the flyout does not yet exist, then we're only animating the bubble bar.
+            // the animating bubble has been updated, so the when the flyout expands it will
+            // show the right message.
+            return
+        }
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            // the flyout is currently expanding and we need to update it with new data
+            bubbleView.suppressDotForBubbleUpdate(true)
+            bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout)
+        } else {
+            // the flyout is expanding but we don't have new flyout data to update it with,
+            // so cancel the expanding flyout.
+            cancelFlyout()
+        }
+    }
+
+    private fun updateAnimationWhileIn(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        // unsuppress the current bubble because we are about to hide its flyout
+        animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+        // we're currently idle, waiting for the hide animation to start. update the flyout
+        // data and reschedule the hide animation to run later to give the user a chance to
+        // see the new flyout.
+        val hideAnimation = animatingBubble.hideAnimation
+        scheduler.cancel(hideAnimation)
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            bubbleView.suppressDotForBubbleUpdate(true)
+            bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) {
+                bubbleStashController.updateTaskbarTouchRegion()
+            }
+        } else {
+            cancelFlyout()
+        }
+    }
+
+    private fun updateAnimationWhileAnimatingOut(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        // unsuppress the current bubble because we are about to hide its flyout
+        animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+        // the hide animation already started so it can't be canceled, just post it again
+        val hideAnimation = animatingBubble.hideAnimation
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (bubbleBarFlyoutController.hasFlyout()) {
+            // the flyout is collapsing. update it with the new flyout
+            if (flyout != null) {
+                moveToState(AnimatingBubble.State.ANIMATING_IN)
+                bubbleView.suppressDotForBubbleUpdate(true)
+                bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) {
+                    moveToState(AnimatingBubble.State.IN)
+                    bubbleStashController.updateTaskbarTouchRegion()
+                }
+            } else {
+                cancelFlyout()
+                moveToState(AnimatingBubble.State.IN)
+            }
+        } else {
+            // the flyout is already gone. if we're animating the handle cancel it. the
+            // animation itself can handle morphing back into the bubble bar and restarting
+            // and show the flyout.
+            val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator()
+            if (handleAnimator != null && handleAnimator.isRunning()) {
+                interceptedHandleAnimator = true
+                handleAnimator.cancel()
+            }
+
+            // if we're not animating the handle, then the hide animation simply hides the
+            // flyout, but if the flyout is gone then the animation has ended.
+        }
+    }
+
     private fun cancelHideAnimation() {
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index fdbbbb0..1452cf6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -39,11 +39,14 @@
     }
 
     private var flyout: BubbleBarFlyoutView? = null
+    private var animator: ValueAnimator? = null
     private val horizontalMargin =
         container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
 
     private enum class AnimationType {
-        COLLAPSE,
+        /** Morphs the flyout between a dot and a rounded rectangle. */
+        MORPH,
+        /** Fades the flyout in or out. */
         FADE,
     }
 
@@ -72,55 +75,105 @@
         lp.marginEnd = horizontalMargin
         container.addView(flyout, lp)
 
-        val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
-        animator.addUpdateListener { _ ->
-            flyout.updateExpansionProgress(animator.animatedValue as Float)
-        }
-        animator.addListener(
-            onStart = {
-                val flyoutTop = flyout.top + flyout.translationY
-                // If the top position of the flyout is negative, then it's bleeding over the
-                // top boundary of its parent view
-                if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
-            },
-            onEnd = {
-                onEnd()
-                flyout.setOnClickListener { callbacks.flyoutClicked() }
-            },
-        )
-        flyout.showFromCollapsed(message) { animator.start() }
         this.flyout = flyout
+        flyout.showFromCollapsed(message) { showFlyout(AnimationType.MORPH, onEnd) }
     }
 
-    fun cancelFlyout(endAction: () -> Unit) {
-        hideFlyout(AnimationType.FADE, endAction)
-    }
-
-    fun collapseFlyout(endAction: () -> Unit) {
-        hideFlyout(AnimationType.COLLAPSE, endAction)
-    }
-
-    private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
-        // TODO: b/277815200 - stop the current animation if it's running
+    private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
         val flyout = this.flyout ?: return
-        val animator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATION_DURATION_MS)
+        val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
+        val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong()
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
+        this.animator = animator
         when (animationType) {
             AnimationType.FADE ->
                 animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
-            AnimationType.COLLAPSE ->
+            AnimationType.MORPH ->
                 animator.addUpdateListener { _ ->
                     flyout.updateExpansionProgress(animator.animatedValue as Float)
                 }
         }
         animator.addListener(
-            onStart = { flyout.setOnClickListener(null) },
+            onStart = { extendTopBoundary() },
             onEnd = {
-                container.removeView(flyout)
-                this@BubbleBarFlyoutController.flyout = null
-                callbacks.resetTopBoundary()
                 endAction()
+                flyout.setOnClickListener { callbacks.flyoutClicked() }
             },
         )
         animator.start()
     }
+
+    fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+        val flyout = flyout ?: return
+        hideFlyout(AnimationType.FADE) {
+            flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
+        }
+    }
+
+    fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) {
+        val flyout = flyout ?: return
+        flyout.updateData(message) { extendTopBoundary() }
+    }
+
+    fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+        val flyout = flyout ?: return
+        animator?.pause()
+        animator?.removeAllListeners()
+        flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) }
+    }
+
+    private fun extendTopBoundary() {
+        val flyout = flyout ?: return
+        val flyoutTop = flyout.top + flyout.translationY
+        // If the top position of the flyout is negative, then it's bleeding over the
+        // top boundary of its parent view
+        if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
+    }
+
+    fun cancelFlyout(endAction: () -> Unit) {
+        hideFlyout(AnimationType.FADE) {
+            cleanupFlyoutView()
+            endAction()
+        }
+    }
+
+    fun collapseFlyout(endAction: () -> Unit) {
+        hideFlyout(AnimationType.MORPH) {
+            cleanupFlyoutView()
+            endAction()
+        }
+    }
+
+    private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
+        val flyout = this.flyout ?: return
+        val startValue = getCurrentAnimatedValueIfRunning() ?: 1f
+        val duration = (ANIMATION_DURATION_MS * startValue).toLong()
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
+        this.animator = animator
+        when (animationType) {
+            AnimationType.FADE ->
+                animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+            AnimationType.MORPH ->
+                animator.addUpdateListener { _ ->
+                    flyout.updateExpansionProgress(animator.animatedValue as Float)
+                }
+        }
+        animator.addListener(onStart = { flyout.setOnClickListener(null) }, onEnd = { endAction() })
+        animator.start()
+    }
+
+    private fun cleanupFlyoutView() {
+        container.removeView(flyout)
+        this@BubbleBarFlyoutController.flyout = null
+        callbacks.resetTopBoundary()
+    }
+
+    fun hasFlyout() = flyout != null
+
+    private fun getCurrentAnimatedValueIfRunning(): Float? {
+        val animator = animator ?: return null
+        return if (animator.isRunning) animator.animatedValue as Float else null
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index bb8a392..af8aaf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -198,6 +198,8 @@
             } else {
                 -positioner.distanceToCollapsedPosition.x
             }
+        // TODO: b/277815200 - before collapsing, recalculate translationToCollapsedPosition because
+        // the collapsed position may have changed
         val tyToCollapsedPosition =
             positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
         translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
@@ -217,6 +219,12 @@
         scheduler.runAfterLayout(expandAnimation)
     }
 
+    /** Updates the content of the flyout and schedules [afterLayout] to run after a layout pass. */
+    fun updateData(flyoutMessage: BubbleBarFlyoutMessage, afterLayout: () -> Unit) {
+        setData(flyoutMessage)
+        scheduler.runAfterLayout(afterLayout)
+    }
+
     private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
         if (flyoutMessage.icon != null) {
             icon.visibility = VISIBLE
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4ad65e1..fe68ebc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -139,6 +139,7 @@
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory;
@@ -419,8 +420,10 @@
             mDepthController.setActivityStarted(isStarted());
         }
 
-        if ((changeBits & ACTIVITY_STATE_RESUMED) != 0 && mTaskbarUIController != null) {
-            mTaskbarUIController.onLauncherPausedOrResumed(isPaused());
+        if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) {
+            if (!FeatureFlags.enableHomeTransitionListener() && mTaskbarUIController != null) {
+                mTaskbarUIController.onLauncherVisibilityChanged(hasBeenResumed());
+            }
         }
 
         super.onActivityFlagsChanged(changeBits);
@@ -1087,10 +1090,12 @@
         );
     }
 
-    public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
-        mTaskbarUIController = taskbarUIController;
+    @Override
+    public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+        mTaskbarUIController = (LauncherTaskbarUIController) taskbarUIController;
     }
 
+    @Override
     public @Nullable LauncherTaskbarUIController getTaskbarUIController() {
         return mTaskbarUIController;
     }
@@ -1397,6 +1402,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index e87ac2f..ca388c6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.uioverrides.states;
 
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 
@@ -91,7 +92,7 @@
 
     @Override
     public boolean detachDesktopCarousel() {
-        return true;
+        return enableDesktopWindowingCarouselDetach();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 015a449..cff352c 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -309,7 +309,9 @@
      * changes in the WM hierarchy (ie. starting recents transition when you are already over home).
      */
     public boolean useSyntheticRecentsTransition() {
-        return mRunningTask.isHomeTask() && Flags.enableFallbackOverviewInWindow();
+        return mRunningTask.isHomeTask()
+                && (Flags.enableFallbackOverviewInWindow()
+                        || Flags.enableLauncherOverviewInWindow());
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 66112c1..1f6c671 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -155,7 +155,7 @@
             mContainerInterface.onAssistantVisibilityChanged(0.f);
         }
 
-        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+        if (SEPARATE_RECENTS_ACTIVITY.get() || Flags.enableLauncherOverviewInWindow()) {
             mIsDefaultHome = false;
             if (defaultHome == null) {
                 defaultHome = mMyHomeIntent.getComponent();
@@ -179,7 +179,7 @@
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
 
-            if (Flags.enableFallbackOverviewInWindow()) {
+            if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
                 mContainerInterface = FallbackWindowInterface.getInstance();
             } else {
                 mContainerInterface = FallbackActivityInterface.INSTANCE;
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index c3b9736..2828a84 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -3,7 +3,6 @@
 import static com.android.launcher3.taskbar.TaskbarThresholdUtils.getFromNavThreshold;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.app.Activity;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Bundle;
@@ -208,8 +207,9 @@
         RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
         OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
         try {
-            return observer.getContainerInterface()
-                    .getCreatedContainer().getRootView().getRootWindowInsets();
+            RecentsViewContainer container = observer.getContainerInterface().getCreatedContainer();
+
+            return container == null ? null : container.getRootView().getRootWindowInsets();
         } finally {
             observer.onDestroy();
             rads.destroy();
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 05bef35..a5cc32a 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -119,6 +119,11 @@
                     topTaskTracker.onTaskMovedToFront(taskInfo);
                 });
             }
+
+            @Override
+            public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+                mMainThreadExecutor.execute(() -> topTaskTracker.onTaskChanged(taskInfo));
+            }
         });
         // We may receive onRunningTaskAppeared events later for tasks which have already been
         // included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index b19f651..6075294 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -70,6 +70,7 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.FallbackTaskbarUIController;
 import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ContextTracker;
 import com.android.launcher3.util.RunnableList;
@@ -115,7 +116,7 @@
     private FallbackRecentsView mFallbackRecentsView;
     private OverviewActionsView<?> mActionsView;
     private TISBindHelper mTISBindHelper;
-    private @Nullable FallbackTaskbarUIController mTaskbarUIController;
+    private @Nullable FallbackTaskbarUIController<RecentsActivity> mTaskbarUIController;
 
     private StateManager<RecentsState, RecentsActivity> mStateManager;
 
@@ -174,11 +175,14 @@
         mTISBindHelper.runOnBindToTouchInteractionService(r);
     }
 
-    public void setTaskbarUIController(FallbackTaskbarUIController taskbarUIController) {
-        mTaskbarUIController = taskbarUIController;
+    @Override
+    public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+        mTaskbarUIController = (FallbackTaskbarUIController<RecentsActivity>) taskbarUIController;
     }
 
-    public FallbackTaskbarUIController getTaskbarUIController() {
+    @Nullable
+    @Override
+    public FallbackTaskbarUIController<RecentsActivity> getTaskbarUIController() {
         return mTaskbarUIController;
     }
 
@@ -515,6 +519,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 7d5bd37..8fc1a78 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -109,7 +109,8 @@
         boolean isOpeningHome = Arrays.stream(appTargets).filter(app -> app.mode == MODE_OPENING
                         && app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME)
                 .count() > 0;
-        if (appCount == 0 && (!Flags.enableFallbackOverviewInWindow() || isOpeningHome)) {
+        if (appCount == 0 && (!(Flags.enableFallbackOverviewInWindow()
+                || Flags.enableLauncherOverviewInWindow()) || isOpeningHome)) {
             ActiveGestureProtoLogProxy.logOnRecentsAnimationStartCancelled();
             // Edge case, if there are no closing app targets, then Launcher has nothing to handle
             notifyAnimationCanceled();
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 8adc11a..06b2972 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -94,7 +94,7 @@
         RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
         for (int i = 0; i < numHandles; i++) {
             TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
-            tvs.setIsDesktopTask(forDesktop);
+            tvs.setIsDesktopTask(forDesktop , i);
             TransformParams transformParams = new TransformParams();
             handles[i] = new RemoteTargetHandle(tvs, transformParams);
         }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 80c07196..79abc0f 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -180,6 +180,7 @@
                 }
             }
         };
+        runOnDestroy(() -> mOrientationListener.disable());
         mNeedsInit = false;
     }
 
@@ -212,6 +213,7 @@
             r.run();
         }
         mNeedsInit = true;
+        mOnDestroyActions.clear();
     }
 
     public boolean isTaskListFrozen() {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index c1d7ffa..cd39c09 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -54,15 +54,16 @@
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.util.ScreenshotRequest;
 import com.android.internal.view.AppearanceRegion;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
@@ -109,14 +110,17 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /**
  * Holds the reference to SystemUI.
  */
-public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
+@LauncherAppSingleton
+public class SystemUiProxy implements ISystemUiProxy, NavHandle {
     private static final String TAG = "SystemUiProxy";
 
-    public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
-            new MainThreadInitializedObject<>(SystemUiProxy::new);
+    public static final DaggerSingletonObject<SystemUiProxy> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSystemUiProxy);
 
     private static final int MSG_SET_SHELF_HEIGHT = 1;
     private static final int MSG_SET_LAUNCHER_KEEP_CLEAR_AREA_HEIGHT = 2;
@@ -188,8 +192,8 @@
     @Nullable
     private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
 
-    @VisibleForTesting
-    protected SystemUiProxy(Context context) {
+    @Inject
+    public SystemUiProxy(@ApplicationContext Context context) {
         mContext = context;
         mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
         final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -206,9 +210,6 @@
     }
 
     @Override
-    public void close() { }
-
-    @Override
     public void onBackPressed() {
         if (mSystemUiProxy != null) {
             try {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 56c978a..0b6794c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -295,7 +295,8 @@
 
         // TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
         if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
-                && Flags.enableFallbackOverviewInWindow()){
+                && (Flags.enableFallbackOverviewInWindow()
+                        || Flags.enableLauncherOverviewInWindow())) {
             mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent, options,
                     mCallbacks, gestureState.useSyntheticRecentsTransition());
             mRecentsWindowsManager.startRecentsWindow(mCallbacks);
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 23a1ec7..71b6573 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -135,6 +135,15 @@
         }
     }
 
+    public void onTaskChanged(RunningTaskInfo taskInfo) {
+        for (int i = 0; i < mOrderedTaskList.size(); i++) {
+            if (mOrderedTaskList.get(i).taskId == taskInfo.taskId) {
+                mOrderedTaskList.set(i, taskInfo);
+                break;
+            }
+        }
+    }
+
     @Override
     public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
         // If a task is not visible anymore or has been moved to undefined, stop tracking it.
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 1481ef2..e8f38be 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -675,7 +675,7 @@
         mDesktopVisibilityController = new DesktopVisibilityController(this);
         mTaskbarManager = new TaskbarManager(
                 this, mAllAppsActionManager, mNavCallbacks, mDesktopVisibilityController);
-        if(Flags.enableFallbackOverviewInWindow()) {
+        if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
             mRecentsWindowManager = new RecentsWindowManager(this);
         }
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
@@ -776,10 +776,13 @@
         mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame);
         RecentsViewContainer newOverviewContainer =
                 mOverviewComponentObserver.getContainerInterface().getCreatedContainer();
-        if (newOverviewContainer != null
-                && newOverviewContainer instanceof StatefulActivity activity) {
-            //TODO(b/368030750) refactor taskbarManager to accept RecentsViewContainer
-            mTaskbarManager.setActivity(activity);
+        if (newOverviewContainer != null) {
+            if (newOverviewContainer instanceof StatefulActivity activity) {
+                // This will also call setRecentsViewContainer() internally.
+                mTaskbarManager.setActivity(activity);
+            } else {
+                mTaskbarManager.setRecentsViewContainer(newOverviewContainer);
+            }
         }
         mTISBinder.onOverviewTargetChange();
     }
@@ -927,9 +930,6 @@
             BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null;
             boolean isOnBubbles = bubbleControllers != null
                     && BubbleBarInputConsumer.isEventOnBubbles(tac, event);
-            if (isInSwipeUpTouchRegion && tac != null) {
-                tac.closeKeyboardQuickSwitchView();
-            }
             if (mDeviceState.isButtonNavMode()
                     && mDeviceState.supportsAssistantGestureInButtonNav()) {
                 reasonString.append("in three button mode which supports Assistant gesture");
@@ -1410,8 +1410,10 @@
     }
 
     public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
+        boolean recentsInWindow =
+                Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
         return mOverviewComponentObserver.isHomeAndOverviewSame()
-                ? mLauncherSwipeHandlerFactory : (Flags.enableFallbackOverviewInWindow()
+                ? mLauncherSwipeHandlerFactory : (recentsInWindow
                 ? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
     }
 
diff --git a/quickstep/src/com/android/quickstep/ViewUtils.java b/quickstep/src/com/android/quickstep/ViewUtils.java
index 3b58dfc..cf6b04e 100644
--- a/quickstep/src/com/android/quickstep/ViewUtils.java
+++ b/quickstep/src/com/android/quickstep/ViewUtils.java
@@ -23,6 +23,7 @@
 
 import com.android.launcher3.Utilities;
 
+import java.util.ArrayList;
 import java.util.function.BooleanSupplier;
 
 /**
@@ -129,4 +130,18 @@
             }
         }
     }
+
+    /**
+     * Adds the view to the list of accessible children.
+     *
+     * @param view The view to add.
+     * @param outChildren The list of accessible children.
+     */
+    public static void addAccessibleChildToList(View view, ArrayList<View> outChildren) {
+        if (view.includeForAccessibility()) {
+            outChildren.add(view);
+        } else {
+            view.addChildrenForAccessibility(outChildren);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index 977c036..b2670e8 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -19,6 +19,7 @@
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.model.WellbeingModel;
+import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.AsyncClockEventDelegate;
 
 /**
@@ -34,4 +35,6 @@
     WellbeingModel getWellbeingModel();
 
     AsyncClockEventDelegate getAsyncClockEventDelegate();
+
+    SystemUiProxy getSystemUiProxy();
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 5a4c769..daad6b7 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -78,7 +78,7 @@
     }
 
     private static BaseContainerInterface<RecentsState, ?> getContainerInterface() {
-        return Flags.enableFallbackOverviewInWindow()
+        return (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow())
                 ? FallbackWindowInterface.getInstance()
                 : FallbackActivityInterface.INSTANCE;
     }
@@ -294,7 +294,8 @@
         }
 
         // disabling this so app icons aren't drawn on top of recent tasks.
-        if (isOverlayEnabled && !Flags.enableFallbackOverviewInWindow()) {
+        if (isOverlayEnabled && !(Flags.enableFallbackOverviewInWindow()
+                || Flags.enableLauncherOverviewInWindow())) {
             runActionOnRemoteHandles(remoteTargetHandle ->
                     remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index 082b96c..34783c7 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.fallback;
 
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
 import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
@@ -152,7 +153,7 @@
 
     @Override
     public boolean detachDesktopCarousel() {
-        return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL);
+        return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL) && enableDesktopWindowingCarouselDetach();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 3017df2..78224ae 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -18,6 +18,7 @@
 
 import android.animation.AnimatorSet
 import android.app.ActivityOptions
+import android.content.ComponentName
 import android.content.Context
 import android.content.LocusId
 import android.os.Bundle
@@ -39,6 +40,7 @@
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory
 import com.android.launcher3.statemanager.StatefulContainer
+import com.android.launcher3.taskbar.TaskbarUIController
 import com.android.launcher3.util.ContextTracker
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.RunnableList
@@ -116,6 +118,7 @@
 
     private var callbacks: RecentsAnimationCallbacks? = null
 
+    private var taskbarUIController: TaskbarUIController? = null
     private var tisBindHelper: TISBindHelper = TISBindHelper(this) {}
 
     // Callback array that corresponds to events defined in @ActivityEvent
@@ -276,6 +279,10 @@
         }
     }
 
+    override fun getComponentName(): ComponentName {
+        return ComponentName(this, RecentsWindowManager::class.java)
+    }
+
     override fun canStartHomeSafely(): Boolean {
         val overviewCommandHelper = tisBindHelper.overviewCommandHelper
         return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
@@ -285,6 +292,18 @@
         return tisBindHelper.desktopVisibilityController
     }
 
+    override fun setTaskbarUIController(taskbarUIController: TaskbarUIController?) {
+        this.taskbarUIController = taskbarUIController
+    }
+
+    override fun getTaskbarUIController(): TaskbarUIController? {
+        return taskbarUIController
+    }
+
+    override fun getTISBindHelper(): TISBindHelper {
+        return tisBindHelper
+    }
+
     fun registerInitListener(onInitListener: Predicate<Boolean>) {
         this.onInitListener = onInitListener
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index e19b338..c4198db 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -157,6 +157,7 @@
         mStartDisplacement = continuingPreviousGesture ? 0 : -mTouchSlop;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+
     }
 
     @Override
@@ -426,8 +427,9 @@
             notifyGestureStarted(true /*isLikelyToStartNewTask*/);
         } else {
             // todo differentiate intent based on if we are on home or in app for overview in window
-            Intent intent = new Intent(Flags.enableFallbackOverviewInWindow()
-                ? mInteractionHandler.getHomeIntent()
+            boolean useHomeIntentForWindow = Flags.enableFallbackOverviewInWindow()
+                    || Flags.enableLauncherOverviewInWindow();
+            Intent intent = new Intent(useHomeIntentForWindow ? mInteractionHandler.getHomeIntent()
                 : mInteractionHandler.getLaunchIntent());
             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 8762e86..623bc53 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -39,6 +39,7 @@
     // The percentage of the previous speed that determines whether this is a rapid deceleration.
     // The bigger this number, the easier it is to trigger the first pause.
     private static final float RAPID_DECELERATION_FACTOR = 0.6f;
+    private static final float RAPID_DECELERATION_FACTOR_TRACKPAD = 0.85f;
 
     /** If no motion is added for this amount of time, assume the motion has paused. */
     private static final long FORCE_PAUSE_TIMEOUT = 300;
@@ -57,6 +58,7 @@
     private final float mSpeedVerySlow;
     private final float mSpeedSlow;
     private final float mSpeedSomewhatFast;
+    private final float mSpeedTrackpadSomewhatFast;
     private final float mSpeedFast;
     private final Alarm mForcePauseTimeout;
     private final boolean mMakePauseHarderToTrigger;
@@ -95,6 +97,8 @@
         mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
         mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
         mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
+        mSpeedTrackpadSomewhatFast = res.getDimension(
+                R.dimen.motion_pause_detector_speed_trackpad_somewhat_fast);
         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> {
@@ -183,7 +187,9 @@
                     // takes too long, so also check for a rapid deceleration.
                     boolean isRapidDeceleration =
                             speed < previousSpeed * getRapidDecelerationFactor();
-                    isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
+                    boolean notSuperFast = speed < mSpeedSomewhatFast
+                            || (mIsTrackpadGesture && speed < mSpeedTrackpadSomewhatFast);
+                    isPaused = isRapidDeceleration && notSuperFast;
                     isPausedReason = new ActiveGestureLog.CompoundString(
                             "Didn't have back to back slow speeds, checking for rapid "
                                     + " deceleration on first pause only");
@@ -265,7 +271,8 @@
     private float getRapidDecelerationFactor() {
         return mIsTrackpadGesture ? Float.parseFloat(
                 Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor",
-                        String.valueOf(RAPID_DECELERATION_FACTOR))) : RAPID_DECELERATION_FACTOR;
+                        String.valueOf(RAPID_DECELERATION_FACTOR_TRACKPAD)))
+                : RAPID_DECELERATION_FACTOR;
     }
 
     public interface OnMotionPauseListener {
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index f5be103..a4b8fec 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -120,6 +120,7 @@
     private boolean mScaleToCarouselTaskSize = false;
     private int mTaskRectTranslationX;
     private int mTaskRectTranslationY;
+    private int mDesktopTaskIndex = 0;
 
     public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
         mContext = context;
@@ -290,8 +291,9 @@
     /**
      * Sets whether this task is part of desktop tasks in overview.
      */
-    public void setIsDesktopTask(boolean desktop) {
+    public void setIsDesktopTask(boolean desktop, int index) {
         mIsDesktopTask = desktop;
+        mDesktopTaskIndex = index;
     }
 
     /**
@@ -545,9 +547,9 @@
             // In shell transitions, the animation leashes are reparented to an animation container
             // so we can bump layers as needed.
             builder.setLayer(mDrawsBelowRecents
-                    ? Integer.MIN_VALUE + app.prefixOrderIndex
                     // 1000 is an arbitrary number to give room for multiple layers.
-                    : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex);
+                    ? Integer.MIN_VALUE + 1000 + app.prefixOrderIndex - mDesktopTaskIndex
+                    : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex - mDesktopTaskIndex);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 6b145bd..15b0a6b 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -39,6 +39,7 @@
 import com.android.launcher3.util.rects.set
 import com.android.quickstep.BaseContainerInterface
 import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.ViewUtils
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.systemui.shared.recents.model.Task
@@ -313,6 +314,11 @@
 
     override fun getThumbnailFullscreenParams() = snapshotDrawParams
 
+    override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
+        super.addChildrenForAccessibility(outChildren)
+        ViewUtils.addAccessibleChildToList(backgroundView, outChildren)
+    }
+
     companion object {
         private const val TAG = "DesktopTaskView"
         private const val DEBUG = false
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index b38d0d7..2d0f15e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -5058,7 +5058,8 @@
         mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
         mSplitSelectStateController
                 .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal
-                        && mSplitHiddenTaskView != null);
+                        && mSplitHiddenTaskView != null
+                        && !(mSplitHiddenTaskView instanceof DesktopTaskView));
 
         // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
         mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index d8036aa..b04753b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep.views;
 
-import android.app.Activity;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.LocusId;
@@ -27,13 +25,16 @@
 import android.view.View;
 import android.view.Window;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.util.TISBindHelper;
 
 /**
  * Interface to be implemented by the parent view of RecentsView
@@ -212,4 +213,10 @@
 
     @Nullable
     DesktopVisibilityController getDesktopVisibilityController();
+
+    void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController);
+
+    @Nullable TaskbarUIController getTaskbarUIController();
+
+    @NonNull TISBindHelper getTISBindHelper();
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index dc5ffee..959516f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -29,6 +29,7 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskUtils
+import com.android.quickstep.ViewUtils.addAccessibleChildToList
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.di.getScope
@@ -182,12 +183,4 @@
         showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
         digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
     }
-
-    private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
-        if (view.includeForAccessibility()) {
-            outChildren.add(view)
-        } else {
-            view.addChildrenForAccessibility(outChildren)
-        }
-    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
index 59900b1..cfa12e2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
@@ -33,36 +33,27 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
-import org.junit.runners.model.Statement
 
 @RunWith(LauncherMultivalentJUnit::class)
 @EmulatedDevices(["pixelTablet2023"])
 class TaskbarAutohideSuspendControllerTest {
 
-    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
-    @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
-    @get:Rule(order = 2)
-    val systemUiProxyRule = TestRule { base, _ ->
-        object : Statement() {
-            override fun evaluate() {
-                getInstrumentation().runOnMainSync {
-                    context.putObject(
-                        SystemUiProxy.INSTANCE,
-                        object : SystemUiProxy(context) {
-                            override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
-                                latestSuspendNotification = suspend
-                            }
-                        },
-                    )
+    @get:Rule(order = 0)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
+                        super.notifyTaskbarAutohideSuspend(suspend)
+                        latestSuspendNotification = suspend
+                    }
                 }
-                base.evaluate()
-            }
+            )
         }
-    }
-    @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
-    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
 
     @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
     @InjectController lateinit var stashController: TaskbarStashController
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
index 12e84b8..3912051 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -42,7 +42,18 @@
 @RunWith(LauncherMultivalentJUnit::class)
 @EmulatedDevices(["pixelTablet2023"])
 class TaskbarScrimViewControllerTest {
-    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 0)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun onBackPressed() {
+                        super.onBackPressed()
+                        backPressed = true
+                    }
+                }
+            )
+        }
     @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context)
     @get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this)
     @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
@@ -53,6 +64,8 @@
     private val animationDuration =
         context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
 
+    private var backPressed = false
+
     @Test
     @TaskbarMode(PINNED)
     fun testOnTaskbarVisibleChanged_onlyTaskbarVisible_noScrim() {
@@ -130,16 +143,6 @@
     @Test
     @TaskbarMode(PINNED)
     fun testOnClick_scrimShown_performsSystemBack() {
-        var backPressed = false
-        context.putObject(
-            SystemUiProxy.INSTANCE,
-            object : SystemUiProxy(context) {
-                override fun onBackPressed() {
-                    backPressed = true
-                }
-            },
-        )
-
         getInstrumentation().runOnMainSync {
             scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
             scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index b0d01d3..48f3fc2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -25,6 +25,7 @@
 import android.view.View
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
+import android.widget.TextView
 import androidx.core.animation.AnimatorTestRule
 import androidx.core.graphics.drawable.toBitmap
 import androidx.dynamicanimation.animation.DynamicAnimation
@@ -834,23 +835,27 @@
 
         // advance the animation handler by the duration of the initial lift
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animatorTestRule.advanceTimeBy(250)
+            animatorTestRule.advanceTimeBy(100)
         }
 
-        // the lift animation is complete; the spring back animation should start now
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
-        barAnimator.assertIsRunning()
-        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
+        // send the expand signal in the middle of the lift animation
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the lift animation complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(150)
+        }
 
         // verify there is a pending hide animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         assertThat(animator.isAnimating).isTrue()
 
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.expandedWhileAnimating()
-        }
-
-        // let the animation finish
+        // the lift animation is complete; the spring back animation should start now. wait for it
+        // to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         // verify that the hide animation was canceled
@@ -923,6 +928,344 @@
         assertThat(notifiedExpanded).isTrue()
     }
 
+    @Test
+    fun interruptAnimation_whileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait until the first frame
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        handleAnimator.assertIsRunning()
+        assertThat(animator.isAnimating).isTrue()
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // verify the hide animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        // verify the hide animation was rescheduled
+        assertThat(animatorScheduler.canceledBlock).isNotNull()
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        waitForFlyoutToFadeOutAndBackIn()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        // interrupt the animation while the flyout is collapsing
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(100)
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+
+            // the flyout should now reverse and expand
+            animatorTestRule.advanceTimeBy(100)
+        }
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        // verify the hide animation was rescheduled and run it
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileAnimatingOut_barToHandle() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // interrupt the animation while the bar is animating to the handle
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) {
+            bubbleBarView.alpha < 0.5
+        }
+
+        // we're about to interrupt the animation which will cancel the current animation and start
+        // a new one. pause the scheduler to delay starting the new animation. this allows us to run
+        // the test deterministically
+        animatorScheduler.pauseScheduler = true
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there's a new job scheduled and start it. this is starting the animation from the
+        // handle back to the bar
+        assertThat(animatorScheduler.pausedBlock).isNotNull()
+        animatorScheduler.pauseScheduler = false
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify the hide animation was rescheduled and run it
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -1019,18 +1362,25 @@
 
     private fun waitForFlyoutToShow() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animatorTestRule.advanceTimeBy(300)
+            animatorTestRule.advanceTimeBy(250)
         }
         assertThat(flyoutView).isNotNull()
     }
 
     private fun waitForFlyoutToHide() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animatorTestRule.advanceTimeBy(300)
+            animatorTestRule.advanceTimeBy(250)
         }
         assertThat(flyoutView).isNull()
     }
 
+    private fun waitForFlyoutToFadeOutAndBackIn() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(500)
+        }
+        assertThat(flyoutView).isNotNull()
+    }
+
     private fun <T> PhysicsAnimator<T>.assertIsRunning() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             assertThat(isRunning()).isTrue()
@@ -1045,20 +1395,30 @@
 
     private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
 
+        var pauseScheduler = false
+        var pausedBlock: Runnable? = null
+            private set
+
         var delayedBlock: Runnable? = null
             private set
 
+        var canceledBlock: Runnable? = null
+            private set
+
         override fun post(block: Runnable) {
+            if (pauseScheduler) {
+                pausedBlock = block
+                return
+            }
             block.run()
         }
 
         override fun postDelayed(delayMillis: Long, block: Runnable) {
-            check(delayedBlock == null) { "there is already a pending block waiting to run" }
             delayedBlock = block
         }
 
         override fun cancel(block: Runnable) {
-            check(delayedBlock == block) { "the pending block does not match the canceled block" }
+            canceledBlock = delayedBlock
             delayedBlock = null
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index 0eea741..fef82c1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -115,11 +115,13 @@
     fun hideFlyout_removedFromContainer() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            assertThat(flyoutController.hasFlyout()).isTrue()
             assertThat(flyoutContainer.childCount).isEqualTo(1)
             flyoutController.collapseFlyout {}
             animatorTestRule.advanceTimeBy(300)
         }
         assertThat(flyoutContainer.childCount).isEqualTo(0)
+        assertThat(flyoutController.hasFlyout()).isFalse()
     }
 
     @Test
@@ -189,6 +191,95 @@
         assertThat(flyoutCallbacks.flyoutClicked).isTrue()
     }
 
+    @Test
+    fun updateFlyoutWhileExpanding() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            assertThat(flyoutController.hasFlyout()).isTrue()
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("message")
+            // advance the animation about halfway
+            animatorTestRule.advanceTimeBy(100)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            // set negative translation to verify that the top boundary extends as a result of
+            // updating while expanding
+            flyout.translationY = -50f
+            flyoutController.updateFlyoutWhileExpanding(newFlyoutMessage)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun updateFlyoutFullyExpanded() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            animatorTestRule.advanceTimeBy(300)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            // set negative translation to verify that the top boundary extends as a result of
+            // updating while fully expanded
+            flyout.translationY = -50f
+            flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {}
+
+            // advance the timer so that the fade out animation plays
+            animatorTestRule.advanceTimeBy(250)
+            assertThat(flyout.alpha).isEqualTo(0)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+
+            // advance the timer so that the fade in animation plays
+            animatorTestRule.advanceTimeBy(250)
+            assertThat(flyout.alpha).isEqualTo(1)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun updateFlyoutWhileCollapsing() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+            animatorTestRule.advanceTimeBy(300)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            var flyoutCollapsed = false
+            flyoutController.collapseFlyout { flyoutCollapsed = true }
+            // advance the fake timer so that the collapse animation runs for 125ms
+            animatorTestRule.advanceTimeBy(125)
+
+            // update the flyout in the middle of collapsing, which should start expanding it.
+            var flyoutReversed = false
+            flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true }
+
+            // the collapse animation ran for 125ms when it was updated, so reversing it should only
+            // run for the same amount of time
+            animatorTestRule.advanceTimeBy(125)
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyout.alpha).isEqualTo(1)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+            // verify that we never called the end action on the collapse animation
+            assertThat(flyoutCollapsed).isFalse()
+            // verify that we called the end action on the reverse animation
+            assertThat(flyoutReversed).isTrue()
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+    }
+
     class FakeFlyoutCallbacks : FlyoutCallbacks {
 
         var topBoundaryExtendedSpace = 0
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index b0d706f..096f879 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -19,12 +19,10 @@
 import android.app.Instrumentation
 import android.app.PendingIntent
 import android.content.IIntentSender
-import android.content.Intent
-import android.provider.Settings
 import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE
 import android.provider.Settings.Secure.USER_SETUP_COMPLETE
+import android.provider.Settings.Secure.getUriFor
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ServiceTestRule
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -35,16 +33,12 @@
 import com.android.launcher3.taskbar.bubbles.BubbleControllers
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
-import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
 import com.android.launcher3.util.TestUtil
 import com.android.quickstep.AllAppsActionManager
-import com.android.quickstep.TouchInteractionService
-import com.android.quickstep.TouchInteractionService.TISBinder
 import java.lang.reflect.Field
 import java.lang.reflect.ParameterizedType
 import java.util.Optional
 import org.junit.Assume.assumeTrue
-import org.junit.rules.RuleChain
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -81,11 +75,6 @@
 ) : TestRule {
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
-    private val serviceTestRule = ServiceTestRule()
-
-    private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE)
-    private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE)
-    private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule)
 
     private lateinit var taskbarManager: TaskbarManager
 
@@ -96,10 +85,6 @@
         }
 
     override fun apply(base: Statement, description: Description): Statement {
-        return settingRules.apply(createStatement(base, description), description)
-    }
-
-    private fun createStatement(base: Statement, description: Description): Statement {
         return object : Statement() {
             override fun evaluate() {
 
@@ -111,34 +96,10 @@
                 }
 
                 // Process secure setting annotations.
-                instrumentation.runOnMainSync {
-                    userSetupCompleteRule.putInt(
-                        if (description.getAnnotation(UserSetupMode::class.java) != null) {
-                            0
-                        } else {
-                            1
-                        }
-                    )
-                    kidsModeRule.putInt(
-                        if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
-                    )
-                }
-
-                // Check for existing Taskbar instance from Launcher process.
-                val launcherTaskbarManager: TaskbarManager? =
-                    if (!isRunningInRobolectric) {
-                        try {
-                            val tisBinder =
-                                serviceTestRule.bindService(
-                                    Intent(context, TouchInteractionService::class.java)
-                                ) as? TISBinder
-                            tisBinder?.taskbarManager
-                        } catch (_: Exception) {
-                            null
-                        }
-                    } else {
-                        null
-                    }
+                context.settingsCacheSandbox[getUriFor(USER_SETUP_COMPLETE)] =
+                    if (description.getAnnotation(UserSetupMode::class.java) != null) 0 else 1
+                context.settingsCacheSandbox[getUriFor(NAV_BAR_KIDS_MODE)] =
+                    if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
 
                 taskbarManager =
                     TestUtil.getOnUiThread {
@@ -161,20 +122,12 @@
                 try {
                     TaskbarViewController.enableModelLoadingForTests(false)
 
-                    // Replace Launcher Taskbar window with test instance.
-                    instrumentation.runOnMainSync {
-                        launcherTaskbarManager?.setSuspended(true)
-                        taskbarManager.onUserUnlocked() // Required to complete initialization.
-                    }
+                    // Required to complete initialization.
+                    instrumentation.runOnMainSync { taskbarManager.onUserUnlocked() }
 
                     base.evaluate()
                 } finally {
-                    // Revert Taskbar window.
-                    instrumentation.runOnMainSync {
-                        taskbarManager.destroy()
-                        launcherTaskbarManager?.setSuspended(false)
-                    }
-
+                    instrumentation.runOnMainSync { taskbarManager.destroy() }
                     TaskbarViewController.enableModelLoadingForTests(true)
                 }
             }
@@ -238,25 +191,4 @@
     @Retention(AnnotationRetention.RUNTIME)
     @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
     annotation class NavBarKidsMode
-
-    /** Rule for Taskbar integer-based secure settings. */
-    private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule {
-
-        override fun apply(base: Statement, description: Description): Statement {
-            return object : Statement() {
-                override fun evaluate() {
-                    val originalValue =
-                        Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0)
-                    try {
-                        base.evaluate()
-                    } finally {
-                        instrumentation.runOnMainSync { putInt(originalValue) }
-                    }
-                }
-            }
-        }
-
-        /** Puts [value] into secure settings under [settingName]. */
-        fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value)
-    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 2d3bfd6..8c51216 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -24,11 +24,24 @@
 import androidx.test.core.app.ApplicationProvider
 import com.android.launcher3.FakeLauncherPrefs
 import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
 import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCacheSandbox
+import com.android.quickstep.SystemUiProxy
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.rules.ExternalResource
 import org.junit.rules.RuleChain
 import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/** Include additional bindings when building a [TaskbarSandboxComponent]. */
+typealias TaskbarComponentBinder =
+    TaskbarWindowSandboxContext.(TaskbarSandboxComponent.Builder) -> Unit
 
 /**
  * [SandboxApplication] for running Taskbar tests.
@@ -37,20 +50,46 @@
  * [DEFAULT_DISPLAY] (i.e. test is executing on a device).
  */
 class TaskbarWindowSandboxContext
-private constructor(base: SandboxApplication, val virtualDisplay: VirtualDisplay) :
-    ContextWrapper(base),
-    ObjectSandbox by base,
-    TestRule by RuleChain.outerRule(virtualDisplayRule(virtualDisplay)).around(base) {
+private constructor(
+    private val base: SandboxApplication,
+    val virtualDisplay: VirtualDisplay,
+    private val componentBinder: TaskbarComponentBinder?,
+) : ContextWrapper(base), ObjectSandbox by base, TestRule {
 
-    init {
-        putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(this))
+    val settingsCacheSandbox = SettingsCacheSandbox()
+
+    private val virtualDisplayRule =
+        object : ExternalResource() {
+            override fun after() = virtualDisplay.release()
+        }
+
+    private val singletonSetupRule =
+        object : ExternalResource() {
+            override fun before() {
+                val context = this@TaskbarWindowSandboxContext
+                val builder =
+                    DaggerTaskbarSandboxComponent.builder()
+                        .bindSystemUiProxy(SystemUiProxy(context))
+                        .bindSettingsCache(settingsCacheSandbox.cache)
+                componentBinder?.invoke(context, builder)
+                base.initDaggerComponent(builder)
+
+                putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(context))
+            }
+        }
+
+    override fun apply(statement: Statement, description: Description): Statement {
+        return RuleChain.outerRule(virtualDisplayRule)
+            .around(base)
+            .around(singletonSetupRule)
+            .apply(statement, description)
     }
 
     companion object {
         private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay"
 
         /** Creates a [SandboxApplication] for Taskbar tests. */
-        fun create(): TaskbarWindowSandboxContext {
+        fun create(componentBinder: TaskbarComponentBinder? = null): TaskbarWindowSandboxContext {
             val base = ApplicationProvider.getApplicationContext<Context>()
             val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java))
 
@@ -70,13 +109,21 @@
             return TaskbarWindowSandboxContext(
                 SandboxApplication(base.createDisplayContext(virtualDisplay.display)),
                 virtualDisplay,
+                componentBinder,
             )
         }
     }
 }
 
-private fun virtualDisplayRule(virtualDisplay: VirtualDisplay): TestRule {
-    return object : ExternalResource() {
-        override fun after() = virtualDisplay.release()
+@LauncherAppSingleton
+@Component
+interface TaskbarSandboxComponent : LauncherAppComponent {
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+        @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder
+
+        override fun build(): TaskbarSandboxComponent
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
new file mode 100644
index 0000000..dcd5352
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.net.Uri
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+
+/**
+ * Provides a sandboxed [SettingsCache] for testing.
+ *
+ * Note that listeners registered to [cache] will never be invoked.
+ */
+class SettingsCacheSandbox {
+    private val values = mutableMapOf<Uri, Int>()
+
+    /** Fake cache that delegates [SettingsCache.getValue] to [values]. */
+    val cache =
+        mock<SettingsCache> {
+            on { getValue(any<Uri>()) } doAnswer { mock.getValue(it.getArgument(0), 1) }
+            on { getValue(any<Uri>(), any<Int>()) } doAnswer
+                {
+                    values.getOrDefault(it.getArgument(0), it.getArgument(1)) == 1
+                }
+        }
+
+    operator fun get(key: Uri): Int? = values[key]
+
+    operator fun set(key: Uri, value: Int) {
+        values[key] = value
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index 1f88743..c3d865f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -20,9 +20,13 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.R
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.shared.system.InputConsumerController
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -58,7 +62,9 @@
 
     @Before
     fun setup() {
-        sandboxContext.putObject(SystemUiProxy.INSTANCE, systemUiProxy)
+        sandboxContext.initDaggerComponent(
+            DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
+        )
         val deviceState = mock(RecentsAnimationDeviceState::class.java)
         whenever(deviceState.rotationTouchHelper).thenReturn(mock(RotationTouchHelper::class.java))
         gestureState = spy(GestureState(OverviewComponentObserver(sandboxContext, deviceState), 0))
@@ -71,7 +77,7 @@
                 gestureState,
                 0,
                 false,
-                inputConsumerController
+                inputConsumerController,
             )
         underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false)
     }
@@ -83,7 +89,7 @@
         verify(systemUiProxy)
             .updateContextualEduStats(
                 /* isTrackpadGesture= */ eq(true),
-                eq(GestureType.HOME.toString())
+                eq(GestureType.HOME.toString()),
             )
     }
 
@@ -93,7 +99,18 @@
         verify(systemUiProxy)
             .updateContextualEduStats(
                 /* isTrackpadGesture= */ eq(false),
-                eq(GestureType.HOME.toString())
+                eq(GestureType.HOME.toString()),
             )
     }
 }
+
+@LauncherAppSingleton
+@Component
+interface TestComponent : LauncherAppComponent {
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+        override fun build(): TestComponent
+    }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
index 04012c0..df98606 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
@@ -33,7 +33,7 @@
 @RunWith(AndroidJUnit4::class)
 class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() {
 
-    lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController
+    lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController<RecentsActivity>
     lateinit var stateListener: StateManager.StateListener<RecentsState>
 
     private val recentsActivity: RecentsActivity = mock()
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index 9bc1c59..2c275f4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -22,7 +22,7 @@
 import android.platform.test.annotations.PlatinumTest;
 
 import com.android.launcher3.tapl.Overview;
-import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
+import com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer;
 import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -76,7 +76,7 @@
         taskMenu.touchOutsideTaskMenuToDismiss();
 
         OverviewTaskMenu splitMenu = overview.getCurrentTask().tapMenu(
-                        OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
+                        OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT);
         assertTrue("App info item not appearing in expanded split task's menu.",
                 splitMenu.hasMenuItem("App info"));
         splitMenu.touchOutsideTaskMenuToDismiss();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
index 694a382..2a8afbf 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -21,6 +21,8 @@
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
 import com.android.launcher3.BuildConfig
+import com.android.launcher3.tapl.LaunchedAppState
+import com.android.launcher3.tapl.OverviewTask
 import com.android.launcher3.ui.AbstractLauncherUiTest
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape
 import com.android.launcher3.uioverrides.QuickstepLauncher
@@ -45,22 +47,16 @@
     @Test
     @PortraitLandscape
     fun enterDesktopViaOverviewMenu() {
-        // Move last launched TEST_ACTIVITY_2 into Desktop
-        mLauncher.workspace
-            .switchToOverview()
-            .getTestActivityTask(TEST_ACTIVITY_2)
-            .tapMenu()
-            .tapDesktopMenuItem()
-        assertTestAppLaunched(TEST_ACTIVITY_2)
+        mLauncher.workspace.switchToOverview()
+        moveTaskToDesktop(TEST_ACTIVITY_2) // Move last launched TEST_ACTIVITY_2 into Desktop
 
         // Scroll back to TEST_ACTIVITY_1, then move it into Desktop
         mLauncher
             .goHome()
             .switchToOverview()
             .apply { flingForward() }
-            .getTestActivityTask(TEST_ACTIVITY_1)
-            .tapMenu()
-            .tapDesktopMenuItem()
+            .also { moveTaskToDesktop(TEST_ACTIVITY_1) }
+
         TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
 
         // Launch static DesktopTaskView
@@ -73,6 +69,83 @@
         TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
     }
 
+    @Test
+    @PortraitLandscape
+    fun dismissFocusedTasks_thenDesktopIsCentered() {
+        // Create DesktopTaskView
+        mLauncher.goHome().switchToOverview()
+        moveTaskToDesktop(TEST_ACTIVITY_2)
+
+        // Create a new task activity to be the focused task
+        mLauncher.goHome()
+        startTestActivity(TEST_ACTIVITY_EXTRA)
+
+        val overview = mLauncher.goHome().switchToOverview()
+
+        // Dismiss focused task
+        val focusedTask1 = overview.currentTask
+        assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_EXTRA)
+        focusedTask1.dismiss()
+
+        // Dismiss new focused task
+        val focusedTask2 = overview.currentTask
+        assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1)
+        focusedTask2.dismiss()
+
+        // Dismiss DesktopTaskView
+        val desktopTask = overview.currentTask
+        assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue()
+        desktopTask.dismiss()
+
+        assertWithMessage("Still have tasks after dismissing all the tasks")
+            .that(mLauncher.workspace.switchToOverview().hasTasks())
+            .isFalse()
+    }
+
+    @Test
+    @PortraitLandscape
+    fun dismissFocusedTask_thenDesktopTask_thenFocusedTaskIsCentered() {
+        // Create extra activity to be DesktopTaskView
+        startTestActivity(TEST_ACTIVITY_EXTRA)
+        mLauncher.goHome().switchToOverview()
+        val desktop = moveTaskToDesktop(TEST_ACTIVITY_EXTRA)
+
+        val overview = desktop.switchToOverview()
+
+        // Dismiss focused task
+        val focusedTask1 = overview.getTestActivityTask(TEST_ACTIVITY_2)
+        assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_2)
+        focusedTask1.dismiss()
+
+        // Dismiss DesktopTaskView
+        val desktopTask = overview.currentTask
+        assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue()
+        desktopTask.dismiss()
+
+        // Dismiss focused task
+        val focusedTask2 = overview.currentTask
+        assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1)
+        focusedTask2.dismiss()
+
+        assertWithMessage("Still have tasks after dismissing all the tasks")
+            .that(mLauncher.workspace.switchToOverview().hasTasks())
+            .isFalse()
+    }
+
+    private fun assertTaskContentDescription(task: OverviewTask, activityIndex: Int) {
+        assertWithMessage("The current task content description is not TestActivity$activityIndex.")
+            .that(task.containsContentDescription("TestActivity$activityIndex"))
+            .isTrue()
+    }
+
+    private fun moveTaskToDesktop(activityIndex: Int): LaunchedAppState {
+        return mLauncher.overview
+            .getTestActivityTask(activityIndex)
+            .tapMenu()
+            .tapDesktopMenuItem()
+            .also { assertTestAppLaunched(activityIndex) }
+    }
+
     private fun startTestAppsWithCheck() {
         TEST_ACTIVITIES.forEach {
             startTestActivity(it)
@@ -91,7 +164,7 @@
             .that(
                 mDevice.wait(
                     Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
-                    DEFAULT_UI_TIMEOUT
+                    DEFAULT_UI_TIMEOUT,
                 )
             )
             .isTrue()
@@ -100,6 +173,7 @@
     companion object {
         const val TEST_ACTIVITY_1 = 2
         const val TEST_ACTIVITY_2 = 3
+        const val TEST_ACTIVITY_EXTRA = 4
         val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2)
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/WindowAnimatorTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/WindowAnimatorTest.kt
deleted file mode 100644
index e5e6df3..0000000
--- a/quickstep/tests/src/com/android/quickstep/desktop/WindowAnimatorTest.kt
+++ /dev/null
@@ -1,139 +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.desktop
-
-import android.animation.ValueAnimator
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.Rect
-import android.util.DisplayMetrics
-import android.view.SurfaceControl
-import android.window.TransitionInfo
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
-import com.android.app.animation.Interpolators
-import com.android.launcher3.desktop.WindowAnimator
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyFloat
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class WindowAnimatorTest {
-
-    private val context = mock<Context>()
-    private val resources = mock<Resources>()
-    private val transaction = mock<SurfaceControl.Transaction>()
-    private val change = mock<TransitionInfo.Change>()
-    private val leash = mock<SurfaceControl>()
-
-    private val displayMetrics = DisplayMetrics().apply { density = 1f }
-
-    @Before
-    fun setup() {
-        whenever(context.resources).thenReturn(resources)
-        whenever(resources.displayMetrics).thenReturn(displayMetrics)
-        whenever(change.leash).thenReturn(leash)
-        whenever(change.startAbsBounds).thenReturn(START_BOUNDS)
-        whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction)
-        whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction)
-    }
-
-    @Test
-    fun createBoundsAnimator_returnsCorrectDefaultAnimatorParams() = runOnUiThread {
-        val boundsAnimParams =
-            WindowAnimator.BoundsAnimationParams(
-                durationMs = 100L,
-                interpolator = Interpolators.STANDARD_ACCELERATE,
-            )
-
-        val valueAnimator =
-            WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
-
-        assertThat(valueAnimator.duration).isEqualTo(100L)
-        assertThat(valueAnimator.interpolator).isEqualTo(Interpolators.STANDARD_ACCELERATE)
-        assertStartAndEndBounds(valueAnimator, startBounds = START_BOUNDS, endBounds = START_BOUNDS)
-    }
-
-    @Test
-    fun createBoundsAnimator_startScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
-        val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
-        whenever(change.startAbsBounds).thenReturn(bounds)
-        val boundsAnimParams =
-            WindowAnimator.BoundsAnimationParams(
-                durationMs = 100L,
-                startOffsetYDp = 10f,
-                startScale = 0.5f,
-                interpolator = Interpolators.STANDARD_ACCELERATE,
-            )
-
-        val valueAnimator =
-            WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
-
-        assertStartAndEndBounds(
-            valueAnimator,
-            startBounds =
-                Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
-            endBounds = bounds,
-        )
-    }
-
-    @Test
-    fun createBoundsAnimator_endScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
-        val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
-        whenever(change.startAbsBounds).thenReturn(bounds)
-        val boundsAnimParams =
-            WindowAnimator.BoundsAnimationParams(
-                durationMs = 100L,
-                endOffsetYDp = 10f,
-                endScale = 0.5f,
-                interpolator = Interpolators.STANDARD_ACCELERATE,
-            )
-
-        val valueAnimator =
-            WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
-
-        assertStartAndEndBounds(
-            valueAnimator,
-            startBounds = bounds,
-            endBounds = Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
-        )
-    }
-
-    private fun assertStartAndEndBounds(
-        valueAnimator: ValueAnimator,
-        startBounds: Rect,
-        endBounds: Rect,
-    ) {
-        valueAnimator.start()
-        valueAnimator.animatedValue
-        assertThat(valueAnimator.animatedValue).isEqualTo(startBounds)
-        valueAnimator.end()
-        assertThat(valueAnimator.animatedValue).isEqualTo(endBounds)
-    }
-
-    companion object {
-        private val START_BOUNDS =
-            Rect(/* left= */ 10, /* top= */ 20, /* right= */ 30, /* bottom= */ 40)
-    }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
index 82361aa..99c74be 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
@@ -43,11 +43,11 @@
         val currentTask = overviewWithSplitPair.currentTask
         currentTask.containsContentDescription(
             By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(),
-            OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT
+            OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT,
         )
         currentTask.containsContentDescription(
             By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(),
-            OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT
+            OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT,
         )
         return overviewWithSplitPair
     }
diff --git a/res/drawable/work_mode_fab_background.xml b/res/drawable/work_mode_fab_background.xml
index 6be33e8..fd948d1 100644
--- a/res/drawable/work_mode_fab_background.xml
+++ b/res/drawable/work_mode_fab_background.xml
@@ -19,6 +19,9 @@
         <shape android:shape="rectangle">
             <corners android:radius="@dimen/work_fab_radius" />
             <solid android:color="@color/work_fab_bg_color" />
+            <padding
+                android:left="@dimen/work_mode_fab_background_horizontal_padding"
+                android:right="@dimen/work_mode_fab_background_horizontal_padding"/>
         </shape>
     </item>
 </ripple>
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 71c77b5..33a50b0 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -15,7 +15,7 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto">
 
-    <FrameLayout
+    <LinearLayout
         android:id="@+id/widgets_two_pane_sheet_paged_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -23,40 +23,17 @@
         android:layout_gravity="start"
         android:clipChildren="false"
         android:clipToPadding="false"
-        android:layout_alignParentStart="true">
-        <!-- Note: the paddingHorizontal has to be on WidgetPagedView level so that talkback
-         correctly orders the lists to be after the search and suggestions header. See b/209579563.
-          -->
-        <com.android.launcher3.widget.picker.WidgetPagedView
-            android:id="@+id/widgets_view_pager"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false"
-            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-            android:descendantFocusability="afterDescendants"
-            launcher:pageIndicator="@+id/tabs" >
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/primary_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false" />
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/work_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false" />
-
-        </com.android.launcher3.widget.picker.WidgetPagedView>
-
+        android:layout_alignParentStart="true"
+        android:orientation="vertical">
         <!-- SearchAndRecommendationsView without the tab layout as well -->
         <!-- Note: the horizontal padding matches with the WidgetPagedView -->
-        <com.android.launcher3.views.StickyHeaderLayout
+        <LinearLayout
             android:id="@+id/search_and_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:clipToOutline="true"
+            android:elevation="1dp"
+            android:background="?attr/widgetPickerPrimarySurfaceColor"
             android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
             android:orientation="vertical">
 
@@ -67,6 +44,7 @@
                 android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 android:gravity="center_vertical"
+                android:layout_marginBottom="8dp"
                 launcher:layout_sticky="true">
                 <FrameLayout
                     android:layout_width="0dp"
@@ -98,7 +76,6 @@
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:id="@+id/suggestions_header"
-                android:layout_marginTop="8dp"
                 android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 launcher:layout_sticky="true">
@@ -140,6 +117,31 @@
                     style="?android:attr/borderlessButtonStyle" />
 
             </com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
-        </com.android.launcher3.views.StickyHeaderLayout>
-    </FrameLayout>
+        </LinearLayout>
+        <!-- Note: the paddingHorizontal has to be on WidgetPagedView level so that talkback
+ correctly orders the lists to be after the search and suggestions header. See b/209579563.
+  -->
+        <com.android.launcher3.widget.picker.WidgetPagedView
+            android:id="@+id/widgets_view_pager"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:descendantFocusability="afterDescendants"
+            launcher:pageIndicator="@+id/tabs" >
+
+            <com.android.launcher3.widget.picker.WidgetsRecyclerView
+                android:id="@+id/primary_widgets_list_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false" />
+
+            <com.android.launcher3.widget.picker.WidgetsRecyclerView
+                android:id="@+id/work_widgets_list_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false" />
+
+        </com.android.launcher3.widget.picker.WidgetPagedView>
+    </LinearLayout>
 </merge>
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index c6b3b74..94f141b 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -15,28 +15,22 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto">
 
-    <FrameLayout
+    <LinearLayout
         android:id="@+id/widgets_two_pane_sheet_recyclerview"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="start"
         android:layout_gravity="start"
         android:clipChildren="false"
-        android:layout_alignParentStart="true">
-
-        <com.android.launcher3.widget.picker.WidgetsRecyclerView
-            android:id="@+id/primary_widgets_list_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-            android:clipToPadding="false" />
-
+        android:layout_alignParentStart="true"
+        android:orientation="vertical">
         <!-- SearchAndRecommendationsView without the tab layout as well -->
-        <com.android.launcher3.views.StickyHeaderLayout
+        <LinearLayout
             android:id="@+id/search_and_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:clipToOutline="true"
+            android:background="?attr/widgetPickerPrimarySurfaceColor"
             android:orientation="vertical">
 
             <LinearLayout
@@ -83,6 +77,13 @@
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 launcher:layout_sticky="true">
             </FrameLayout>
-        </com.android.launcher3.views.StickyHeaderLayout>
-    </FrameLayout>
+        </LinearLayout>
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/primary_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:clipToPadding="false" />
+    </LinearLayout>
 </merge>
\ No newline at end of file
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index b3484c9..fc59e56 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -23,18 +23,16 @@
     android:gravity="center_vertical"
     android:background="@drawable/work_mode_fab_background"
     android:forceHasOverlappingRendering="false"
-    android:contentDescription="@string/work_apps_pause_btn_text"
-    android:paddingStart="@dimen/work_mode_fab_background_start_padding"
-    android:paddingEnd="@dimen/work_mode_fab_background_end_padding"
-    android:animateLayoutChanges="true">
+    android:contentDescription="@string/work_apps_pause_btn_text">
     <ImageView
         android:id="@+id/work_icon"
         android:layout_width="@dimen/work_fab_icon_size"
         android:layout_height="@dimen/work_fab_icon_size"
+        android:layout_marginVertical="@dimen/work_fab_icon_vertical_margin"
         android:importantForAccessibility="no"
-        android:layout_marginEnd="@dimen/work_fab_icon_end_margin"
         android:src="@drawable/ic_corp_off"
         android:tint="@color/work_fab_icon_color"
+        android:layout_marginStart="@dimen/work_fab_icon_start_margin_expanded"
         android:scaleType="center"/>
     <TextView
         android:id="@+id/pause_text"
@@ -46,8 +44,8 @@
         android:includeFontPadding="false"
         android:textDirection="locale"
         android:text="@string/work_apps_pause_btn_text"
+        android:layout_marginStart="@dimen/work_fab_text_start_margin"
         android:layout_marginEnd="@dimen/work_fab_text_end_margin"
-        android:ellipsize="end"
         android:maxLines="1"
         style="@style/TextHeadline"/>
 </com.android.launcher3.allapps.WorkModeSwitch>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 5ddf8a2..6941139 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -189,7 +189,7 @@
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Nyahjeda"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Tapis"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Gagal: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
-    <string name="private_space_label" msgid="2359721649407947001">"Ruang privasi"</string>
+    <string name="private_space_label" msgid="2359721649407947001">"Ruang persendirian"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Ketik untuk menyediakan atau membuka"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Persendirian"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Tetapan Ruang Peribadi"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 57c9bc7..16ea0cd 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -208,8 +208,16 @@
         <attr name="layout_sticky" format="boolean" />
     </declare-styleable>
 
+    <declare-styleable name="NumRows">
+        <attr name="minDeviceWidthPx" format="float"/>
+        <attr name="minDeviceHeightPx" format="float"/>
+        <attr name="numRowsNew" format="integer"/>
+        <attr name="dbFile" />
+    </declare-styleable>
+
     <declare-styleable name="GridDisplayOption">
         <attr name="name" format="string" />
+        <attr name="title" />
 
         <attr name="numRows" format="integer" />
         <attr name="numColumns" format="integer" />
@@ -294,6 +302,7 @@
         <!-- File that contains the specs for all apps icon and text size.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="allAppsCellSpecsId" format="reference" />
+        <attr name="rowCountSpecsId" format="reference" />
         <!-- defaults to allAppsCellSpecsId, if not specified -->
         <attr name="allAppsCellSpecsTwoPanelId" format="reference" />
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 731e24e..037687d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -156,15 +156,16 @@
     <dimen name="work_fab_height">56dp</dimen>
     <dimen name="work_fab_radius">16dp</dimen>
     <dimen name="work_fab_icon_size">24dp</dimen>
-    <dimen name="work_fab_icon_end_margin">12dp</dimen>
-    <dimen name="work_fab_text_end_margin">16dp</dimen>
+    <dimen name="work_fab_icon_vertical_margin">16dp</dimen>
+    <dimen name="work_fab_icon_start_margin_expanded">4dp</dimen>
+    <dimen name="work_fab_text_start_margin">8dp</dimen>
+    <dimen name="work_fab_text_end_margin">10dp</dimen>
     <dimen name="work_card_padding_horizontal">10dp</dimen>
     <dimen name="work_fab_width">214dp</dimen>
     <dimen name="work_card_button_height">52dp</dimen>
     <dimen name="work_fab_margin">16dp</dimen>
     <dimen name="work_fab_margin_bottom">20dp</dimen>
-    <dimen name="work_mode_fab_background_start_padding">16dp</dimen>
-    <dimen name="work_mode_fab_background_end_padding">4dp</dimen>
+    <dimen name="work_mode_fab_background_horizontal_padding">16dp</dimen>
     <dimen name="work_profile_footer_padding">20dp</dimen>
     <dimen name="work_edu_card_margin">16dp</dimen>
     <dimen name="work_edu_card_radius">16dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9d06021..d918698 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -355,8 +355,9 @@
     <!-- Title for an app whose download has been started. -->
     <string name="app_waiting_download_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> waiting to install</string>
     <!-- Title for an app which is archived. -->
-    <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived. Tap to download and restore.</string>
-
+    <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived.</string>
+    <!-- Accessibility Action for an app which is archived. -->
+    <string name="app_unarchiving_action">download and restore</string>
 
     <!-- Title shown on the alert dialog prompting the user to update the application in market
      in order to re-enable the disabled shortcuts -->
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
index 0f0dde2..27fddc8 100644
--- a/res/xml/backupscheme.xml
+++ b/res/xml/backupscheme.xml
@@ -3,6 +3,8 @@
 
     <include domain="database" path="launcher.db" />
     <include domain="database" path="launcher_6_by_5.db" />
+    <include domain="database" path="launcher_5_by_6.db" />
+    <include domain="database" path="launcher_4_by_6.db" />
     <include domain="database" path="launcher_4_by_5.db" />
     <include domain="database" path="launcher_4_by_4.db" />
     <include domain="database" path="launcher_3_by_3.db" />
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 3774ae3..2e75261 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -306,10 +306,6 @@
         removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED);
     }
 
-    public boolean isPaused() {
-        return !hasBeenResumed() && (mActivityFlags & ACTIVITY_STATE_DEFERRED_RESUMED) == 0;
-    }
-
     /**
      * Sets the activity to appear as resumed.
      */
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 5f5fb72..50e78ac 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -50,8 +50,6 @@
     // automatically when user interacts with the launcher.
     public static final Object AUTO_CANCEL_ACTION_MODE = new Object();
 
-    private boolean mIsThemeUpdatedBeforeRecreate;
-
     private ActionMode mCurrentActionMode;
 
     private int mThemeRes = R.style.AppTheme;
@@ -82,13 +80,8 @@
         updateTheme();
     }
 
-    public boolean isThemeUpdatedBeforeRecreate() {
-        return mIsThemeUpdatedBeforeRecreate;
-    }
-
     protected void updateTheme() {
         if (mThemeRes != Themes.getActivityThemeRes(this)) {
-            mIsThemeUpdatedBeforeRecreate = true;
             recreate();
         }
     }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 909272e..34cf56b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -57,12 +57,14 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewDebug;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.TextView;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 
 import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
@@ -519,6 +521,16 @@
         }
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (getTag() instanceof ItemInfoWithIcon infoWithIcon && infoWithIcon.isInactiveArchive()) {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+                    AccessibilityNodeInfoCompat.ACTION_CLICK,
+                    getContext().getString(R.string.app_unarchiving_action)));
+        }
+    }
+
     /** This is used for testing to forcefully set the display. */
     @VisibleForTesting
     public void setDisplay(int display) {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index ece6540..7acba75 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -58,9 +58,9 @@
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Partner;
+import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -186,6 +186,8 @@
     @XmlRes
     public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
+    public int rowCountSpecsId = INVALID_RESOURCE_HANDLE;;
+    @XmlRes
     public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
     @XmlRes
     public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
@@ -232,8 +234,6 @@
         if (!newGridName.equals(gridName)) {
             LauncherPrefs.get(context).put(GRID_NAME, newGridName);
         }
-        LockedUserState.get(context).runOnUserUnlocked(() ->
-            new DeviceGridState(this).writeToPrefs(context));
 
         DisplayController.INSTANCE.get(context).setPriorityListener(
                 (displayContext, info, flags) -> {
@@ -340,15 +340,29 @@
         Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
         @DeviceType int deviceType = displayInfo.getDeviceType();
 
-        ArrayList<DisplayOption> allOptions =
+        List<DisplayOption> allOptions =
                 getPredefinedDeviceProfiles(context, gridName, deviceType,
                         RestoreDbTask.isPending(context));
+
+        // Filter out options that don't have the same number of columns as the grid
+        DeviceGridState deviceGridState = new DeviceGridState(context);
+        List<DisplayOption> allOptionsFilteredByColCount =
+                filterByColumnCount(allOptions, deviceGridState.getColumns());
+
         DisplayOption displayOption =
-                invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
+                invDistWeightedInterpolate(displayInfo, allOptionsFilteredByColCount.isEmpty()
+                        ? new ArrayList<>(allOptions)
+                        : new ArrayList<>(allOptionsFilteredByColCount), deviceType);
         initGrid(context, displayInfo, displayOption, deviceType);
         return displayOption.grid.name;
     }
 
+    private List<DisplayOption> filterByColumnCount(
+            List<DisplayOption> allOptions, int numColumns) {
+        return allOptions.stream().filter(
+                option -> option.grid.numColumns == numColumns).toList();
+    }
+
     /**
      * @deprecated This is a temporary solution because on the backup and restore case we modify the
      * IDP, this resets it. b/332974074
@@ -383,6 +397,7 @@
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+        rowCountSpecsId = closestProfile.mRowCountSpecsId;
         workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
         allAppsSpecsId = closestProfile.mAllAppsSpecsId;
         allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
@@ -495,7 +510,6 @@
         mChangeListeners.remove(listener);
     }
 
-
     public void setCurrentGrid(Context context, String gridName) {
         LauncherPrefs.get(context).put(GRID_NAME, gridName);
         MAIN_EXECUTOR.execute(() -> {
@@ -526,7 +540,7 @@
         }
     }
 
-    private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
+    private List<DisplayOption> getPredefinedDeviceProfiles(Context context,
             String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
 
@@ -539,7 +553,8 @@
                         && GridOption.TAG_NAME.equals(parser.getName())) {
 
                     GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
-                    if (gridOption.isEnabled(deviceType) || allowDisabledGrid) {
+                    if ((gridOption.isEnabled(deviceType) || allowDisabledGrid)
+                            && (Flags.oneGridSpecs() == gridOption.isNewGridOption())) {
                         final int displayDepth = parser.getDepth();
                         while (((type = parser.next()) != XmlPullParser.END_TAG
                                 || parser.getDepth() > displayDepth)
@@ -566,13 +581,16 @@
                 }
             }
         }
-        if (filteredProfiles.isEmpty()) {
-            // No grid found, use the default options
+        if (filteredProfiles.isEmpty() && TextUtils.isEmpty(gridName)) {
+            // Use the default options since gridName is empty and there's no valid grids.
             for (DisplayOption option : profiles) {
                 if (option.canBeDefault) {
                     filteredProfiles.add(option);
                 }
             }
+        } else if (filteredProfiles.isEmpty()) {
+            // In this case we had a grid selected but we couldn't find it.
+            filteredProfiles.addAll(profiles);
         }
         if (filteredProfiles.isEmpty()) {
             throw new RuntimeException("No display option with canBeDefault=true");
@@ -581,6 +599,72 @@
     }
 
     /**
+     * Parses through the xml to find NumRows specs. Then calls findBestRowCount to get the correct
+     * row count for this GridOption.
+     *
+     * @return the result of {@link #findBestRowCount(List, Context, int)}.
+     */
+    public static NumRows getRowCount(ResourceHelper resourceHelper, Context context,
+            int deviceType) {
+        ArrayList<NumRows> rowCounts = new ArrayList<>();
+
+        try (XmlResourceParser parser = resourceHelper.getXml()) {
+            final int depth = parser.getDepth();
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG
+                    || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if ((type == XmlPullParser.START_TAG)
+                        && "NumRows".equals(parser.getName())) {
+                    rowCounts.add(new NumRows(context, Xml.asAttributeSet(parser)));
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+
+        return findBestRowCount(rowCounts, context, deviceType);
+    }
+
+    /**
+     * @return the biggest row count that fits the display dimensions spec using NumRows to
+     * determine that. If no best row count is found, return -1.
+     */
+    public static NumRows findBestRowCount(List<NumRows> list, Context context,
+            @DeviceType int deviceType) {
+        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+        int minWidthPx = Integer.MAX_VALUE;
+        int minHeightPx = Integer.MAX_VALUE;
+        for (WindowBounds bounds : displayInfo.supportedBounds) {
+            boolean isTablet = displayInfo.isTablet(bounds);
+            if (isTablet && deviceType == TYPE_MULTI_DISPLAY) {
+                // For split displays, take half width per page
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+            } else if (!isTablet && bounds.isLandscape()) {
+                // We will use transposed layout in this case
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
+            } else {
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+            }
+        }
+
+        NumRows selectedRow = null;
+        for (NumRows item: list) {
+            if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
+                if (selectedRow == null || selectedRow.mNumRowsNew < item.mNumRowsNew) {
+                    selectedRow = item;
+                }
+            }
+        }
+        if (selectedRow != null) {
+            return selectedRow;
+        }
+        return null;
+    }
+
+    /**
      * Returns the GridOption associated to the given file name or null if the fileName is not
      * supported.
      * Ej, launcher.db -> "normal grid", launcher_4_by_4.db -> "practical grid"
@@ -626,6 +710,7 @@
         return parseAllDefinedGridOptions(context)
                 .stream()
                 .filter(go -> go.isEnabled(deviceType))
+                .filter(go -> (Flags.oneGridSpecs() == go.isNewGridOption()))
                 .collect(Collectors.toList());
     }
 
@@ -709,7 +794,7 @@
     }
 
     private static DisplayOption invDistWeightedInterpolate(
-            Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) {
+            Info displayInfo, List<DisplayOption> points, @DeviceType int deviceType) {
         int minWidthPx = Integer.MAX_VALUE;
         int minHeightPx = Integer.MAX_VALUE;
         for (WindowBounds bounds : displayInfo.supportedBounds) {
@@ -733,7 +818,7 @@
         float height = dpiFromPx(minHeightPx, displayInfo.getDensityDpi());
 
         // Sort the profiles based on the closeness to the device size
-        Collections.sort(points, (a, b) ->
+        points.sort((a, b) ->
                 Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                         dist(width, height, b.minWidthDps, b.minHeightDps)));
 
@@ -855,6 +940,7 @@
         private static final int DONT_INLINE_QSB = 0;
 
         public final String name;
+        public final String title;
         public final int numRows;
         public final int numColumns;
         public final int numSearchContainerColumns;
@@ -894,17 +980,30 @@
         private final int mWorkspaceCellSpecsTwoPanelId;
         private final int mAllAppsCellSpecsId;
         private final int mAllAppsCellSpecsTwoPanelId;
+        private final int mRowCountSpecsId;
 
         public GridOption(Context context, AttributeSet attrs) {
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.GridDisplayOption);
             name = a.getString(R.styleable.GridDisplayOption_name);
-            numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+            title = a.getString(R.styleable.GridDisplayOption_title);
+            deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
+                    DEVICE_CATEGORY_ALL);
+            mRowCountSpecsId = a.getResourceId(
+                    R.styleable.GridDisplayOption_rowCountSpecsId, INVALID_RESOURCE_HANDLE);
+            if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+                ResourceHelper resourceHelper = new ResourceHelper(context, mRowCountSpecsId);
+                NumRows numR = getRowCount(resourceHelper, context, deviceCategory);
+                numRows = numR.mNumRowsNew;
+                dbFile = numR.mDbFile;
+            } else {
+                numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+                dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
+            }
+
             numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
             numSearchContainerColumns = a.getInt(
                     R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns);
-
-            dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
             defaultLayoutId = a.getResourceId(
                     R.styleable.GridDisplayOption_defaultLayoutId, 0);
             demoModeLayoutId = a.getResourceId(
@@ -969,8 +1068,6 @@
                     R.styleable.GridDisplayOption_isScalable, false);
             devicePaddingId = a.getResourceId(
                     R.styleable.GridDisplayOption_devicePaddingId, INVALID_RESOURCE_HANDLE);
-            deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
-                    DEVICE_CATEGORY_ALL);
 
             if (FeatureFlags.enableResponsiveWorkspace()) {
                 mWorkspaceSpecsId = a.getResourceId(
@@ -1053,6 +1150,28 @@
                     return false;
             }
         }
+
+        public boolean isNewGridOption() {
+            return mRowCountSpecsId != INVALID_RESOURCE_HANDLE;
+        }
+    }
+
+    public static final class NumRows {
+        final int mNumRowsNew;
+        final float mMinDeviceWidthPx;
+        final float mMinDeviceHeightPx;
+        final String mDbFile;
+
+        NumRows(Context context, AttributeSet attrs) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumRows);
+
+            mNumRowsNew = (int) a.getFloat(R.styleable.NumRows_numRowsNew, 0);
+            mMinDeviceWidthPx = a.getFloat(R.styleable.NumRows_minDeviceWidthPx, 0);
+            mMinDeviceHeightPx = a.getFloat(R.styleable.NumRows_minDeviceHeightPx, 0);
+            mDbFile = a.getString(R.styleable.NumRows_dbFile);
+
+            a.recycle();
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8547eb4..983cf8d 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1345,7 +1345,8 @@
         if (requestArgs != null) {
             setWaitingForResult(requestArgs);
         }
-        mPendingActivityRequestCode = savedState.getInt(RUNTIME_STATE_PENDING_REQUEST_CODE);
+        mPendingActivityRequestCode = savedState.getInt(
+                RUNTIME_STATE_PENDING_REQUEST_CODE, mPendingActivityRequestCode);
 
         mPendingActivityResult = savedState.getParcelable(RUNTIME_STATE_PENDING_ACTIVITY_RESULT);
 
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index d730cea..1148f79 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -18,6 +18,8 @@
     public static final String LAUNCHER_DB = "launcher.db";
     public static final String LAUNCHER_6_BY_5_DB = "launcher_6_by_5.db";
     public static final String LAUNCHER_4_BY_5_DB = "launcher_4_by_5.db";
+    public static final String LAUNCHER_4_BY_6_DB = "launcher_4_by_6.db";
+    public static final String LAUNCHER_5_BY_6_DB = "launcher_5_by_6.db";
     public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
     public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
     public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
@@ -35,6 +37,8 @@
             LAUNCHER_DB,
             LAUNCHER_6_BY_5_DB,
             LAUNCHER_4_BY_5_DB,
+            LAUNCHER_4_BY_6_DB,
+            LAUNCHER_5_BY_6_DB,
             LAUNCHER_4_BY_4_DB,
             LAUNCHER_3_BY_3_DB,
             LAUNCHER_2_BY_2_DB));
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index a013eaa..85ecd58 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -25,7 +25,6 @@
 import android.util.Pair
 import androidx.annotation.WorkerThread
 import com.android.launcher3.celllayout.CellPosMapper
-import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.AddWorkspaceItemsTask
 import com.android.launcher3.model.AllAppsList
@@ -67,8 +66,8 @@
     private val context: Context,
     private val mApp: LauncherAppState,
     private val iconCache: IconCache,
-    private val appFilter: AppFilter,
-    private val mPmHelper: PackageManagerHelper,
+    appFilter: AppFilter,
+    mPmHelper: PackageManagerHelper,
     isPrimaryInstance: Boolean,
 ) {
 
@@ -304,9 +303,6 @@
                     launcherBinder.bindAllApps()
                     launcherBinder.bindDeepShortcuts()
                     launcherBinder.bindWidgets()
-                    if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                        this.modelDelegate.bindAllModelExtras(callbacksList)
-                    }
                     return true
                 } else {
                     mLoaderTask =
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 1094768..0dd2791 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -296,6 +296,10 @@
             // Add the search box above everything else in this container (if the flag is enabled,
             // it's added to drag layer in onAttach instead).
             addView(mSearchContainer);
+            // The search container is visually at the top of the all apps UI, and should thus be
+            // focused by default. It's added to end of the children list, so it needs to be
+            // explicitly marked as focused by default.
+            mSearchContainer.setFocusedByDefault(true);
         }
         mSearchUiManager = (SearchUiManager) mSearchContainer;
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index e705d94..51d1c9f 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -331,6 +331,9 @@
 
     public void setLettersToScrollLayout(
             List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections) {
+        if (fastScrollSections.isEmpty()) {
+            return;
+        }
         if (mLetterList != null) {
             mLetterList.removeAllViews();
         }
@@ -364,6 +367,8 @@
         mLetterList.addView(lastLetterListTextView);
         constraintTextViewsVertically(mLetterList, textViews);
         mLetterList.setVisibility(VISIBLE);
+        // Set the alpha to 0 to avoid the letter list being shown when it shouldn't be.
+        mLetterList.setAlpha(0);
     }
 
     private void constraintTextViewsVertically(ConstraintLayout constraintLayout,
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 6049574..f1f72b2 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -15,10 +15,18 @@
  */
 package com.android.launcher3.allapps;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Rect;
+import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.view.ViewGroup;
 import android.view.WindowInsets;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
@@ -26,10 +34,12 @@
 import androidx.core.graphics.Insets;
 import androidx.core.view.WindowInsetsCompat;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedPropertySetter;
 import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.views.ActivityContext;
@@ -39,9 +49,14 @@
 public class WorkModeSwitch extends LinearLayout implements Insettable,
         KeyboardInsetAnimationCallback.KeyboardInsetListener {
 
+    private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
+    private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
+    private static final int EXPAND_COLLAPSE_DURATION = 300;
+    private static final int TEXT_ALPHA_EXPAND_DELAY = 80;
+    private static final int TEXT_ALPHA_COLLAPSE_DELAY = 0;
     private static final int FLAG_FADE_ONGOING = 1 << 1;
     private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
-    private static final int FLAG_PROFILE_TOGGLE_ONGOING = 1 << 3;
+    private static final int FLAG_IS_EXPAND = 1 << 3;
     private static final int SCROLL_THRESHOLD_DP = 10;
 
     private final Rect mInsets = new Rect();
@@ -49,11 +64,15 @@
     private int mFlags;
     private final ActivityContext mActivityContext;
     private final Context mContext;
+    private final int mTextMarginStart;
+    private final int mTextMarginEnd;
+    private final int mIconMarginStart;
 
     // Threshold when user scrolls up/down to determine when should button extend/collapse
     private final int mScrollThreshold;
     private TextView mTextView;
-
+    private ImageView mIcon;
+    private ValueAnimator mPauseFABAnim;
 
     public WorkModeSwitch(@NonNull Context context) {
         this(context, null, 0);
@@ -68,6 +87,12 @@
         mContext = context;
         mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
         mActivityContext = ActivityContext.lookupContext(getContext());
+        mTextMarginStart = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_text_start_margin);
+        mTextMarginEnd = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_text_end_margin);
+        mIconMarginStart = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_icon_start_margin_expanded);
     }
 
     @Override
@@ -75,11 +100,13 @@
         super.onFinishInflate();
 
         mTextView = findViewById(R.id.pause_text);
+        mIcon = findViewById(R.id.work_icon);
         setSelected(true);
         KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
                 new KeyboardInsetAnimationCallback(this);
         setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
-
+        // Expand is the default state upon initialization.
+        addFlag(FLAG_IS_EXPAND);
         setInsets(mActivityContext.getDeviceProfile().getInsets());
         updateStringFromCache();
     }
@@ -114,18 +141,18 @@
 
     @Override
     public boolean isEnabled() {
-        return super.isEnabled() && getVisibility() == VISIBLE && mFlags == 0;
+        return super.isEnabled() && getVisibility() == VISIBLE;
     }
 
     public void animateVisibility(boolean visible) {
         clearAnimation();
         if (visible) {
-            setFlag(FLAG_FADE_ONGOING);
+            addFlag(FLAG_FADE_ONGOING);
             setVisibility(VISIBLE);
             extend();
             animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
         } else if (getVisibility() != GONE) {
-            setFlag(FLAG_FADE_ONGOING);
+            addFlag(FLAG_FADE_ONGOING);
             animate().alpha(0).withEndAction(() -> {
                 removeFlag(FLAG_FADE_ONGOING);
                 setVisibility(GONE);
@@ -156,6 +183,79 @@
         super.setTranslationY(Math.min(translationY, -mInsets.bottom));
     }
 
+
+    private void animatePillTransition(boolean isExpanding) {
+        if (!shouldAnimate(isExpanding)) {
+            return;
+        }
+        AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
+        mTextView.measure(0,0);
+        int currentWidth = mTextView.getWidth();
+        int fullWidth = mTextView.getMeasuredWidth();
+        float from = isExpanding ? 0 : currentWidth;
+        float to = isExpanding ? fullWidth : 0;
+        mPauseFABAnim = ObjectAnimator.ofFloat(from, to);
+        mPauseFABAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        mPauseFABAnim.setInterpolator(Interpolators.STANDARD);
+        mPauseFABAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float translation = (float) valueAnimator.getAnimatedValue();
+                float translationFraction = translation / fullWidth;
+                ViewGroup.MarginLayoutParams textViewLayoutParams =
+                        (ViewGroup.MarginLayoutParams) mTextView.getLayoutParams();
+                textViewLayoutParams.width = (int) translation;
+                textViewLayoutParams.setMarginStart((int) (mTextMarginStart * translationFraction));
+                textViewLayoutParams.setMarginEnd((int) (mTextMarginEnd * translationFraction));
+                mTextView.setLayoutParams(textViewLayoutParams);
+                ViewGroup.MarginLayoutParams iconLayoutParams =
+                        (ViewGroup.MarginLayoutParams) mIcon.getLayoutParams();
+                iconLayoutParams.setMarginStart((int) (mIconMarginStart * translationFraction));
+                mIcon.setLayoutParams(iconLayoutParams);
+            }
+        });
+        mPauseFABAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (isExpanding) {
+                    addFlag(FLAG_IS_EXPAND);
+                } else {
+                    mTextView.setVisibility(GONE);
+                    removeFlag(FLAG_IS_EXPAND);
+                }
+                mTextView.setHorizontallyScrolling(false);
+                mTextView.setEllipsize(TextUtils.TruncateAt.END);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mTextView.setHorizontallyScrolling(true);
+                mTextView.setVisibility(VISIBLE);
+                mTextView.setEllipsize(null);
+            }
+        });
+        animatorSet.playTogether(mPauseFABAnim, updatePauseTextAlpha(isExpanding));
+        animatorSet.start();
+    }
+
+
+    private ValueAnimator updatePauseTextAlpha(boolean expand) {
+        float from = expand ? 0 : 1;
+        float to = expand ? 1 : 0;
+        ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
+        alphaAnim.setDuration(expand ? TEXT_EXPAND_OPACITY_DURATION
+                : TEXT_COLLAPSE_OPACITY_DURATION);
+        alphaAnim.setStartDelay(expand ? TEXT_ALPHA_EXPAND_DELAY : TEXT_ALPHA_COLLAPSE_DELAY);
+        alphaAnim.setInterpolator(Interpolators.LINEAR);
+        alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mTextView.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return alphaAnim;
+    }
+
     private void setInsets(Rect rect, Insets insets) {
         rect.set(insets.left, insets.top, insets.right, insets.bottom);
     }
@@ -166,7 +266,7 @@
 
     @Override
     public void onTranslationStart() {
-        setFlag(FLAG_TRANSLATION_ONGOING);
+        addFlag(FLAG_TRANSLATION_ONGOING);
     }
 
     @Override
@@ -174,7 +274,7 @@
         removeFlag(FLAG_TRANSLATION_ONGOING);
     }
 
-    private void setFlag(int flag) {
+    private void addFlag(int flag) {
         mFlags |= flag;
     }
 
@@ -182,12 +282,25 @@
         mFlags &= ~flag;
     }
 
+    private boolean containsFlag(int flag) {
+        return (mFlags & flag) == flag;
+    }
+
     public void extend() {
-        mTextView.setVisibility(VISIBLE);
+        animatePillTransition(true);
     }
 
     public void shrink(){
-        mTextView.setVisibility(GONE);
+         animatePillTransition(false);
+    }
+
+    /**
+     * Determines if the button should animate based on current state. It should animate the button
+     * only if it is not in the same state it is animating to.
+     */
+    private boolean shouldAnimate(boolean expanding) {
+        return expanding != containsFlag(FLAG_IS_EXPAND)
+                && (mPauseFABAnim == null || !mPauseFABAnim.isRunning());
     }
 
     public int getScrollThreshold() {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8fe1b34..9e38824 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -62,17 +62,6 @@
      * and set a default value for the flag. This will be the default value on Debug builds.
      * <p>
      */
-    // TODO(Block 3): Clean up flags
-    public static final BooleanFlag ENABLE_WORKSPACE_LOADING_OPTIMIZATION = getDebugFlag(251502424,
-            "ENABLE_WORKSPACE_LOADING_OPTIMIZATION", DISABLED,
-            "load the current workspace screen visible to the user before the rest rather than "
-                    + "loading all of them at once.");
-
-    public static final BooleanFlag CHANGE_MODEL_DELEGATE_LOADING_ORDER = getDebugFlag(251502424,
-            "CHANGE_MODEL_DELEGATE_LOADING_ORDER", DISABLED,
-            "changes the timing of the loading and binding of delegate items during "
-                    + "data preparation for loading the home screen");
-
     // TODO(Block 6): Clean up flags
     public static final BooleanFlag SECONDARY_DRAG_N_DROP_TO_PIN = getDebugFlag(270395140,
             "SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED,
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index ecc5bb2..0fa275e 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -18,9 +18,11 @@
 
 import android.content.Context;
 
+import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.DaggerSingletonTracker;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.SettingsCache;
@@ -39,13 +41,15 @@
  */
 public interface LauncherBaseAppComponent {
     DaggerSingletonTracker getDaggerSingletonTracker();
-    RefreshRateTracker getRefreshRateTracker();
-    InstallSessionHelper getInstallSessionHelper();
     ApiWrapper getApiWrapper();
+    CustomWidgetManager getCustomWidgetManager();
+    IconShape getIconShape();
+    InstallSessionHelper getInstallSessionHelper();
+    RefreshRateTracker getRefreshRateTracker();
     ScreenOnTracker getScreenOnTracker();
     SettingsCache getSettingsCache();
-    CustomWidgetManager getCustomWidgetManager();
     PluginManagerWrapper getPluginManagerWrapper();
+    PackageManagerHelper getPackageManagerHelper();
 
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 259e543..a5bcd0f 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -73,6 +73,7 @@
     private static final String TAG = "GridCustomizationsProvider";
 
     private static final String KEY_NAME = "name";
+    private static final String KEY_GRID_TITLE = "grid_title";
     private static final String KEY_ROWS = "rows";
     private static final String KEY_COLS = "cols";
     private static final String KEY_PREVIEW_COUNT = "preview_count";
@@ -117,6 +118,7 @@
                 for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
                     cursor.newRow()
                             .add(KEY_NAME, gridOption.name)
+                            .add(KEY_GRID_TITLE, gridOption.title)
                             .add(KEY_ROWS, gridOption.numRows)
                             .add(KEY_COLS, gridOption.numColumns)
                             .add(KEY_PREVIEW_COUNT, 1)
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
index 5f8f2dc..cb14587 100644
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ b/src/com/android/launcher3/graphics/IconShape.java
@@ -41,10 +41,12 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.views.ClipPathView;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -54,19 +56,22 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /**
  * Abstract representation of the shape of an icon shape
  */
-public final class IconShape implements SafeCloseable {
+@LauncherAppSingleton
+public final class IconShape {
 
-    public static final MainThreadInitializedObject<IconShape> INSTANCE =
-            new MainThreadInitializedObject<>(IconShape::new);
-
+    public static DaggerSingletonObject<IconShape> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getIconShape);
 
     private ShapeDelegate mDelegate = new Circle();
     private float mNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
 
-    private IconShape(Context context) {
+    @Inject
+    public IconShape(@ApplicationContext Context context) {
         pickBestShape(context);
     }
 
@@ -78,9 +83,6 @@
         return mNormalizationScale;
     }
 
-    @Override
-    public void close() { }
-
     /**
      * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
      */
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 5faa2b8..b51f855 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -59,11 +58,9 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
@@ -100,13 +97,29 @@
     public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
         Trace.beginSection("BaseLauncherBinder#bindWorkspace");
         try {
-            if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
-                DisjointWorkspaceBinder workspaceBinder =
-                    initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
-                workspaceBinder.bindCurrentWorkspacePages(isBindSync);
-                workspaceBinder.bindOtherWorkspacePages();
-            } else {
-                bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+            // Save a copy of all the bg-thread collections
+            ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
+            final IntArray orderedScreenIds = new IntArray();
+            ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
+            final int workspaceItemCount;
+            synchronized (mBgDataModel) {
+                workspaceItems.addAll(mBgDataModel.workspaceItems);
+                appWidgets.addAll(mBgDataModel.appWidgets);
+                orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
+                mBgDataModel.extraItems.forEach(extraItems::add);
+                if (incrementBindId) {
+                    mBgDataModel.lastBindId++;
+                    mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
+                }
+                mMyBindingId = mBgDataModel.lastBindId;
+                workspaceItemCount = mBgDataModel.itemsIdMap.size();
+            }
+
+            for (Callbacks cb : mCallbacksList) {
+                new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+                        workspaceItems, appWidgets, extraItems, orderedScreenIds)
+                        .bind(isBindSync, workspaceItemCount);
             }
         } finally {
             Trace.endSection();
@@ -114,53 +127,6 @@
     }
 
     /**
-     * Initializes the WorkspaceBinder for binding.
-     *
-     * @param incrementBindId this is used to stop previously started binding tasks that are
-     *                        obsolete but still queued.
-     * @param workspacePages this allows the Launcher to add the correct workspace screens.
-     */
-    public DisjointWorkspaceBinder initWorkspaceBinder(boolean incrementBindId,
-            IntArray workspacePages) {
-
-        synchronized (mBgDataModel) {
-            if (incrementBindId) {
-                mBgDataModel.lastBindId++;
-                mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
-            }
-            mMyBindingId = mBgDataModel.lastBindId;
-            return new DisjointWorkspaceBinder(workspacePages);
-        }
-    }
-
-    private void bindWorkspaceAllAtOnce(boolean incrementBindId, boolean isBindSync) {
-        // Save a copy of all the bg-thread collections
-        ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
-        final IntArray orderedScreenIds = new IntArray();
-        ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
-        final int workspaceItemCount;
-        synchronized (mBgDataModel) {
-            workspaceItems.addAll(mBgDataModel.workspaceItems);
-            appWidgets.addAll(mBgDataModel.appWidgets);
-            orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
-            mBgDataModel.extraItems.forEach(extraItems::add);
-            if (incrementBindId) {
-                mBgDataModel.lastBindId++;
-                mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
-            }
-            mMyBindingId = mBgDataModel.lastBindId;
-            workspaceItemCount = mBgDataModel.itemsIdMap.size();
-        }
-
-        for (Callbacks cb : mCallbacksList) {
-            new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                    workspaceItems, appWidgets, extraItems, orderedScreenIds)
-                    .bind(isBindSync, workspaceItemCount);
-        }
-    }
-
-    /**
      * BindDeepShortcuts is abstract because it is a no-op for the go launcher.
      */
     public void bindDeepShortcuts() {
@@ -347,10 +313,8 @@
                 bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
                 bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
             }
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mExtraItems.forEach(item ->
-                        executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
-            }
+            mExtraItems.forEach(item ->
+                    executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
 
             RunnableList pendingTasks = new RunnableList();
             Executor pendingExecutor = pendingTasks::add;
@@ -440,126 +404,4 @@
             });
         }
     }
-
-    private class DisjointWorkspaceBinder {
-        private final IntArray mOrderedScreenIds;
-        private final IntSet mCurrentScreenIds = new IntSet();
-        private final Set<Integer> mBoundItemIds = new HashSet<>();
-
-        protected DisjointWorkspaceBinder(IntArray orderedScreenIds) {
-            mOrderedScreenIds = orderedScreenIds;
-
-            for (Callbacks cb : mCallbacksList) {
-                mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds));
-            }
-            if (mCurrentScreenIds.size() == 0) {
-                mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID);
-            }
-        }
-
-        /**
-         * Binds the currently loaded items in the Data Model. Also signals to the Callbacks[]
-         * that these items have been bound and their respective screens are ready to be shown.
-         *
-         * If this method is called after all the items on the workspace screen have already been
-         * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will
-         * not bind any items.
-         */
-        protected void bindCurrentWorkspacePages(boolean isBindSync) {
-            // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems;
-            ArrayList<LauncherAppWidgetInfo> appWidgets;
-            ArrayList<FixedContainerItems> fciList = new ArrayList<>();
-            final int workspaceItemCount;
-            synchronized (mBgDataModel) {
-                workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
-                appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
-                if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                    mBgDataModel.extraItems.forEach(fciList::add);
-                }
-                workspaceItemCount = mBgDataModel.itemsIdMap.size();
-            }
-
-            workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
-            appWidgets.forEach(it -> mBoundItemIds.add(it.id));
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                fciList.forEach(item ->
-                        executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
-            }
-
-            sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
-            // Tell the workspace that we're about to start binding items
-            executeCallbacksTask(c -> {
-                c.clearPendingBinds();
-                c.startBinding();
-            }, mUiExecutor);
-
-            // Bind workspace screens
-            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
-
-            bindWorkspaceItems(workspaceItems);
-            bindAppWidgets(appWidgets);
-            executeCallbacksTask(c -> {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
-                RunnableList onCompleteSignal = new RunnableList();
-                onCompleteSignal.executeAllAndDestroy();
-                c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal,
-                        workspaceItemCount, isBindSync);
-            }, mUiExecutor);
-        }
-
-        protected void bindOtherWorkspacePages() {
-            // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems;
-            ArrayList<LauncherAppWidgetInfo> appWidgets;
-
-            synchronized (mBgDataModel) {
-                workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
-                appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
-            }
-
-            workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id));
-            appWidgets.removeIf(it -> mBoundItemIds.contains(it.id));
-
-            sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
-            bindWorkspaceItems(workspaceItems);
-            bindAppWidgets(appWidgets);
-
-            executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor);
-            mUiExecutor.execute(() -> {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
-                ItemInstallQueue.INSTANCE.get(mApp.getContext())
-                        .resumeModelPush(FLAG_LOADER_RUNNING);
-            });
-
-            StringCache cacheClone = mBgDataModel.stringCache.clone();
-            executeCallbacksTask(c -> c.bindStringCache(cacheClone), mUiExecutor);
-        }
-
-        private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) {
-            // Bind the workspace items
-            int count = workspaceItems.size();
-            for (int i = 0; i < count; i += ITEMS_CHUNK) {
-                final int start = i;
-                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
-                executeCallbacksTask(
-                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                        mUiExecutor);
-            }
-        }
-
-        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) {
-            // Bind the widgets, one at a time
-            int count = appWidgets.size();
-            for (int i = 0; i < count; i++) {
-                final ItemInfo widget = appWidgets.get(i);
-                executeCallbacksTask(
-                        c -> c.bindItems(Collections.singletonList(widget), false),
-                        mUiExecutor);
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/model/DbEntry.java b/src/com/android/launcher3/model/DbEntry.java
deleted file mode 100644
index c0c51da..0000000
--- a/src/com/android/launcher3/model/DbEntry.java
+++ /dev/null
@@ -1,155 +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.model;
-
-import android.content.ContentValues;
-import android.content.Intent;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.util.ContentWriter;
-
-import java.net.URISyntaxException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class DbEntry extends ItemInfo implements Comparable<DbEntry> {
-
-    private static final String TAG = "DbEntry";
-
-    String mIntent;
-    String mProvider;
-    Map<String, Set<Integer>> mFolderItems = new HashMap<>();
-
-    /**
-     * Id of the specific widget.
-     */
-    public int appWidgetId = NO_ID;
-
-    /** Comparator according to the reading order */
-    @Override
-    public int compareTo(DbEntry another) {
-        if (screenId != another.screenId) {
-            return Integer.compare(screenId, another.screenId);
-        }
-        if (cellY != another.cellY) {
-            return Integer.compare(cellY, another.cellY);
-        }
-        return Integer.compare(cellX, another.cellX);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof DbEntry)) return false;
-        DbEntry entry = (DbEntry) o;
-        return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(getEntryMigrationId());
-    }
-
-    /**
-     *  Puts the updated DbEntry values into ContentValues which we then use to insert the
-     *  entry to the DB.
-     */
-    public void updateContentValues(ContentValues values) {
-        values.put(LauncherSettings.Favorites.SCREEN, screenId);
-        values.put(LauncherSettings.Favorites.CELLX, cellX);
-        values.put(LauncherSettings.Favorites.CELLY, cellY);
-        values.put(LauncherSettings.Favorites.SPANX, spanX);
-        values.put(LauncherSettings.Favorites.SPANY, spanY);
-    }
-
-    @Override
-    public void writeToValues(@NonNull ContentWriter writer) {
-        super.writeToValues(writer);
-        writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
-    }
-
-    @Override
-    public void readFromValues(@NonNull ContentValues values) {
-        super.readFromValues(values);
-        appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
-    }
-
-    /**
-     * This id is not used in the DB is only used while doing the migration and it identifies
-     * an entry on each workspace. For example two calculator icons would have the same
-     * migration id even thought they have different database ids.
-     */
-    public String getEntryMigrationId() {
-        switch (itemType) {
-            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-            case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
-                return getFolderMigrationId();
-            case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                // mProvider is the app the widget belongs to and appWidgetId it's the unique
-                // is of the widget, we need both because if you remove a widget and then add it
-                // again, then it can change and the WidgetProvider would not know the widget.
-                return mProvider + appWidgetId;
-            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                final String intentStr = cleanIntentString(mIntent);
-                try {
-                    Intent i = Intent.parseUri(intentStr, 0);
-                    return Objects.requireNonNull(i.getComponent()).toString();
-                } catch (Exception e) {
-                    return intentStr;
-                }
-            default:
-                return cleanIntentString(mIntent);
-        }
-    }
-
-    /**
-     * This method should return an id that should be the same for two folders containing the
-     * same elements.
-     */
-    @NonNull
-    private String getFolderMigrationId() {
-        return mFolderItems.keySet().stream()
-                .map(intentString -> mFolderItems.get(intentString).size()
-                        + cleanIntentString(intentString))
-                .sorted()
-                .collect(Collectors.joining(","));
-    }
-
-    /**
-     * This is needed because sourceBounds can change and make the id of two equal items
-     * different.
-     */
-    @NonNull
-    private String cleanIntentString(@NonNull String intentStr) {
-        try {
-            Intent i = Intent.parseUri(intentStr, 0);
-            i.setSourceBounds(null);
-            return i.toURI();
-        } catch (URISyntaxException e) {
-            Log.e(TAG, "Unable to parse Intent string", e);
-            return intentStr;
-        }
-
-    }
-}
diff --git a/src/com/android/launcher3/model/DbEntry.kt b/src/com/android/launcher3/model/DbEntry.kt
new file mode 100644
index 0000000..b79d312
--- /dev/null
+++ b/src/com/android/launcher3/model/DbEntry.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.model
+
+import android.content.ContentValues
+import android.content.Intent
+import android.util.Log
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.LauncherSettings.Favorites.CELLX
+import com.android.launcher3.LauncherSettings.Favorites.CELLY
+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_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Favorites.SCREEN
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ContentWriter
+import java.net.URISyntaxException
+import java.util.Objects
+
+class DbEntry : ItemInfo(), Comparable<DbEntry> {
+    @JvmField var mIntent: String? = null
+    @JvmField var mProvider: String? = null
+    @JvmField var mFolderItems: MutableMap<String, Set<Int>> = HashMap()
+
+    /** Id of the specific widget. */
+    @JvmField var appWidgetId: Int = NO_ID
+
+    /** Comparator according to the reading order */
+    override fun compareTo(other: DbEntry): Int {
+        if (screenId != other.screenId) {
+            return screenId.compareTo(other.screenId)
+        }
+        if (cellY != other.cellY) {
+            return cellY.compareTo(other.cellY)
+        }
+        return cellX.compareTo(other.cellX)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DbEntry) return false
+        return getEntryMigrationId() == other.getEntryMigrationId()
+    }
+
+    override fun hashCode(): Int = Objects.hash(getEntryMigrationId())
+
+    /**
+     * Puts the updated DbEntry values into ContentValues which we then use to insert the entry to
+     * the DB.
+     */
+    fun updateContentValues(values: ContentValues) =
+        values.apply {
+            put(SCREEN, screenId)
+            put(CELLX, cellX)
+            put(CELLY, cellY)
+            put(SPANX, spanX)
+            put(SPANY, spanY)
+        }
+
+    override fun writeToValues(writer: ContentWriter) {
+        super.writeToValues(writer)
+        writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId)
+    }
+
+    override fun readFromValues(values: ContentValues) {
+        super.readFromValues(values)
+        appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID)
+    }
+
+    /**
+     * This id is not used in the DB is only used while doing the migration and it identifies an
+     * entry on each workspace. For example two calculator icons would have the same migration id
+     * even thought they have different database ids.
+     */
+    private fun getEntryMigrationId(): String? {
+        when (itemType) {
+            ITEM_TYPE_FOLDER,
+            ITEM_TYPE_APP_PAIR -> return getFolderMigrationId()
+            ITEM_TYPE_APPWIDGET ->
+                // mProvider is the app the widget belongs to and appWidgetId it's the unique
+                // is of the widget, we need both because if you remove a widget and then add it
+                // again, then it can change and the WidgetProvider would not know the widget.
+                return mProvider + appWidgetId
+            ITEM_TYPE_APPLICATION -> {
+                val intentStr = mIntent?.let { cleanIntentString(it) }
+                try {
+                    val i = Intent.parseUri(intentStr, 0)
+                    return Objects.requireNonNull(i.component).toString()
+                } catch (e: Exception) {
+                    return intentStr
+                }
+            }
+
+            else -> return mIntent?.let { cleanIntentString(it) }
+        }
+    }
+
+    /**
+     * This method should return an id that should be the same for two folders containing the same
+     * elements.
+     */
+    private fun getFolderMigrationId(): String =
+        mFolderItems.keys
+            .map { intentString: String ->
+                mFolderItems[intentString]?.size.toString() + cleanIntentString(intentString)
+            }
+            .sorted()
+            .joinToString(",")
+
+    /**
+     * This is needed because sourceBounds can change and make the id of two equal items different.
+     */
+    private fun cleanIntentString(intentStr: String): String {
+        try {
+            return Intent.parseUri(intentStr, 0).apply { sourceBounds = null }.toURI()
+        } catch (e: URISyntaxException) {
+            Log.e(TAG, "Unable to parse Intent string", e)
+            return intentStr
+        }
+    }
+
+    companion object {
+        private const val TAG = "DbEntry"
+    }
+}
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 729b381..90af215 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -156,10 +156,16 @@
     }
 
     public Integer getColumns() {
+        if (TextUtils.isEmpty(mGridSizeString)) {
+            return -1;
+        }
         return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[0]));
     }
 
     public Integer getRows() {
+        if (TextUtils.isEmpty(mGridSizeString)) {
+            return -1;
+        }
         return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[1]));
     }
 
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 9531d5b..bad7577 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -328,10 +328,9 @@
             } else {
                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
             }
-            newId = helper.generateNewItemId();
-            while (idsInUse.contains(newId)) {
+            do {
                 newId = helper.generateNewItemId();
-            }
+            } while (idsInUse.contains(newId));
             values.put(LauncherSettings.Favorites._ID, newId);
             helper.getWritableDatabase().insert(destTableName, null, values);
         }
@@ -453,7 +452,7 @@
         final Context mContext;
         int mLastScreenId = -1;
 
-        final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
+        final Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
                 new ArrayMap<>();
 
         public DbReader(SQLiteDatabase db, String tableName, Context context) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.java b/src/com/android/launcher3/model/GridSizeMigrationLogic.java
deleted file mode 100644
index 12a14b2..0000000
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.java
+++ /dev/null
@@ -1,466 +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.model;
-
-import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
-import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
-import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
-import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
-import static com.android.launcher3.model.GridSizeMigrationDBController.copyCurrentGridToNewGrid;
-import static com.android.launcher3.model.GridSizeMigrationDBController.insertEntryInDb;
-import static com.android.launcher3.model.GridSizeMigrationDBController.needsToMigrate;
-import static com.android.launcher3.model.GridSizeMigrationDBController.removeEntryFromDb;
-import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
-import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
-import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Point;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.Flags;
-import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.provider.LauncherDbUtils;
-import com.android.launcher3.util.CellAndSpan;
-import com.android.launcher3.util.GridOccupancy;
-import com.android.launcher3.util.IntArray;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-public class GridSizeMigrationLogic {
-
-    private static final String TAG = "GridSizeMigrationLogic";
-    private static final boolean DEBUG = true;
-
-    /**
-     * Migrates the grid size from srcDeviceState to destDeviceState and make those changes
-     * in the target DB, using the source DB to determine what to add/remove/move/resize
-     * in the destination DB.
-     */
-    public void migrateGrid(
-            @NonNull Context context,
-            @NonNull DeviceGridState srcDeviceState,
-            @NonNull DeviceGridState destDeviceState,
-            @NonNull DatabaseHelper target,
-            @NonNull SQLiteDatabase source) {
-        if (!needsToMigrate(srcDeviceState, destDeviceState)) {
-            return;
-        }
-
-        boolean isFirstLoad = LauncherPrefs.get(context).get(IS_FIRST_LOAD_AFTER_RESTORE);
-        Log.d(TAG, "Begin grid migration. First load: " + isFirstLoad);
-
-        // 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(isFirstLoad, srcDeviceState, destDeviceState)) {
-            copyCurrentGridToNewGrid(context, destDeviceState, target, source);
-            return;
-        }
-        copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
-
-        long migrationStartTime = System.currentTimeMillis();
-        try (LauncherDbUtils.SQLiteTransaction t =
-                     new LauncherDbUtils.SQLiteTransaction(target.getWritableDatabase())) {
-            GridSizeMigrationDBController.DbReader srcReader = new GridSizeMigrationDBController
-                    .DbReader(t.getDb(), TMP_TABLE, context);
-            GridSizeMigrationDBController.DbReader destReader =
-                    new GridSizeMigrationDBController.DbReader(
-                            t.getDb(), TABLE_NAME, context);
-
-            Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows());
-            // Migrate hotseat.
-            migrateHotseat(destDeviceState.getNumHotseat(), srcReader, destReader, target);
-            // Migrate workspace.
-            migrateWorkspace(srcReader, destReader, target, targetSize);
-
-            dropTable(t.getDb(), TMP_TABLE);
-            t.commit();
-        } catch (Exception e) {
-            Log.e(TAG, "Error during grid migration", e);
-        } finally {
-            Log.v(TAG, "Workspace migration completed in "
-                    + (System.currentTimeMillis() - migrationStartTime));
-
-            // Save current configuration, so that the migration does not run again.
-            destDeviceState.writeToPrefs(context);
-        }
-    }
-
-    private void migrateHotseat(int destHotseatSize,
-            GridSizeMigrationDBController.DbReader srcReader,
-            GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper) {
-        final List<DbEntry> srcHotseatItems =
-                srcReader.loadHotseatEntries();
-        final List<DbEntry> dstHotseatItems =
-                destReader.loadHotseatEntries();
-
-
-        final List<DbEntry> hotseatToBeAdded =
-                getItemsToBeAdded(srcHotseatItems, dstHotseatItems);
-        final IntArray toBeRemoved = new IntArray();
-        toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems));
-
-        if (DEBUG) {
-            Log.d(TAG, "Start hotseat migration:"
-                    + "\n Removing Hotseat Items:"
-                    + dstHotseatItems.stream().filter(entry -> toBeRemoved
-                    .contains(entry.id)).map(DbEntry::toString)
-                    .collect(Collectors.joining(",\n", "[", "]"))
-                    + "\n Adding Hotseat Items:"
-                    + hotseatToBeAdded.stream().map(DbEntry::toString)
-                    .collect(Collectors.joining(",\n", "[", "]"))
-            );
-        }
-
-        // Removes the items that we need to remove from the destination DB.
-        if (!toBeRemoved.isEmpty()) {
-            removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
-        }
-
-        placeHotseatItems(
-                hotseatToBeAdded, dstHotseatItems, destHotseatSize, helper, srcReader, destReader);
-    }
-
-    private void placeHotseatItems(List<DbEntry> hotseatToBeAdded,
-            List<DbEntry> dstHotseatItems, int destHotseatSize,
-            DatabaseHelper helper, GridSizeMigrationDBController.DbReader srcReader,
-            GridSizeMigrationDBController.DbReader destReader) {
-        if (hotseatToBeAdded.isEmpty()) {
-            return;
-        }
-
-        List<Integer> idsInUse = dstHotseatItems.stream().map(entry -> entry.id).toList();
-
-        Collections.sort(hotseatToBeAdded);
-
-        List<DbEntry> placementSolutionHotseat =
-                solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded);
-        for (DbEntry entryToPlace: placementSolutionHotseat) {
-            insertEntryInDb(helper, entryToPlace, srcReader.mTableName, destReader.mTableName,
-                    idsInUse);
-        }
-    }
-
-    private void migrateWorkspace(GridSizeMigrationDBController.DbReader srcReader,
-            GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper,
-            Point targetSize) {
-
-
-        final List<DbEntry> srcWorkspaceItems =
-                srcReader.loadAllWorkspaceEntries();
-
-        final List<DbEntry> dstWorkspaceItems =
-                destReader.loadAllWorkspaceEntries();
-
-        final IntArray toBeRemoved = new IntArray();
-
-        List<DbEntry> workspaceToBeAdded =
-                getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems);
-        toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems));
-
-        if (DEBUG) {
-            Log.d(TAG, "Start workspace migration:"
-                    + "\n Source Device:"
-                    + srcWorkspaceItems.stream().map(
-                            DbEntry::toString)
-                    .collect(Collectors.joining(",\n", "[", "]"))
-                    + "\n Target Device:"
-                    + dstWorkspaceItems.stream().map(
-                            DbEntry::toString)
-                    .collect(Collectors.joining(",\n", "[", "]"))
-                    + "\n Removing Workspace Items:"
-                    + dstWorkspaceItems.stream().filter(entry -> toBeRemoved
-                            .contains(entry.id)).map(
-                            DbEntry::toString)
-                    .collect(Collectors.joining(",\n", "[", "]"))
-                    + "\n Adding Workspace Items:"
-                    + workspaceToBeAdded.stream().map(
-                            DbEntry::toString)
-                    .collect(Collectors.joining(",\n", "[", "]"))
-            );
-        }
-
-        // Removes the items that we need to remove from the destination DB.
-        if (!toBeRemoved.isEmpty()) {
-            removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
-        }
-
-        placeWorkspaceItems(workspaceToBeAdded, dstWorkspaceItems, targetSize.x, targetSize.y,
-                helper, srcReader, destReader);
-    }
-
-    private void placeWorkspaceItems(
-            List<DbEntry> workspaceToBeAdded,
-            List<DbEntry> dstWorkspaceItems,
-            int trgX, int trgY, DatabaseHelper helper,
-            GridSizeMigrationDBController.DbReader srcReader,
-            GridSizeMigrationDBController.DbReader destReader) {
-        if (workspaceToBeAdded.isEmpty()) {
-            return;
-        }
-
-        List<Integer> idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect(
-                Collectors.toList());
-
-        Collections.sort(workspaceToBeAdded);
-
-
-        // First we create a collection of the screens
-        List<Integer> screens = new ArrayList<>();
-        for (int screenId = 0; screenId <= destReader.mLastScreenId; screenId++) {
-            screens.add(screenId);
-        }
-
-        // Then we place the items on the screens
-        WorkspaceItemsToPlace itemsToPlace =
-                new WorkspaceItemsToPlace(workspaceToBeAdded);
-        for (int screenId : screens) {
-            if (DEBUG) {
-                Log.d(TAG, "Migrating " + screenId);
-            }
-            itemsToPlace = solveGridPlacement(
-                    destReader.mContext, screenId, trgX, trgY, itemsToPlace.mRemainingItemsToPlace,
-                    destReader.mWorkspaceEntriesByScreenId.get(screenId));
-            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse);
-            while (!itemsToPlace.mPlacementSolution.isEmpty()) {
-                insertEntryInDb(helper, itemsToPlace.mPlacementSolution.remove(0),
-                        srcReader.mTableName, destReader.mTableName, idsInUse);
-            }
-            if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
-                break;
-            }
-        }
-
-        // In case the new grid is smaller, there might be some leftover items that don't fit on
-        // any of the screens, in this case we add them to new screens until all of them are placed.
-        int screenId = destReader.mLastScreenId + 1;
-        while (!itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
-            itemsToPlace = solveGridPlacement(destReader.mContext, screenId,
-                    trgX, trgY, itemsToPlace.mRemainingItemsToPlace,
-                    destReader.mWorkspaceEntriesByScreenId.get(screenId));
-            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse);
-            screenId++;
-        }
-    }
-
-    private void placeItems(WorkspaceItemsToPlace itemsToPlace, DatabaseHelper helper,
-            GridSizeMigrationDBController.DbReader srcReader,
-            GridSizeMigrationDBController.DbReader destReader, List<Integer> idsInUse) {
-        while (!itemsToPlace.mPlacementSolution.isEmpty()) {
-            insertEntryInDb(helper, itemsToPlace.mPlacementSolution.remove(0),
-                    srcReader.mTableName, destReader.mTableName, idsInUse);
-        }
-    }
-
-
-    /**
-     * Only migrate the grid in this manner if the target grid is taller and not wider.
-     */
-    private boolean shouldMigrateToStrictlyTallerGrid(boolean isFirstLoad,
-            @NonNull DeviceGridState srcDeviceState,
-            @NonNull DeviceGridState destDeviceState) {
-        if (isFirstLoad
-                && Flags.enableGridMigrationFix()
-                && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
-                && srcDeviceState.getRows() < destDeviceState.getRows()) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Finds all the items that are in the old grid which aren't in the new grid, meaning they
-     * need to be added to the new grid.
-     *
-     * @return a list of DbEntry's which we need to add.
-     */
-    private List<DbEntry> getItemsToBeAdded(
-            @NonNull final List<DbEntry> src,
-            @NonNull final List<DbEntry> dest) {
-        Map<DbEntry, Integer> entryCountDiff =
-                calcDiff(src, dest);
-        List<DbEntry> toBeAdded = new ArrayList<>();
-        src.forEach(entry -> {
-            if (entryCountDiff.get(entry) > 0) {
-                toBeAdded.add(entry);
-                entryCountDiff.put(entry, entryCountDiff.get(entry) - 1);
-            }
-        });
-        return toBeAdded;
-    }
-
-    /**
-     * Finds all the items that are in the new grid which aren't in the old grid, meaning they
-     * need to be removed from the new grid.
-     *
-     * @return an IntArray of item id's which we need to remove.
-     */
-    private IntArray getItemsToBeRemoved(
-            @NonNull final List<DbEntry> src,
-            @NonNull final List<DbEntry> dest) {
-        Map<DbEntry, Integer> entryCountDiff =
-                calcDiff(src, dest);
-        IntArray toBeRemoved = new IntArray();
-        dest.forEach(entry -> {
-            if (entryCountDiff.get(entry) < 0) {
-                toBeRemoved.add(entry.id);
-                if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
-                    entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
-                }
-                entryCountDiff.put(entry, entryCountDiff.get(entry) + 1);
-            }
-        });
-        return toBeRemoved;
-    }
-
-    /**
-     * Calculates the difference between the old and new grid items in terms of how many of each
-     * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the
-     * difference there would be 2. While if the old grid has 0 Calculator icons and the
-     * new grid has 1, then the difference would be -1.
-     *
-     * @return a Map with each DbEntry as a key and the count of said entry as the value.
-     */
-    private Map<DbEntry, Integer> calcDiff(
-            @NonNull final List<DbEntry> src,
-            @NonNull final List<DbEntry> dest) {
-        Map<DbEntry, Integer> entryCountDiff = new HashMap<>();
-        src.forEach(entry ->
-                entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) + 1));
-        dest.forEach(entry ->
-                entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) - 1));
-        return entryCountDiff;
-    }
-
-    private List<DbEntry> solveHotseatPlacement(final int hotseatSize,
-            @NonNull final List<DbEntry> placedHotseatItems,
-            @NonNull final List<DbEntry> itemsToPlace) {
-        List<DbEntry> placementSolution = new ArrayList<>();
-        List<DbEntry> remainingItemsToPlace =
-                new ArrayList<>(itemsToPlace);
-        final boolean[] occupied = new boolean[hotseatSize];
-        for (DbEntry entry : placedHotseatItems) {
-            occupied[entry.screenId] = true;
-        }
-
-        for (int i = 0; i < occupied.length; i++) {
-            if (!occupied[i] && !remainingItemsToPlace.isEmpty()) {
-                DbEntry entry = remainingItemsToPlace.remove(0);
-                entry.screenId = i;
-                // These values does not affect the item position, but we should set them
-                // to something other than -1.
-                entry.cellX = i;
-                entry.cellY = 0;
-
-                placementSolution.add(entry);
-                occupied[entry.screenId] = true;
-            }
-        }
-        return placementSolution;
-    }
-
-    private WorkspaceItemsToPlace solveGridPlacement(
-            Context context,
-            final int screenId, final int trgX, final int trgY,
-            @NonNull final List<DbEntry> sortedItemsToPlace,
-            List<DbEntry> existedEntries) {
-        WorkspaceItemsToPlace itemsToPlace = new WorkspaceItemsToPlace(sortedItemsToPlace);
-        final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
-        final Point trg = new Point(trgX, trgY);
-        final Point next = new Point(0, screenId == 0
-                && (FeatureFlags.QSB_ON_FIRST_SCREEN
-                && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(context)
-                .getBoolean(SMARTSPACE_ON_HOME_SCREEN, true))
-                && !SHOULD_SHOW_FIRST_PAGE_WIDGET)
-                ? 1 /* smartspace */ : 0);
-        if (existedEntries != null) {
-            for (DbEntry entry : existedEntries) {
-                occupied.markCells(entry, true);
-            }
-        }
-        Iterator<DbEntry> iterator =
-                itemsToPlace.mRemainingItemsToPlace.iterator();
-        while (iterator.hasNext()) {
-            final DbEntry entry = iterator.next();
-            if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
-                iterator.remove();
-                continue;
-            }
-            CellAndSpan placement = findPlacementForEntry(
-                    entry, next.x, next.y, trg, occupied);
-            if (placement != null) {
-                entry.screenId = screenId;
-                entry.cellX = placement.cellX;
-                entry.cellY = placement.cellY;
-                entry.spanX = placement.spanX;
-                entry.spanY = placement.spanY;
-                occupied.markCells(entry, true);
-                next.set(entry.cellX + entry.spanX, entry.cellY);
-                itemsToPlace.mPlacementSolution.add(entry);
-                iterator.remove();
-            }
-        }
-        return itemsToPlace;
-    }
-
-    /**
-     * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as
-     * a memoization of last placement, we can start our search for next placement from there
-     * to speed up the search.
-     *
-     * @return NewEntryPlacement object if we found a valid placement, null if we didn't.
-     */
-    private CellAndSpan findPlacementForEntry(
-            @NonNull final DbEntry entry,
-            int startPosX, int startPosY, @NonNull final Point trg,
-            @NonNull final GridOccupancy occupied) {
-        for (int y = startPosY; y <  trg.y; y++) {
-            for (int x = startPosX; x < trg.x; x++) {
-                boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY);
-                if (minFits) {
-                    return (new CellAndSpan(x, y, entry.minSpanX, entry.minSpanY));
-                }
-            }
-            startPosX = 0;
-        }
-        return null;
-    }
-
-    private static class WorkspaceItemsToPlace {
-        List<DbEntry> mRemainingItemsToPlace;
-        List<DbEntry> mPlacementSolution;
-
-        WorkspaceItemsToPlace(List<DbEntry> sortedItemsToPlace) {
-            mRemainingItemsToPlace = new ArrayList<>(sortedItemsToPlace);
-            mPlacementSolution = new ArrayList<>();
-        }
-
-    }
-}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
new file mode 100644
index 0000000..9470abf
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -0,0 +1,524 @@
+/*
+ * 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.model
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.get
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
+import com.android.launcher3.provider.LauncherDbUtils
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.util.GridOccupancy
+import com.android.launcher3.util.IntArray
+
+class GridSizeMigrationLogic {
+    /**
+     * Migrates the grid size from srcDeviceState to destDeviceState and make those changes in the
+     * target DB, using the source DB to determine what to add/remove/move/resize in the destination
+     * DB.
+     */
+    fun migrateGrid(
+        context: Context,
+        srcDeviceState: DeviceGridState,
+        destDeviceState: DeviceGridState,
+        target: DatabaseHelper,
+        source: SQLiteDatabase,
+    ) {
+        if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
+            return
+        }
+
+        val isFirstLoad = get(context).get(LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE)
+        Log.d(TAG, "Begin grid migration. First load: $isFirstLoad")
+
+        // 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(isFirstLoad, srcDeviceState, destDeviceState)) {
+            GridSizeMigrationDBController.copyCurrentGridToNewGrid(
+                context,
+                destDeviceState,
+                target,
+                source,
+            )
+            return
+        }
+        LauncherDbUtils.copyTable(
+            source,
+            LauncherSettings.Favorites.TABLE_NAME,
+            target.writableDatabase,
+            LauncherSettings.Favorites.TMP_TABLE,
+            context,
+        )
+
+        val migrationStartTime = System.currentTimeMillis()
+        try {
+            SQLiteTransaction(target.writableDatabase).use { t ->
+                val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context)
+                val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context)
+
+                val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
+
+                // Here we keep all the DB ids we have in the destination DB such that we don't
+                // assign
+                // an item that we want to add to the destination DB the same id as an already
+                // existing
+                // item.
+                val idsInUse = mutableListOf<Int>()
+
+                // Migrate hotseat.
+                migrateHotseat(destDeviceState.numHotseat, srcReader, destReader, target, idsInUse)
+                // Migrate workspace.
+                migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
+
+                LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
+                t.commit()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Error during grid migration", e)
+        } finally {
+            Log.v(
+                TAG,
+                "Workspace migration completed in " +
+                    (System.currentTimeMillis() - migrationStartTime),
+            )
+
+            // Save current configuration, so that the migration does not run again.
+            destDeviceState.writeToPrefs(context)
+        }
+    }
+
+    /** Handles hotseat migration. */
+    @VisibleForTesting
+    fun migrateHotseat(
+        destHotseatSize: Int,
+        srcReader: DbReader,
+        destReader: DbReader,
+        helper: DatabaseHelper,
+        idsInUse: MutableList<Int>,
+    ) {
+        val srcHotseatItems = srcReader.loadHotseatEntries()
+        val dstHotseatItems = destReader.loadHotseatEntries()
+
+        val hotseatToBeAdded = getItemsToBeAdded(srcHotseatItems, dstHotseatItems)
+        val toBeRemoved = IntArray()
+        toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems))
+
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                """Start hotseat migration:
+            |Removing Hotseat Items: [${dstHotseatItems.filter { toBeRemoved.contains(it.id) }
+                .joinToString(",\n") { it.toString() }}]
+            |Adding Hotseat Items: [${hotseatToBeAdded
+                .joinToString(",\n") { it.toString() }}]
+            |"""
+                    .trimMargin(),
+            )
+        }
+
+        // Removes the items that we need to remove from the destination DB.
+        if (!toBeRemoved.isEmpty) {
+            GridSizeMigrationDBController.removeEntryFromDb(
+                destReader.mDb,
+                destReader.mTableName,
+                toBeRemoved,
+            )
+        }
+
+        placeHotseatItems(
+            hotseatToBeAdded,
+            dstHotseatItems,
+            destHotseatSize,
+            helper,
+            srcReader,
+            destReader,
+            idsInUse,
+        )
+    }
+
+    private fun placeHotseatItems(
+        hotseatToBeAdded: MutableList<DbEntry>,
+        dstHotseatItems: List<DbEntry>,
+        destHotseatSize: Int,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: MutableList<Int>,
+    ) {
+        if (hotseatToBeAdded.isEmpty()) {
+            return
+        }
+
+        idsInUse.addAll(dstHotseatItems.map { entry: DbEntry -> entry.id })
+
+        hotseatToBeAdded.sort()
+
+        val placementSolutionHotseat =
+            solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded)
+        for (entryToPlace in placementSolutionHotseat) {
+            GridSizeMigrationDBController.insertEntryInDb(
+                helper,
+                entryToPlace,
+                srcReader.mTableName,
+                destReader.mTableName,
+                idsInUse,
+            )
+        }
+    }
+
+    @VisibleForTesting
+    fun migrateWorkspace(
+        srcReader: DbReader,
+        destReader: DbReader,
+        helper: DatabaseHelper,
+        targetSize: Point,
+        idsInUse: MutableList<Int>,
+    ) {
+        val srcWorkspaceItems = srcReader.loadAllWorkspaceEntries()
+
+        val dstWorkspaceItems = destReader.loadAllWorkspaceEntries()
+
+        val toBeRemoved = IntArray()
+
+        val workspaceToBeAdded = getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems)
+        toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems))
+
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                """Start workspace migration:
+            |Source Device: [${srcWorkspaceItems.joinToString(",\n") { it.toString() }}]
+            |Target Device: [${dstWorkspaceItems.joinToString(",\n") { it.toString() }}]
+            |Removing Workspace Items: [${dstWorkspaceItems.filter { toBeRemoved.contains(it.id) }
+                .joinToString(",\n") { it.toString() }}]
+            |Adding Workspace Items: [${workspaceToBeAdded
+                .joinToString(",\n") { it.toString() }}]
+            |"""
+                    .trimMargin(),
+            )
+        }
+
+        // Removes the items that we need to remove from the destination DB.
+        if (!toBeRemoved.isEmpty) {
+            GridSizeMigrationDBController.removeEntryFromDb(
+                destReader.mDb,
+                destReader.mTableName,
+                toBeRemoved,
+            )
+        }
+
+        placeWorkspaceItems(
+            workspaceToBeAdded,
+            dstWorkspaceItems,
+            targetSize.x,
+            targetSize.y,
+            helper,
+            srcReader,
+            destReader,
+            idsInUse,
+        )
+    }
+
+    private fun placeWorkspaceItems(
+        workspaceToBeAdded: MutableList<DbEntry>,
+        dstWorkspaceItems: List<DbEntry>,
+        trgX: Int,
+        trgY: Int,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: MutableList<Int>,
+    ) {
+        if (workspaceToBeAdded.isEmpty()) {
+            return
+        }
+
+        idsInUse.addAll(dstWorkspaceItems.map { entry: DbEntry -> entry.id })
+
+        workspaceToBeAdded.sort()
+
+        // First we create a collection of the screens
+        val screens: MutableList<Int> = ArrayList()
+        for (screenId in 0..destReader.mLastScreenId) {
+            screens.add(screenId)
+        }
+
+        // Then we place the items on the screens
+        var itemsToPlace = WorkspaceItemsToPlace(workspaceToBeAdded, mutableListOf())
+        for (screenId in screens) {
+            if (DEBUG) {
+                Log.d(TAG, "Migrating $screenId")
+            }
+            itemsToPlace =
+                solveGridPlacement(
+                    destReader.mContext,
+                    screenId,
+                    trgX,
+                    trgY,
+                    itemsToPlace.mRemainingItemsToPlace,
+                    destReader.mWorkspaceEntriesByScreenId[screenId],
+                )
+            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+            while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+                GridSizeMigrationDBController.insertEntryInDb(
+                    helper,
+                    itemsToPlace.mPlacementSolution.removeAt(0),
+                    srcReader.mTableName,
+                    destReader.mTableName,
+                    idsInUse,
+                )
+            }
+            if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
+                break
+            }
+        }
+
+        // In case the new grid is smaller, there might be some leftover items that don't fit on
+        // any of the screens, in this case we add them to new screens until all of them are placed.
+        var screenId = destReader.mLastScreenId + 1
+        while (itemsToPlace.mRemainingItemsToPlace.isNotEmpty()) {
+            itemsToPlace =
+                solveGridPlacement(
+                    destReader.mContext,
+                    screenId,
+                    trgX,
+                    trgY,
+                    itemsToPlace.mRemainingItemsToPlace,
+                    destReader.mWorkspaceEntriesByScreenId[screenId],
+                )
+            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+            screenId++
+        }
+    }
+
+    private fun placeItems(
+        itemsToPlace: WorkspaceItemsToPlace,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: List<Int>,
+    ) {
+        while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+            GridSizeMigrationDBController.insertEntryInDb(
+                helper,
+                itemsToPlace.mPlacementSolution.removeAt(0),
+                srcReader.mTableName,
+                destReader.mTableName,
+                idsInUse,
+            )
+        }
+    }
+
+    /** Only migrate the grid in this manner if the target grid is taller and not wider. */
+    private fun shouldMigrateToStrictlyTallerGrid(
+        isFirstLoad: Boolean,
+        srcDeviceState: DeviceGridState,
+        destDeviceState: DeviceGridState,
+    ): Boolean {
+        return (isFirstLoad && Flags.enableGridMigrationFix()) &&
+            srcDeviceState.columns == destDeviceState.columns &&
+            srcDeviceState.rows < destDeviceState.rows
+    }
+
+    /**
+     * Finds all the items that are in the old grid which aren't in the new grid, meaning they need
+     * to be added to the new grid.
+     *
+     * @return a list of DbEntry's which we need to add.
+     */
+    private fun getItemsToBeAdded(src: List<DbEntry>, dest: List<DbEntry>): MutableList<DbEntry> {
+        val entryCountDiff = calcDiff(src, dest)
+        val toBeAdded: MutableList<DbEntry> = ArrayList()
+        src.forEach { entry ->
+            entryCountDiff[entry]?.let { entryDiff ->
+                if (entryDiff > 0) {
+                    toBeAdded.add(entry)
+                    entryCountDiff[entry] = entryDiff - 1
+                }
+            }
+        }
+        return toBeAdded
+    }
+
+    /**
+     * Finds all the items that are in the new grid which aren't in the old grid, meaning they need
+     * to be removed from the new grid.
+     *
+     * @return an IntArray of item id's which we need to remove.
+     */
+    private fun getItemsToBeRemoved(src: List<DbEntry>, dest: List<DbEntry>): IntArray {
+        val entryCountDiff = calcDiff(src, dest)
+        val toBeRemoved =
+            IntArray().apply {
+                dest.forEach { entry ->
+                    entryCountDiff[entry]?.let { entryDiff ->
+                        if (entryDiff < 0) {
+                            add(entry.id)
+                            if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                                entry.mFolderItems.values.forEach { ids ->
+                                    ids.forEach { value -> add(value) }
+                                }
+                            }
+                        }
+                        entryCountDiff[entry] = entryDiff.plus(1)
+                    }
+                }
+            }
+        return toBeRemoved
+    }
+
+    /**
+     * Calculates the difference between the old and new grid items in terms of how many of each
+     * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the
+     * difference there would be 2. While if the old grid has 0 Calculator icons and the new grid
+     * has 1, then the difference would be -1.
+     *
+     * @return a Map with each DbEntry as a key and the count of said entry as the value.
+     */
+    private fun calcDiff(src: List<DbEntry>, dest: List<DbEntry>): MutableMap<DbEntry, Int> {
+        val entryCountDiff: MutableMap<DbEntry, Int> = HashMap()
+        src.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) + 1 }
+        dest.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) - 1 }
+        return entryCountDiff
+    }
+
+    private fun solveHotseatPlacement(
+        hotseatSize: Int,
+        placedHotseatItems: List<DbEntry>,
+        itemsToPlace: List<DbEntry>,
+    ): List<DbEntry> {
+        val placementSolution: MutableList<DbEntry> = ArrayList()
+        val remainingItemsToPlace: MutableList<DbEntry> = ArrayList(itemsToPlace)
+        val occupied = BooleanArray(hotseatSize)
+        for (entry in placedHotseatItems) {
+            occupied[entry.screenId] = true
+        }
+
+        for (i in occupied.indices) {
+            if (!occupied[i] && remainingItemsToPlace.isNotEmpty()) {
+                val entry: DbEntry =
+                    remainingItemsToPlace.removeAt(0).apply {
+                        screenId = i
+                        // These values does not affect the item position, but we should set them
+                        // to something other than -1.
+                        cellX = i
+                        cellY = 0
+                    }
+                placementSolution.add(entry)
+                occupied[entry.screenId] = true
+            }
+        }
+        return placementSolution
+    }
+
+    private fun solveGridPlacement(
+        context: Context,
+        screenId: Int,
+        trgX: Int,
+        trgY: Int,
+        sortedItemsToPlace: MutableList<DbEntry>,
+        existedEntries: MutableList<DbEntry>?,
+    ): WorkspaceItemsToPlace {
+        val itemsToPlace = WorkspaceItemsToPlace(sortedItemsToPlace, mutableListOf())
+        val occupied = GridOccupancy(trgX, trgY)
+        val trg = Point(trgX, trgY)
+        val next: Point =
+            if (
+                screenId == 0 &&
+                    (FeatureFlags.QSB_ON_FIRST_SCREEN &&
+                        (!Flags.enableSmartspaceRemovalToggle() ||
+                            getPrefs(context)
+                                .getBoolean(LoaderTask.SMARTSPACE_ON_HOME_SCREEN, true)) &&
+                        !Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET)
+            ) {
+                Point(0, 1 /* smartspace */)
+            } else {
+                Point(0, 0)
+            }
+        if (existedEntries != null) {
+            for (entry in existedEntries) {
+                occupied.markCells(entry, true)
+            }
+        }
+        val iterator = itemsToPlace.mRemainingItemsToPlace.iterator()
+        while (iterator.hasNext()) {
+            val entry = iterator.next()
+            if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
+                iterator.remove()
+                continue
+            }
+            findPlacementForEntry(entry, next.x, next.y, trg, occupied)?.let {
+                entry.screenId = screenId
+                entry.cellX = it.cellX
+                entry.cellY = it.cellY
+                entry.spanX = it.spanX
+                entry.spanY = it.spanY
+                occupied.markCells(entry, true)
+                next[entry.cellX + entry.spanX] = entry.cellY
+                itemsToPlace.mPlacementSolution.add(entry)
+                iterator.remove()
+            }
+        }
+        return itemsToPlace
+    }
+
+    /**
+     * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as a
+     * memoization of last placement, we can start our search for next placement from there to speed
+     * up the search.
+     *
+     * @return NewEntryPlacement object if we found a valid placement, null if we didn't.
+     */
+    private fun findPlacementForEntry(
+        entry: DbEntry,
+        startPosX: Int,
+        startPosY: Int,
+        trg: Point,
+        occupied: GridOccupancy,
+    ): CellAndSpan? {
+        var newStartPosX = startPosX
+        for (y in startPosY until trg.y) {
+            for (x in newStartPosX until trg.x) {
+                if (occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY)) {
+                    return (CellAndSpan(x, y, entry.minSpanX, entry.minSpanY))
+                }
+            }
+            newStartPosX = 0
+        }
+        return null
+    }
+
+    private data class WorkspaceItemsToPlace(
+        val mRemainingItemsToPlace: MutableList<DbEntry>,
+        val mPlacementSolution: MutableList<DbEntry>,
+    )
+
+    companion object {
+        private const val TAG = "GridSizeMigrationLogic"
+        private const val DEBUG = true
+    }
+}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index b0108c2..06d8b59 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -287,11 +287,6 @@
             }
             logASplit("loadAllApps");
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                logASplit("allAppsDelegateItems");
-            }
             verifyNotStopped();
             mLauncherBinder.bindAllApps();
             logASplit("bindAllApps");
@@ -356,12 +351,6 @@
                 prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(true));
             }
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                logASplit("otherDelegateItems");
-                verifyNotStopped();
-            }
-
             updateHandler.updateIcons(allWidgetsList,
                     CachedObjectCachingLogic.INSTANCE,
                     mApp.getModel()::onWidgetLabelsUpdated);
@@ -413,13 +402,6 @@
         }
         logASplit("loadWorkspace");
 
-        if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-            verifyNotStopped();
-            mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-            mModelDelegate.markActive();
-            logASplit("workspaceDelegateItems");
-        }
         mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN
                 && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(
                 mApp.getContext()).getBoolean(SMARTSPACE_ON_HOME_SCREEN, true));
@@ -490,14 +472,12 @@
                 IOUtils.closeSilently(c);
             }
 
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                mModelDelegate.markActive();
-            }
+            mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
+                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+            mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
+                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+            mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
+            mModelDelegate.markActive();
 
             // Break early if we've stopped loading
             if (mStopped) {
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 4f0f162..094798b 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -20,6 +20,7 @@
 import static android.util.Base64.NO_WRAP;
 
 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
+import static com.android.launcher3.LauncherPrefs.DB_FILE;
 import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
@@ -128,16 +129,20 @@
 
     private synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = createDatabaseHelper(false /* forMigration */);
+            String dbFile = LauncherPrefs.get(mContext).get(DB_FILE);
+            if (dbFile.isEmpty()) {
+                dbFile = InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+            }
+            mOpenHelper = createDatabaseHelper(false /* forMigration */, dbFile);
             printDBs("before: ");
             RestoreDbTask.restoreIfNeeded(mContext, this);
             printDBs("after: ");
         }
     }
 
-    protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
+    protected DatabaseHelper createDatabaseHelper(boolean forMigration, String dbFile) {
         boolean isSandbox = mContext instanceof SandboxContext;
-        String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+        String dbName = isSandbox ? null : dbFile;
 
         // Set the flag for empty DB
         Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
@@ -325,7 +330,7 @@
     private boolean isThereExistingDb() {
         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
             // If we already have a new DB, ignore migration
-            Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
+            FileLog.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
             return true;
         }
         return false;
@@ -336,7 +341,7 @@
         if (GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
             return true;
         }
-        Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+        FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
         return false;
     }
 
@@ -344,7 +349,7 @@
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
         String targetDbName = new DeviceGridState(idp).getDbFile();
         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
-            Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
+            FileLog.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
             return true;
         }
         return false;
@@ -364,7 +369,7 @@
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
         DatabaseHelper oldHelper = mOpenHelper;
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
-                : createDatabaseHelper(true /* forMigration */);
+                : createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
         try {
             // This is the current grid we have, given by the mContext
             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
@@ -388,7 +393,6 @@
      * Migrates the DB if needed. If the migration failed, it clears the DB.
      */
     public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
-
         if (!migrateGridIfNeeded()) {
             if (restoreEventLogger != null) {
                 if (LauncherPrefs.get(mContext).get(NO_DB_FILES_RESTORED)) {
@@ -428,7 +432,7 @@
         }
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
         if (!GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
-            Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+            FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
             return true;
         }
         String targetDbName = new DeviceGridState(idp).getDbFile();
@@ -438,7 +442,7 @@
         }
         DatabaseHelper oldHelper = mOpenHelper;
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
-                : createDatabaseHelper(true /* forMigration */);
+                : createDatabaseHelper(true /* forMigration */, targetDbName);
         try {
             // This is the current grid we have, given by the mContext
             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
deleted file mode 100644
index 3ae643e..0000000
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2016 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.provider;
-
-import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
-import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ShortcutInfo;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Icon;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserManager;
-import android.text.TextUtils;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.model.LoaderCursor;
-import com.android.launcher3.model.UserManagerState;
-import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.PackageManagerHelper;
-
-/**
- * A set of utility methods for Launcher DB used for DB updates and migration.
- */
-public class LauncherDbUtils {
-    /**
-     * Returns a string which can be used as a where clause for DB query to match the given itemId
-     */
-    public static String itemIdMatch(int itemId) {
-        return "_id=" + itemId;
-    }
-
-    public static IntArray queryIntArray(boolean distinct, SQLiteDatabase db, String tableName,
-            String columnName, String selection, String groupBy, String orderBy) {
-        IntArray out = new IntArray();
-        try (Cursor c = db.query(distinct, tableName, new String[] { columnName }, selection, null,
-                groupBy, null, orderBy, null)) {
-            while (c.moveToNext()) {
-                out.add(c.getInt(0));
-            }
-        }
-        return out;
-    }
-
-    public static boolean tableExists(SQLiteDatabase db, String tableName) {
-        try (Cursor c = db.query(true, "sqlite_master", new String[] {"tbl_name"},
-                "tbl_name = ?", new String[] {tableName},
-                null, null, null, null, null)) {
-            return c.getCount() > 0;
-        }
-    }
-
-    public static void dropTable(SQLiteDatabase db, String tableName) {
-        db.execSQL("DROP TABLE IF EXISTS " + tableName);
-    }
-
-    /** Copy fromTable in fromDb to toTable in toDb. */
-    public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
-            String toTable, Context context) {
-        long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
-                Process.myUserHandle());
-        dropTable(toDb, toTable);
-        Favorites.addTableToDb(toDb, userSerial, false, toTable);
-        if (fromDb != toDb) {
-            toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
-            toDb.execSQL(
-                    "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
-                        + " FROM from_db." + fromTable);
-            toDb.execSQL("DETACH DATABASE 'from_db'");
-        } else {
-            toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
-                    + fromTable);
-        }
-    }
-
-    /**
-     * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher.
-     * Removes any invalid shortcut or any shortcut which requires some permission to launch
-     */
-    public static void migrateLegacyShortcuts(Context context, SQLiteDatabase db) {
-        Cursor c = db.query(
-                Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null);
-        UserManagerState ums = new UserManagerState();
-        PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
-        ums.init(UserCache.INSTANCE.get(context),
-                context.getSystemService(UserManager.class));
-        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper,
-                null);
-        IntSet deletedShortcuts = new IntSet();
-
-        while (lc.moveToNext()) {
-            if (lc.user != Process.myUserHandle()) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            Intent intent = lc.parseIntent();
-            if (intent == null) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            if (TextUtils.isEmpty(lc.getTitle())) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-
-            // Make sure the target intent can be launched without any permissions. Otherwise remove
-            // the shortcut
-            ResolveInfo ri = context.getPackageManager().resolveActivity(intent, 0);
-            if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            PersistableBundle extras = new PersistableBundle();
-            extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, ri.activityInfo.packageName);
-            ShortcutInfo.Builder infoBuilder = new ShortcutInfo.Builder(
-                    context, "migrated_shortcut-" + lc.id)
-                    .setIntent(intent)
-                    .setExtras(extras)
-                    .setShortLabel(lc.getTitle());
-
-            Bitmap bitmap = null;
-            byte[] iconData = lc.getIconBlob();
-            if (iconData != null) {
-                bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
-            }
-            if (bitmap != null) {
-                infoBuilder.setIcon(Icon.createWithBitmap(bitmap));
-            }
-
-            ShortcutInfo info = infoBuilder.build();
-            try {
-                if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
-                    deletedShortcuts.add(lc.id);
-                    continue;
-                }
-            } catch (Exception e) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            ContentValues update = new ContentValues();
-            update.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_DEEP_SHORTCUT);
-            update.put(Favorites.INTENT,
-                    ShortcutKey.makeIntent(info.getId(), context.getPackageName()).toUri(0));
-            db.update(Favorites.TABLE_NAME, update, "_id = ?",
-                    new String[] {Integer.toString(lc.id)});
-        }
-        lc.close();
-        if (!deletedShortcuts.isEmpty()) {
-            db.delete(Favorites.TABLE_NAME,
-                    Utilities.createDbSelectionQuery(Favorites._ID, deletedShortcuts.getArray()),
-                    null);
-        }
-
-        // Drop the unused columns
-        db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconPackage;");
-        db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconResource;");
-    }
-
-    /**
-     * Utility class to simplify managing sqlite transactions
-     */
-    public static class SQLiteTransaction implements AutoCloseable {
-        private final SQLiteDatabase mDb;
-
-        public SQLiteTransaction(SQLiteDatabase db) {
-            mDb = db;
-            db.beginTransaction();
-        }
-
-        public void commit() {
-            mDb.setTransactionSuccessful();
-        }
-
-        @Override
-        public void close() {
-            mDb.endTransaction();
-        }
-
-        public SQLiteDatabase getDb() {
-            return mDb;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
new file mode 100644
index 0000000..3c68e46
--- /dev/null
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2016 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.provider
+
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import android.os.PersistableBundle
+import android.os.Process
+import android.os.UserManager
+import android.text.TextUtils
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.LoaderCursor
+import com.android.launcher3.model.UserManagerState
+import com.android.launcher3.pm.PinRequestHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.PackageManagerHelper
+
+/** A set of utility methods for Launcher DB used for DB updates and migration. */
+object LauncherDbUtils {
+    /**
+     * Returns a string which can be used as a where clause for DB query to match the given itemId
+     */
+    @JvmStatic fun itemIdMatch(itemId: Int): String = "_id=$itemId"
+
+    @JvmStatic
+    fun queryIntArray(
+        distinct: Boolean,
+        db: SQLiteDatabase,
+        tableName: String,
+        columnName: String,
+        selection: String?,
+        groupBy: String?,
+        orderBy: String?,
+    ): IntArray {
+        val out = IntArray()
+        db.query(
+                distinct,
+                tableName,
+                arrayOf(columnName),
+                selection,
+                null,
+                groupBy,
+                null,
+                orderBy,
+                null,
+            )
+            .use { c ->
+                while (c.moveToNext()) {
+                    out.add(c.getInt(0))
+                }
+            }
+        return out
+    }
+
+    @JvmStatic
+    fun tableExists(db: SQLiteDatabase, tableName: String): Boolean =
+        db.query(
+                /* distinct = */ true,
+                /* table = */ "sqlite_master",
+                /* columns = */ arrayOf("tbl_name"),
+                /* selection = */ "tbl_name = ?",
+                /* selectionArgs = */ arrayOf(tableName),
+                /* groupBy = */ null,
+                /* having = */ null,
+                /* orderBy = */ null,
+                /* limit = */ null,
+                /* cancellationSignal = */ null,
+            )
+            .use { c ->
+                return c.count > 0
+            }
+
+    @JvmStatic
+    fun dropTable(db: SQLiteDatabase, tableName: String) =
+        db.execSQL("DROP TABLE IF EXISTS $tableName")
+
+    /** Copy fromTable in fromDb to toTable in toDb. */
+    @JvmStatic
+    fun copyTable(
+        fromDb: SQLiteDatabase,
+        fromTable: String,
+        toDb: SQLiteDatabase,
+        toTable: String,
+        context: Context,
+    ) {
+        val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
+        dropTable(toDb, toTable)
+        LauncherSettings.Favorites.addTableToDb(toDb, userSerial, false, toTable)
+        if (fromDb != toDb) {
+            toDb.run {
+                execSQL("ATTACH DATABASE '${fromDb.path}' AS from_db")
+                execSQL(
+                    "INSERT INTO $toTable SELECT ${LauncherSettings.Favorites.getColumns(userSerial)} FROM from_db.$fromTable"
+                )
+                execSQL("DETACH DATABASE 'from_db'")
+            }
+        } else {
+            toDb.run {
+                execSQL(
+                    "INSERT INTO $toTable SELECT ${
+                        LauncherSettings.Favorites.getColumns(
+                            userSerial
+                        )
+                    } FROM $fromTable"
+                )
+            }
+        }
+    }
+
+    /**
+     * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
+     * shortcut or any shortcut which requires some permission to launch
+     */
+    @JvmStatic
+    fun migrateLegacyShortcuts(context: Context, db: SQLiteDatabase) {
+        val c =
+            db.query(
+                LauncherSettings.Favorites.TABLE_NAME,
+                null,
+                "itemType = 1",
+                null,
+                null,
+                null,
+                null,
+            )
+        val pmHelper = PackageManagerHelper.INSTANCE[context]
+        val ums = UserManagerState()
+        ums.run {
+            init(UserCache.INSTANCE[context], context.getSystemService(UserManager::class.java))
+        }
+        val lc = LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper, null)
+        val deletedShortcuts = IntSet()
+
+        while (lc.moveToNext()) {
+            if (lc.user !== Process.myUserHandle()) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val intent = lc.parseIntent()
+            if (intent == null) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            if (TextUtils.isEmpty(lc.title)) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+
+            // Make sure the target intent can be launched without any permissions. Otherwise remove
+            // the shortcut
+            val ri = context.packageManager.resolveActivity(intent, 0)
+            if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val extras =
+                PersistableBundle().apply {
+                    putString(
+                        IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE,
+                        ri.activityInfo.packageName,
+                    )
+                }
+            val infoBuilder =
+                ShortcutInfo.Builder(context, "migrated_shortcut-${lc.id}")
+                    .setIntent(intent)
+                    .setExtras(extras)
+                    .setShortLabel(lc.title)
+
+            var bitmap: Bitmap? = null
+            val iconData = lc.iconBlob
+            if (iconData != null) {
+                bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.size)
+            }
+            if (bitmap != null) {
+                infoBuilder.setIcon(Icon.createWithBitmap(bitmap))
+            }
+
+            val info = infoBuilder.build()
+            try {
+                if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
+                    deletedShortcuts.add(lc.id)
+                    continue
+                }
+            } catch (e: Exception) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val update =
+                ContentValues().apply {
+                    put(
+                        LauncherSettings.Favorites.ITEM_TYPE,
+                        LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+                    )
+                    put(
+                        LauncherSettings.Favorites.INTENT,
+                        ShortcutKey.makeIntent(info.id, context.packageName).toUri(0),
+                    )
+                }
+            db.update(
+                LauncherSettings.Favorites.TABLE_NAME,
+                update,
+                "_id = ?",
+                arrayOf(lc.id.toString()),
+            )
+        }
+        lc.close()
+        if (deletedShortcuts.isEmpty.not()) {
+            db.delete(
+                /* table = */ LauncherSettings.Favorites.TABLE_NAME,
+                /* whereClause = */ Utilities.createDbSelectionQuery(
+                    LauncherSettings.Favorites._ID,
+                    deletedShortcuts.array,
+                ),
+                /* whereArgs = */ null,
+            )
+        }
+
+        // Drop the unused columns
+        db.run {
+            execSQL("ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconPackage;")
+            execSQL(
+                "ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconResource;"
+            )
+        }
+    }
+
+    /** Utility class to simplify managing sqlite transactions */
+    class SQLiteTransaction(val db: SQLiteDatabase) : AutoCloseable {
+        init {
+            db.beginTransaction()
+        }
+
+        fun commit() = db.setTransactionSuccessful()
+
+        override fun close() = db.endTransaction()
+    }
+}
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 775d248..59c27af 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -125,7 +125,8 @@
         LauncherPrefs.get(context).removeSync(RESTORE_DEVICE);
 
         if (Flags.enableNarrowGridRestore()) {
-            String oldPhoneFileName = idp.dbFile;
+            DeviceGridState deviceGridState = new DeviceGridState(context);
+            String oldPhoneFileName = deviceGridState.getDbFile();
             List<String> previousDbs = existingDbs(context);
             removeOldDBs(context, oldPhoneFileName);
             // The idp before this contains data about the old phone, after this it becomes the idp
@@ -148,6 +149,7 @@
                 context, oldPhoneDbFileName);
         // The grid option could be null if current phone doesn't support the previous db.
         if (oldPhoneGridOption != null) {
+
             /* If the user only used the default db on the previous phone and the new default db is
              * bigger than or equal to the previous one, then keep the new default db */
             if (previousDbs.size() == 1 && oldPhoneGridOption.numColumns <= idp.numColumns
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 0b45118..26912eb 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -255,7 +255,9 @@
                 || config.fontScale != mInfo.fontScale
                 || !mInfo.mScreenSizeDp.equals(
                         new PortraitSize(config.screenHeightDp, config.screenWidthDp))
-                || mWindowContext.getDisplay().getRotation() != mInfo.rotation) {
+                || mWindowContext.getDisplay().getRotation() != mInfo.rotation
+                || WindowManagerProxy.INSTANCE.get(mContext).showLockedTaskbarOnHome(mWindowContext)
+                        != mInfo.showLockedTaskbarOnHome()) {
             notifyConfigChange();
         }
     }
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index f183f18..72e3e79 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -52,9 +52,9 @@
     public static final String WEB_APP_SEARCH_LOGGING = "WebAppSearchLogging";
 
     /**
-     * When turned on, we enable quick launch v2 related logging.
+     * When turned on, we enable quick launch related logging.
      */
-    public static final String QUICK_LAUNCH_V2 = "QuickLaunchV2";
+    public static final String QUICK_LAUNCH = "QuickLaunch";
 
     /**
      * When turned on, we enable Gms Play related logging.
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index e51609a..4b60d98 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -42,6 +42,9 @@
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -51,16 +54,19 @@
 import java.util.List;
 import java.util.Objects;
 
+import javax.inject.Inject;
+
 /**
  * Utility methods using package manager
  */
-public class PackageManagerHelper implements SafeCloseable{
+@LauncherAppSingleton
+public class PackageManagerHelper {
 
     private static final String TAG = "PackageManagerHelper";
 
     @NonNull
-    public static final MainThreadInitializedObject<PackageManagerHelper> INSTANCE =
-            new MainThreadInitializedObject<>(PackageManagerHelper::new);
+    public static DaggerSingletonObject<PackageManagerHelper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getPackageManagerHelper);
 
     @NonNull
     private final Context mContext;
@@ -73,17 +79,15 @@
 
     private final String[] mLegacyMultiInstanceSupportedApps;
 
-    public PackageManagerHelper(@NonNull final Context context) {
+    @Inject
+    public PackageManagerHelper(@ApplicationContext final Context context) {
         mContext = context;
         mPm = context.getPackageManager();
         mLauncherApps = Objects.requireNonNull(context.getSystemService(LauncherApps.class));
         mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray(
-                R.array.config_appsSupportMultiInstancesSplit);
+                    R.array.config_appsSupportMultiInstancesSplit);
     }
 
-    @Override
-    public void close() { }
-
     /**
      * Returns the installing app package for the given package
      */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index c8ad564..1860977 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -145,7 +145,9 @@
     protected DeviceProfile mDeviceProfile;
 
     protected TextView mNoWidgetsView;
-    protected StickyHeaderLayout mSearchScrollView;
+    protected LinearLayout mSearchScrollView;
+    // Reference to the mSearchScrollView when it is is a sticky header.
+    private @Nullable StickyHeaderLayout mStickyHeaderLayout;
     protected WidgetRecommendationsView mWidgetRecommendationsView;
     protected LinearLayout mWidgetRecommendationsContainer;
     protected View mTabBar;
@@ -220,7 +222,11 @@
 
     protected void setupViews() {
         mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
-        mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
+        if (mSearchScrollView instanceof StickyHeaderLayout) {
+            mStickyHeaderLayout = (StickyHeaderLayout) mSearchScrollView;
+            mStickyHeaderLayout.setCurrentRecyclerView(
+                    findViewById(R.id.primary_widgets_list_view));
+        }
         mNoWidgetsView = findViewById(R.id.no_widgets_text);
         mFastScroller = findViewById(R.id.fast_scroller);
         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
@@ -284,7 +290,9 @@
             reset();
             resetExpandedHeaders();
             mCurrentWidgetsRecyclerView = recyclerView;
-            mSearchScrollView.setCurrentRecyclerView(recyclerView);
+            if (mStickyHeaderLayout != null) {
+                mStickyHeaderLayout.setCurrentRecyclerView(recyclerView);
+            }
         }
     }
 
@@ -313,7 +321,9 @@
             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
         }
         mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
-        mSearchScrollView.reset(/* animate= */ true);
+        if (mStickyHeaderLayout != null) {
+            mStickyHeaderLayout.reset(/* animate= */ true);
+        }
     }
 
     @VisibleForTesting
@@ -1051,7 +1061,7 @@
         }
 
         private int getEmptySpaceHeight() {
-            return mSearchScrollView.getHeaderHeight();
+            return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0;
         }
 
         void setup(WidgetsRecyclerView recyclerView) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 8dd1de4..3d3a669 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -396,15 +396,6 @@
         LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
         if (layoutManager == null) return;
 
-        if (position == mVisibleEntries.size() - 2
-                && mVisibleEntries.get(mVisibleEntries.size() - 1)
-                instanceof WidgetsListContentEntry) {
-            // If the selected header is in the last position and its content is showing, then
-            // scroll to the final position so the last list of widgets will show.
-            layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
-            return;
-        }
-
         // Scroll to the header view's current offset, accounting for the recycler view's padding.
         // If the header view couldn't be found, then it will appear at the top of the list.
         layoutManager.scrollToPositionWithOffset(
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 8770859..55a028b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -79,7 +79,7 @@
         val statusBarNaturalPx: Int,
         val statusBarRotatedPx: Int,
         val gesturePx: Int,
-        val cutoutPx: Int
+        val cutoutPx: Int,
     )
 
     open val deviceSpecs =
@@ -91,7 +91,7 @@
                     statusBarNaturalPx = 118,
                     statusBarRotatedPx = 74,
                     gesturePx = 63,
-                    cutoutPx = 118
+                    cutoutPx = 118,
                 ),
             "tablet" to
                 DeviceSpec(
@@ -100,7 +100,7 @@
                     statusBarNaturalPx = 104,
                     statusBarRotatedPx = 104,
                     gesturePx = 0,
-                    cutoutPx = 0
+                    cutoutPx = 0,
                 ),
             "twopanel-phone" to
                 DeviceSpec(
@@ -109,7 +109,7 @@
                     statusBarNaturalPx = 133,
                     statusBarRotatedPx = 110,
                     gesturePx = 63,
-                    cutoutPx = 133
+                    cutoutPx = 133,
                 ),
             "twopanel-tablet" to
                 DeviceSpec(
@@ -118,14 +118,14 @@
                     statusBarNaturalPx = 110,
                     statusBarRotatedPx = 133,
                     gesturePx = 0,
-                    cutoutPx = 0
-                )
+                    cutoutPx = 0,
+                ),
         )
 
     protected fun initializeVarsForPhone(
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean = true,
-        isVerticalBar: Boolean = false
+        isVerticalBar: Boolean = false,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
@@ -137,14 +137,14 @@
             displayInfo,
             rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
             isGestureMode,
-            densityDpi = deviceSpec.densityDpi
+            densityDpi = deviceSpec.densityDpi,
         )
     }
 
     protected fun initializeVarsForTablet(
         deviceSpec: DeviceSpec,
         isLandscape: Boolean = false,
-        isGestureMode: Boolean = true
+        isGestureMode: Boolean = true,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
@@ -156,7 +156,7 @@
             displayInfo,
             rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
             isGestureMode,
-            densityDpi = deviceSpec.densityDpi
+            densityDpi = deviceSpec.densityDpi,
         )
     }
 
@@ -165,7 +165,7 @@
         deviceSpecFolded: DeviceSpec,
         isLandscape: Boolean = false,
         isGestureMode: Boolean = true,
-        isFolded: Boolean = false
+        isFolded: Boolean = false,
     ) {
         val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
         val unfoldedWindowsBounds =
@@ -182,7 +182,7 @@
         val perDisplayBoundsCache =
             mapOf(
                 unfoldedDisplayInfo to unfoldedWindowsBounds,
-                foldedDisplayInfo to foldedWindowsBounds
+                foldedDisplayInfo to foldedWindowsBounds,
             )
 
         if (isFolded) {
@@ -191,7 +191,7 @@
                 displayInfo = foldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecFolded.densityDpi
+                densityDpi = deviceSpecFolded.densityDpi,
             )
         } else {
             initializeCommonVars(
@@ -199,7 +199,7 @@
                 displayInfo = unfoldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecUnfolded.densityDpi
+                densityDpi = deviceSpecUnfolded.densityDpi,
             )
         }
     }
@@ -208,7 +208,7 @@
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
 
@@ -217,14 +217,14 @@
                 0,
                 max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
                 0,
-                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
+                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
             )
         val rotation90Insets =
             Rect(
                 deviceSpec.cutoutPx,
                 deviceSpec.statusBarRotatedPx,
                 if (isGestureMode) 0 else buttonsNavHeight,
-                if (isGestureMode) deviceSpec.gesturePx else 0
+                if (isGestureMode) deviceSpec.gesturePx else 0,
             )
         val rotation180Insets =
             Rect(
@@ -233,29 +233,29 @@
                 0,
                 max(
                     if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
-                    deviceSpec.cutoutPx
-                )
+                    deviceSpec.cutoutPx,
+                ),
             )
         val rotation270Insets =
             Rect(
                 if (isGestureMode) 0 else buttonsNavHeight,
                 deviceSpec.statusBarRotatedPx,
                 deviceSpec.cutoutPx,
-                if (isGestureMode) deviceSpec.gesturePx else 0
+                if (isGestureMode) deviceSpec.gesturePx else 0,
             )
 
         return listOf(
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270),
         )
     }
 
     private fun tabletWindowsBounds(
         deviceSpec: DeviceSpec,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
         val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
@@ -264,7 +264,7 @@
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270),
         )
     }
 
@@ -273,7 +273,7 @@
         displayInfo: CachedDisplayInfo,
         rotation: Int,
         isGestureMode: Boolean = true,
-        densityDpi: Int
+        densityDpi: Int,
     ) {
         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE)
         LauncherPrefs.get(testContext).put(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE, true)
@@ -307,6 +307,10 @@
 
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
+        whenever(launcherPrefs.get(LauncherPrefs.HOTSEAT_COUNT)).thenReturn(-1)
+        whenever(launcherPrefs.get(LauncherPrefs.DEVICE_TYPE)).thenReturn(-1)
+        whenever(launcherPrefs.get(LauncherPrefs.WORKSPACE_SIZE)).thenReturn("")
+        whenever(launcherPrefs.get(LauncherPrefs.DB_FILE)).thenReturn("")
         val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
similarity index 94%
rename from tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index c6f291d..7933331 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -41,10 +41,10 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-/** Unit tests for [GridSizeMigrationUtil] */
+/** Unit tests for [GridSizeMigrationDBController, GridSizeMigrationLogic] */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class GridSizeMigrationUtilTest {
+class GridSizeMigrationTest {
 
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var context: Context
@@ -130,13 +130,21 @@
         val srcReader = DbReader(db, TMP_TABLE, context)
         val destReader = DbReader(db, TABLE_NAME, context)
         if (Flags.gridMigrationRefactor()) {
-            val gridSizeMigrationLogic = GridSizeMigrationLogic()
-            gridSizeMigrationLogic.migrateGrid(
-                context,
-                DeviceGridState(context),
-                DeviceGridState(idp),
+            var gridSizeMigrationLogic = GridSizeMigrationLogic()
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                srcReader,
+                destReader,
                 dbHelper,
-                db,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                srcReader,
+                destReader,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
             )
         } else {
             GridSizeMigrationDBController.migrate(
@@ -266,12 +274,20 @@
         // migrate from A -> B
         if (Flags.gridMigrationRefactor()) {
             var gridSizeMigrationLogic = GridSizeMigrationLogic()
-            gridSizeMigrationLogic.migrateGrid(
-                context,
-                DeviceGridState(context),
-                DeviceGridState(idp),
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                readerGridA,
+                readerGridB,
                 dbHelper,
-                db,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                readerGridA,
+                readerGridB,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
             )
         } else {
             GridSizeMigrationDBController.migrate(
@@ -445,12 +461,20 @@
     ) {
         if (Flags.gridMigrationRefactor()) {
             var gridSizeMigrationLogic = GridSizeMigrationLogic()
-            gridSizeMigrationLogic.migrateGrid(
-                context,
-                DeviceGridState(context),
-                DeviceGridState(idp),
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                srcReader,
+                destReader,
                 dbHelper,
-                db,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                srcReader,
+                destReader,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
             )
         } else {
             GridSizeMigrationDBController.migrate(
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 308f200..a3a680e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -207,4 +207,21 @@
             .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
         assertFalse(displayController.getInfo().isTransientTaskbar())
     }
+
+    @Test
+    @UiThreadTest
+    fun testLockedTaskbarChangeOnConfigurationChanged() {
+        whenever(windowManagerProxy.showLockedTaskbarOnHome(any())).thenReturn(true)
+        whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
+        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+        whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+        DisplayController.enableTaskbarModePreferenceForTests(true)
+        assertTrue(displayController.getInfo().isTransientTaskbar())
+
+        displayController.onConfigurationChanged(configuration)
+
+        verify(displayInfoChangeListener)
+            .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+        assertFalse(displayController.getInfo().isTransientTaskbar())
+    }
 }
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index b17cd4d..ef7242f 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -3,6 +3,8 @@
 import android.appwidget.AppWidgetManager
 import android.content.Intent
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -203,7 +205,8 @@
         }
 
     @Test
-    fun `When launcher_broadcast_installed_apps and is restore then send installed item broadcast`() {
+    @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When secure setting true and is restore then send installed item broadcast`() {
         // Given
         val spyContext = spy(context)
         `when`(app.context).thenReturn(spyContext)
@@ -271,6 +274,76 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When broadcast flag true and is restore then send installed item broadcast`() {
+        // Given
+        val spyContext = spy(context)
+        `when`(app.context).thenReturn(spyContext)
+        whenever(
+                FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                )
+            )
+            .thenReturn(listOf(expectedBroadcastModel))
+
+        whenever(
+                FirstScreenBroadcastHelper.sendBroadcastsForModels(
+                    spyContext,
+                    listOf(expectedBroadcastModel),
+                )
+            )
+            .thenCallRealMethod()
+
+        Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0)
+        RestoreDbTask.setPending(spyContext)
+
+        // When
+        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+            .runSyncOnBackgroundThread()
+
+        // Then
+        val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+        verify(spyContext).sendBroadcast(argumentCaptor.capture())
+        val actualBroadcastIntent = argumentCaptor.value
+        assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`)
+        assertEquals(
+            ArrayList(expectedBroadcastModel.installedWorkspaceItems),
+            actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.installedHotseatItems),
+            actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems"),
+        )
+        assertEquals(
+            ArrayList(
+                expectedBroadcastModel.firstScreenInstalledWidgets +
+                    expectedBroadcastModel.secondaryScreenInstalledWidgets
+            ),
+            actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingCollectionItems),
+            actualBroadcastIntent.getStringArrayListExtra("folderItem"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingWorkspaceItems),
+            actualBroadcastIntent.getStringArrayListExtra("workspaceItem"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingHotseatItems),
+            actualBroadcastIntent.getStringArrayListExtra("hotseatItem"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingWidgetItems),
+            actualBroadcastIntent.getStringArrayListExtra("widgetItem"),
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
     fun `When not a restore then installed item broadcast not sent`() {
         // Given
         val spyContext = spy(context)
@@ -304,7 +377,8 @@
     }
 
     @Test
-    fun `When launcher_broadcast_installed_apps false then installed item broadcast not sent`() {
+    @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When broadcast flag and secure setting false then installed item broadcast not sent`() {
         // Given
         val spyContext = spy(context)
         `when`(app.context).thenReturn(spyContext)
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
index c08237c..b96dbcd 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -136,12 +136,20 @@
         LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
             if (Flags.gridMigrationRefactor()) {
                 val gridSizeMigrationLogic = GridSizeMigrationLogic()
-                gridSizeMigrationLogic.migrateGrid(
-                    context,
-                    srcGrid.toGridState(),
-                    dstGrid.toGridState(),
+                val idsInUse = mutableListOf<Int>()
+                gridSizeMigrationLogic.migrateHotseat(
+                    dstGrid.size.x,
+                    GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+                    GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
                     dbHelper,
-                    it.db,
+                    idsInUse,
+                )
+                gridSizeMigrationLogic.migrateWorkspace(
+                    GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+                    GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+                    dbHelper,
+                    dstGrid.size,
+                    idsInUse,
                 )
             } else {
                 GridSizeMigrationDBController.migrate(
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 0edcfea..1002ca4 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -369,7 +369,6 @@
         }
     }
 
-
     int getTaskCount() {
         return getTasks().size();
     }
@@ -441,7 +440,7 @@
                     "Not expecting an actions bar: device is tablet and task is not centered");
             return false;
         }
-        if (task.isTaskSplit() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
+        if (task.isGrouped() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
             testLogD(TAG, "Not expecting an actions bar: device is phone and task is split");
             // Overview actions aren't visible for split screen tasks, except for save app pair
             // button on tablets.
@@ -504,11 +503,11 @@
                 "want to assert overview actions view visibility="
                         + isActionsViewVisible()
                         + ", focused task is "
-                        + (task == null ? "null" : (task.isTaskSplit() ? "split" : "not split"))
+                        + (task == null ? "null" : (task.isGrouped() ? "split" : "not split"))
                 )) {
 
             if (isActionsViewVisible()) {
-                if (task.isTaskSplit()) {
+                if (task.isGrouped()) {
                     mLauncher.waitForOverviewObject("action_save_app_pair");
                 } else {
                     mLauncher.waitForOverviewObject("action_buttons");
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 9a8d952..5fd4dac 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -16,9 +16,10 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.DEFAULT;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DEFAULT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DESKTOP;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT;
 
 import android.graphics.Rect;
 
@@ -69,11 +70,11 @@
      * divider between.
      */
     int getVisibleHeight() {
-        if (isTaskSplit()) {
+        if (isGrouped()) {
             return getCombinedSplitTaskHeight();
         }
 
-        UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
+        UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes);
         return taskSnapshot1.getVisibleBounds().height();
     }
 
@@ -102,7 +103,7 @@
      * divider between.
      */
     int getVisibleWidth() {
-        if (isTaskSplit()) {
+        if (isGrouped()) {
             return getCombinedSplitTaskWidth();
         }
 
@@ -164,8 +165,11 @@
 
             dismissBySwipingUp();
 
+            long numNonDesktopTasks = mOverview.getCurrentTasksForTablet()
+                    .stream().filter(t -> !t.isDesktop()).count();
+
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("dismissed")) {
-                if (taskWasFocused) {
+                if (taskWasFocused && numNonDesktopTasks > 0) {
                     mLauncher.assertNotNull("No task became focused",
                             mOverview.getFocusedTaskForTablet());
                 }
@@ -256,7 +260,7 @@
 
     /** Taps the task menu of the split task. Returns the split task's menu object. */
     @NonNull
-    public OverviewTaskMenu tapMenu(OverviewSplitTask task) {
+    public OverviewTaskMenu tapMenu(OverviewTaskContainer task) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "want to tap the task menu")) {
@@ -270,10 +274,6 @@
         }
     }
 
-    boolean isTaskSplit() {
-        return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null;
-    }
-
     private UiObject2 findObjectInTask(String resName) {
         return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
     }
@@ -285,8 +285,8 @@
      * TODO(b/342627272): remove Nullable support once the bug causing it to be null is fixed.
      */
     public boolean containsContentDescription(@Nullable String expected,
-            OverviewSplitTask overviewSplitTask) {
-        String actual = findObjectInTask(overviewSplitTask.snapshotRes).getContentDescription();
+            OverviewTaskContainer overviewTaskContainer) {
+        String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription();
         if (actual == null && expected == null) {
             return true;
         }
@@ -315,21 +315,31 @@
         }
     }
 
+    boolean isGrouped() {
+        return mType == TaskViewType.GROUPED;
+    }
+
+    public boolean isDesktop() {
+        return mType == TaskViewType.DESKTOP;
+    }
+
     /**
-     * Enum used to specify  which task is retrieved when it is a split task.
+     * Enum used to specify which resource name should be used depending on the type of the task.
      */
-    public enum OverviewSplitTask {
+    public enum OverviewTaskContainer {
         // The main task when the task is not split.
         DEFAULT("snapshot", "icon"),
         // The first task in split task.
         SPLIT_TOP_OR_LEFT("snapshot", "icon"),
         // The second task in split task.
-        SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon");
+        SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"),
+        // The desktop task.
+        DESKTOP("background", "icon");
 
         public final String snapshotRes;
         public final String iconAppRes;
 
-        OverviewSplitTask(String snapshotRes, String iconAppRes) {
+        OverviewTaskContainer(String snapshotRes, String iconAppRes) {
             this.snapshotRes = snapshotRes;
             this.iconAppRes = iconAppRes;
         }