Merge "Add Arrows to Folder PageIndicatorDots for Accessibility Purposes." into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index c5774bb..9f505a4 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -656,3 +656,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "sync_app_launch_with_taskbar_stash"
+  namespace: "launcher"
+  description: "Syncs the two animations (app launch, taskbar stash) so they play at the same time."
+  bug: "319162553"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/quickstep/res/color/app_chip_menu_item_color_fg.xml b/quickstep/res/color/app_chip_menu_item_color_fg.xml
new file mode 100644
index 0000000..fa1dc34
--- /dev/null
+++ b/quickstep/res/color/app_chip_menu_item_color_fg.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:alpha="0.15" android:color="@color/materialColorOnSurface" android:state_enabled="true" android:state_pressed="true" />
+    <item android:alpha="0.11" android:color="@color/materialColorOnSurface" android:state_enabled="true" android:state_hovered="true" />
+    <item android:color="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/quickstep/res/color/app_chip_state_color_fg.xml b/quickstep/res/color/app_chip_state_color_fg.xml
new file mode 100644
index 0000000..58dfee0
--- /dev/null
+++ b/quickstep/res/color/app_chip_state_color_fg.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:alpha="0.15" android:color="@color/materialColorOnSurface" android:state_enabled="true" android:state_pressed="true" />
+    <item android:alpha="0.11" android:color="@color/materialColorOnSurface" android:state_enabled="true" android:state_hovered="true" />
+    <item android:color="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/app_chip_fg.xml b/quickstep/res/drawable/app_chip_fg.xml
new file mode 100644
index 0000000..7b19c9e
--- /dev/null
+++ b/quickstep/res/drawable/app_chip_fg.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/app_chip_state_color_fg" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/app_chip_menu_item_bg.xml b/quickstep/res/drawable/app_chip_menu_item_bg.xml
new file mode 100644
index 0000000..39e88d2
--- /dev/null
+++ b/quickstep/res/drawable/app_chip_menu_item_bg.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/materialColorSurfaceBright" />
+</shape>
diff --git a/quickstep/res/drawable/app_chip_menu_item_fg.xml b/quickstep/res/drawable/app_chip_menu_item_fg.xml
new file mode 100644
index 0000000..96d067d
--- /dev/null
+++ b/quickstep/res/drawable/app_chip_menu_item_fg.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2025 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/app_chip_menu_item_color_fg" />
+    <corners android:radius="@dimen/task_menu_item_corner_radius" />
+</shape>
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index 0972be1..de05d59 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -26,6 +26,7 @@
     android:importantForAccessibility="no"
     android:autoMirrored="true"
     android:elevation="@dimen/task_thumbnail_icon_menu_elevation"
+    android:foreground="@drawable/app_chip_fg"
     android:background="@color/materialColorSurfaceBright">
 
     <!-- ignoring warning because the user of the anchor is a Rect where RTL is not needed -->
@@ -77,4 +78,5 @@
         android:background="@drawable/icon_menu_arrow_background"
         android:src="@drawable/ic_chevron_down"
         android:importantForAccessibility="no" />
+
 </com.android.quickstep.views.IconAppChipView>
\ No newline at end of file
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 885bdb9..2dea79c 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -18,6 +18,8 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/keyboard_quick_switch_view"
+    android:contentDescription="@string/quick_switch_content_description"
+    android:accessibilityPaneTitle="@string/quick_switch_pane_title"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 65f4b3c..7578bd5 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -325,6 +325,12 @@
     <!-- Label for creating an application bubble (from the Taskbar only). -->
     <string name="open_app_as_a_bubble">Open app as a bubble</string>
 
+    <!-- Accessibility pane title for quick switch view, which lists apps opened by the user, ordered by how recently the app was opened. -->
+    <string name="quick_switch_pane_title">Recent apps</string>
+
+    <!-- Content description for the quick switch view, which lists apps opened by the user, ordered by how recently the app was opened. -->
+    <string name="quick_switch_content_description">Recent app list</string>
+
     <!-- Label for quick switch tile showing how many more apps are available. The number will be displayed above this text. [CHAR LIMIT=NONE] -->
     <string name="quick_switch_overflow">{count, plural,
             =1{more app}
@@ -336,6 +342,8 @@
 
     <!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
     <string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
+    <!-- Accessibility label for quick switch tiles that include information about the tile's position in the parent list [CHAR LIMIT=NONE] -->
+    <string name="quick_switch_task_with_position_in_parent"><xliff:g id="task_description" example="Chrome">%1$s</xliff:g>, item <xliff:g id="index_in_parent" example="1">%2$d</xliff:g> of <xliff:g id="total_tasks" example="5">%3$d</xliff:g></string>
 
     <!-- Accessibility label for an arrow button within quick switch UI that scrolls the quick switch content left
         TODO(b/397975686): Make these translatable when verified by UX. -->
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 4d3e3be..cdae9ca 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -166,7 +166,7 @@
         mApp = LauncherAppState.getInstance(this);
         InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
         mDeviceProfile = idp.getDeviceProfile(this);
-        mModel = new WidgetsModel();
+        mModel = new WidgetsModel(mApp.getContext());
         mWidgetPickerDataProvider = new WidgetPickerDataProvider(this);
 
         setContentView(R.layout.widget_picker_activity);
@@ -309,8 +309,7 @@
      */
     private void refreshAndBindWidgets() {
         MODEL_EXECUTOR.execute(() -> {
-            LauncherAppState app = LauncherAppState.getInstance(this);
-            mModel.update(app, null);
+            mModel.update(null);
 
             StringCache stringCache = new StringCache();
             stringCache.loadStrings(this);
@@ -321,9 +320,10 @@
             // animation.
             openWidgetsSheet();
             if (mUiSurface != null) {
-                mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
-                        mUiSurface, mModel.getWidgetsByComponentKeyForPicker());
-                mWidgetPredictionsRequester.request(mAddedWidgets, /*listener=*/ this);
+                mWidgetPredictionsRequester = new WidgetPredictionsRequester(
+                        getApplicationContext(), mUiSurface,
+                        mModel.getWidgetsByComponentKeyForPicker());
+                mWidgetPredictionsRequester.request(mAddedWidgets, this);
             }
         });
     }
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 40cfe92..a01846d 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -30,6 +30,7 @@
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
 import com.android.quickstep.views.DesktopTaskView
 import com.android.quickstep.views.TaskContainer
 import com.android.quickstep.views.TaskView
@@ -60,7 +61,11 @@
                 callback,
             )
         val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
-        systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
+        if (areMultiDesksFlagsEnabled()) {
+            systemUiProxy.activateDesk(desktopTaskView.deskId, transition)
+        } else {
+            systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
+        }
     }
 
     /** Launch desktop tasks from recents view */
diff --git a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java
index fd71151..3544844 100644
--- a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java
@@ -32,9 +32,9 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.ConstantItem;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
 import com.android.launcher3.model.data.AppInfo;
@@ -65,8 +65,8 @@
     @Override
     public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
             @NonNull AllAppsList apps) {
-        LauncherAppState app = taskController.getApp();
-        Context context = app.getContext();
+        IconCache iconCache = taskController.getIconCache();
+        Context context = taskController.getContext();
 
         // TODO: remove this
         LauncherPrefs.get(context).put(LAST_PREDICTION_ENABLED, !mTargets.isEmpty());
@@ -84,7 +84,7 @@
             if (si != null) {
                 usersForChangedShortcuts.add(si.getUserHandle());
                 itemInfo = new WorkspaceItemInfo(si, context);
-                app.getIconCache().getShortcutIcon(itemInfo, si);
+                iconCache.getShortcutIcon(itemInfo, si);
             } else {
                 String className = target.getClassName();
                 if (COMPONENT_CLASS_MARKER.equals(className)) {
@@ -96,7 +96,7 @@
                 itemInfo = apps.data.stream()
                         .filter(info -> user.equals(info.user) && cn.equals(info.componentName))
                         .map(ai -> {
-                            app.getIconCache().getTitleAndIcon(ai, mPredictorState.lookupFlag);
+                            iconCache.getTitleAndIcon(ai, mPredictorState.lookupFlag);
                             return ai.makeWorkspaceItem(context);
                         })
                         .findAny()
@@ -107,7 +107,7 @@
                                 return null;
                             }
                             AppInfo ai = new AppInfo(context, lai, user);
-                            app.getIconCache().getTitleAndIcon(ai, lai, DEFAULT_LOOKUP_FLAG);
+                            iconCache.getTitleAndIcon(ai, lai, DEFAULT_LOOKUP_FLAG);
                             return ai.makeWorkspaceItem(context);
                         });
 
@@ -123,8 +123,7 @@
         FixedContainerItems fci = new FixedContainerItems(mPredictorState.containerId, items);
         dataModel.extraItems.put(fci.containerId, fci);
         taskController.bindExtraContainerItems(fci);
-        usersForChangedShortcuts.forEach(
-                u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
+        usersForChangedShortcuts.forEach(u -> dataModel.updateShortcutPinnedState(context, u));
 
         // Save to disk
         mPredictorState.storage.write(context, fci.items);
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 0a4b7c8..da55b40 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -82,7 +82,7 @@
                                 && !widgetsInWorkspace.contains(entry.getValue())
                         ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
 
-        Context context = taskController.getApp().getContext();
+        Context context = taskController.getContext();
 
         List<WidgetItem> servicePredictedItems = new ArrayList<>();
         List<String> addedWidgetApps = new ArrayList<>();
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index eb24df1..810fa6f 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -401,6 +401,39 @@
         DisplayController.INSTANCE.get(context).notifyConfigChange()
     }
 
+    private fun notifyOnDeskAdded(displayId: Int, deskId: Int) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyOnDeskAdded: displayId=$displayId, deskId=$deskId")
+        }
+
+        for (listener in desktopVisibilityListeners) {
+            listener.onDeskAdded(displayId, deskId)
+        }
+    }
+
+    private fun notifyOnDeskRemoved(displayId: Int, deskId: Int) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyOnDeskRemoved: displayId=$displayId, deskId=$deskId")
+        }
+
+        for (listener in desktopVisibilityListeners) {
+            listener.onDeskRemoved(displayId, deskId)
+        }
+    }
+
+    private fun notifyOnActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "notifyOnActiveDeskChanged: displayId=$displayId, newActiveDesk=$newActiveDesk, oldActiveDesk=$oldActiveDesk",
+            )
+        }
+
+        for (listener in desktopVisibilityListeners) {
+            listener.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+        }
+    }
+
     /** TODO: b/333533253 - Remove after flag rollout */
     private fun setBackgroundStateEnabled(backgroundStateEnabled: Boolean) {
         if (DEBUG) {
@@ -511,6 +544,8 @@
                 "Found a duplicate desk Id: $deskId on display: $displayId"
             }
         }
+
+        notifyOnDeskAdded(displayId, deskId)
     }
 
     private fun onDeskRemoved(displayId: Int, deskId: Int) {
@@ -526,6 +561,8 @@
                 it.activeDeskId = INACTIVE_DESK_ID
             }
         }
+
+        notifyOnDeskRemoved(displayId, deskId)
     }
 
     private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
@@ -539,12 +576,16 @@
             check(oldActiveDesk == it.activeDeskId) {
                 "Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
             }
-            check(it.deskIds.contains(newActiveDesk)) {
+            check(newActiveDesk == INACTIVE_DESK_ID || it.deskIds.contains(newActiveDesk)) {
                 "newActiveDesk: $newActiveDesk was never added to display: $displayId"
             }
             it.activeDeskId = newActiveDesk
         }
 
+        if (newActiveDesk != oldActiveDesk) {
+            notifyOnActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+        }
+
         if (wasInDesktopMode != isInDesktopModeAndNotInOverview(displayId)) {
             notifyIsInDesktopModeChanged(displayId, !wasInDesktopMode)
         }
@@ -650,8 +691,11 @@
                         "duration= " +
                         transitionDuration),
                 )
-                controller.get()?.isInDesktopMode = true
-                controller.get()?.notifyTaskbarDesktopModeListenersForEntry(transitionDuration)
+                val controller = controller.get()
+                if (controller != null && !controller.isInDesktopMode) {
+                    controller.isInDesktopMode = true
+                    controller.notifyTaskbarDesktopModeListenersForEntry(transitionDuration)
+                }
             }
         }
 
@@ -663,8 +707,11 @@
                         "duration= " +
                         transitionDuration),
                 )
-                controller.get()?.isInDesktopMode = false
-                controller.get()?.notifyTaskbarDesktopModeListenersForExit(transitionDuration)
+                val controller = controller.get()
+                if (controller != null && controller.isInDesktopMode) {
+                    controller.isInDesktopMode = false
+                    controller.notifyTaskbarDesktopModeListenersForExit(transitionDuration)
+                }
             }
         }
 
@@ -718,6 +765,6 @@
         private const val TAG = "DesktopVisController"
         private const val DEBUG = false
 
-        public const val INACTIVE_DESK_ID = -1
+        const val INACTIVE_DESK_ID = -1
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
index 09a8670..aa3feb7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
@@ -23,6 +23,7 @@
 
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.BaseContext;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.SystemUiProxy;
 
@@ -32,10 +33,20 @@
         implements SystemShortcut.BubbleActivityStarter {
 
     protected final LayoutInflater mLayoutInflater;
+    private final boolean mIsPrimaryDisplay;
 
-    public BaseTaskbarContext(Context windowContext) {
+    public BaseTaskbarContext(Context windowContext, boolean isPrimaryDisplay) {
         super(windowContext, Themes.getActivityThemeRes(windowContext));
         mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
+        mIsPrimaryDisplay = isPrimaryDisplay;
+    }
+
+    public boolean isTransientTaskbar() {
+        return DisplayController.isTransientTaskbar(this) && mIsPrimaryDisplay;
+    }
+
+    public boolean isPrimaryDisplay() {
+        return mIsPrimaryDisplay;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index cc94824..1698050 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -170,7 +170,7 @@
                                 mHasDesktopTask,
                                 mWasDesktopTaskFilteredOut);
                     }, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
-                            : RecentsFilterState.getEmptyDesktopTaskFilter());
+                            : RecentsFilterState.getDesktopTaskFilter());
                 }
 
                 mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
@@ -232,7 +232,7 @@
                     mWasDesktopTaskFilteredOut,
                     wasOpenedFromTaskbar);
         }, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
-                : RecentsFilterState.getEmptyDesktopTaskFilter());
+                : RecentsFilterState.getDesktopTaskFilter());
     }
 
     private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 5f867cd..15be03a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -66,6 +66,11 @@
     @Nullable private ImageView mIcon2;
     @Nullable private View mContent;
 
+    // Describe the task position in the parent container. Used to add information about the task's
+    // position in a task list to the task view's content description.
+    private int mIndexInParent = -1;
+    private int mTotalTasksInParent = -1;
+
     public KeyboardQuickSwitchTaskView(@NonNull Context context) {
         this(context, null);
     }
@@ -155,36 +160,51 @@
         applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction);
         applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction);
 
+        // Update content description, even in cases task icons, and content descriptions need to be
+        // loaded asynchronously to ensure that the task has non empty description (assuming task
+        // position information was set), as KeyboardQuickSwitch view may request accessibility
+        // focus to be moved to the task when the quick switch UI gets shown. The description will
+        // be updated once the task metadata has been loaded - the delay should be very short, and
+        // the content description when task titles are not available still gives some useful
+        // information to the user (the task's position in the list).
+        updateContentDesctiptionForTasks(task1, task2);
+
         if (iconUpdateFunction == null) {
             applyIcon(mIcon1, task1);
             applyIcon(mIcon2, task2);
-            setContentDescription(task2 == null
-                    ? task1.titleDescription
-                    : getContext().getString(
-                            R.string.quick_switch_split_task,
-                            task1.titleDescription,
-                            task2.titleDescription));
             return;
         }
+
         iconUpdateFunction.updateIconInBackground(task1, t -> {
             applyIcon(mIcon1, task1);
             if (task2 != null) {
                 return;
             }
-            setContentDescription(task1.titleDescription);
+            updateContentDesctiptionForTasks(task1, null);
         });
+
         if (task2 == null) {
             return;
         }
         iconUpdateFunction.updateIconInBackground(task2, t -> {
             applyIcon(mIcon2, task2);
-            setContentDescription(getContext().getString(
-                    R.string.quick_switch_split_task,
-                    task1.titleDescription,
-                    task2.titleDescription));
+            updateContentDesctiptionForTasks(task1, task2);
         });
     }
 
+    /**
+     * Initializes information about the task's position within the parent container context - used
+     * to add position information to the view's content description.
+     * Should be called before associating the view with tasks.
+     *
+     * @param index The view's 0-based index within the parent task container.
+     * @param totalTasks The total number of tasks in the parent task container.
+     */
+    protected void setPositionInformation(int index, int totalTasks) {
+        mIndexInParent = index;
+        mTotalTasksInParent = totalTasks;
+    }
+
     protected void setThumbnailsForSplitTasks(
             @NonNull Task task1,
             @Nullable Task task2,
@@ -283,6 +303,28 @@
                 constantState.newDrawable(getResources(), getContext().getTheme()));
     }
 
+    /**
+     * Updates the task view's content description to reflect tasks represented by the view.
+     */
+    private void updateContentDesctiptionForTasks(@NonNull Task task1, @Nullable Task task2) {
+        String tasksDescription = task1.titleDescription == null || task2 == null
+                ? task1.titleDescription
+                : getContext().getString(
+                        R.string.quick_switch_split_task,
+                        task1.titleDescription,
+                        task2.titleDescription);
+        if (mIndexInParent < 0) {
+            setContentDescription(tasksDescription);
+            return;
+        }
+
+        setContentDescription(
+                getContext().getString(R.string.quick_switch_task_with_position_in_parent,
+                        tasksDescription != null ? tasksDescription : "",
+                        mIndexInParent + 1,
+                        mTotalTasksInParent));
+    }
+
     protected interface ThumbnailUpdateFunction {
 
         void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 845e19f..ab147bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -301,6 +301,7 @@
                 continue;
             }
 
+            currentTaskView.setPositionInformation(i, tasksToDisplay);
             currentTaskView.setThumbnailsForSplitTasks(
                     task1,
                     task2,
@@ -548,6 +549,9 @@
 
 
         ViewOutlineProvider outlineProvider = getOutlineProvider();
+        int defaultFocusedTaskIndex = Math.min(
+                getTaskCount() - 1,
+                currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride);
         mOpenAnimation.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -601,9 +605,7 @@
                             });
                 }
 
-                animateFocusMove(-1, Math.min(
-                        getTaskCount() - 1,
-                        currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));
+                animateFocusMove(-1, defaultFocusedTaskIndex);
                 displayedContent.setVisibility(VISIBLE);
                 setVisibility(VISIBLE);
                 requestFocus();
@@ -623,6 +625,11 @@
                 invalidateOutline();
                 mOpenAnimation = null;
                 InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
+
+                View focusedTask = getTaskAt(defaultFocusedTaskIndex);
+                if (focusedTask != null) {
+                    focusedTask.requestAccessibilityFocus();
+                }
             }
         });
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 5f7a026..b5f2532 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -40,7 +40,6 @@
 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;
@@ -106,8 +105,7 @@
             boolean hasDesktopTask,
             boolean wasDesktopTaskFilteredOut,
             boolean wasOpenedFromTaskbar) {
-        final boolean isTransientTaskBar = DisplayController.isTransientTaskbar(
-                mControllers.taskbarActivityContext);
+        final boolean isTransientTaskBar = mControllers.taskbarActivityContext.isTransientTaskbar();
         positionView(wasOpenedFromTaskbar, isTransientTaskBar);
 
         // Keep the taskbar unstashed if the KQS is opened.
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 2272d11..e998388 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -135,11 +135,11 @@
     @Override
     protected void onDestroy() {
         onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */);
+        mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
         super.onDestroy();
         mTaskbarLauncherStateController.onDestroy();
 
         mLauncher.setTaskbarUIController(null);
-        mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
         mHomeState.removeListener(mVisibilityChangeListener);
     }
 
@@ -225,9 +225,8 @@
         if (isVisible || isPinnedTaskbar) {
             return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinnedTaskbar);
         } else {
-            return DisplayController.isTransientTaskbar(mLauncher)
-                    ? TRANSIENT_TASKBAR_TRANSITION_DURATION
-                    : TASKBAR_TO_APP_DURATION;
+            return mControllers.taskbarActivityContext.isTransientTaskbar()
+                    ? TRANSIENT_TASKBAR_TRANSITION_DURATION : TASKBAR_TO_APP_DURATION;
         }
     }
 
@@ -279,7 +278,10 @@
     private void postAdjustHotseatForBubbleBar() {
         Hotseat hotseat = mLauncher.getHotseat();
         if (hotseat == null || !isBubbleBarVisible()) return;
-        hotseat.post(() -> adjustHotseatForBubbleBar(isBubbleBarVisible()));
+        hotseat.post(() -> {
+            if (mControllers == null) return;
+            adjustHotseatForBubbleBar(isBubbleBarVisible());
+        });
     }
 
     private boolean isBubbleBarVisible() {
@@ -334,7 +336,7 @@
         }
 
         // Persistent features EDU tooltip.
-        if (!DisplayController.isTransientTaskbar(mLauncher)) {
+        if (!mControllers.taskbarActivityContext.isTransientTaskbar()) {
             mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu();
             return;
         }
@@ -357,7 +359,7 @@
         }
 
         // Persistent features EDU tooltip.
-        if (!DisplayController.isTransientTaskbar(mLauncher)) {
+        if (!mControllers.taskbarActivityContext.isTransientTaskbar()) {
             return !OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.hasReachedMax(mLauncher);
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index ee5b8d1..27b38e5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -870,6 +870,9 @@
         if (predictiveBackThreeButtonNav() && buttonType == BUTTON_BACK) {
             // set up special touch listener for back button to support predictive back
             setBackButtonTouchListener(buttonView, navButtonController);
+            // Set this View clickable, so that NearestTouchFrame.java forwards closeby touches to
+            // this View
+            buttonView.setClickable(true);
         } else {
             buttonView.setOnClickListener(view ->
                     navButtonController.onButtonClick(buttonType, view));
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 2e5bebc..6bd3d85 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -217,7 +216,7 @@
                 .getTransientTaskbarIconLayoutBounds();
         float startRadius = mStashedHandleRadius;
 
-        if (DisplayController.isTransientTaskbar(mActivity)) {
+        if (mActivity.isTransientTaskbar()) {
             // Account for the full visual height of the transient taskbar.
             int heightDiff = (mTaskbarSize - visualBounds.height()) / 2;
             visualBounds.top -= heightDiff;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6afbebf..de91c54 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -99,6 +99,7 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.config.FeatureFlags;
@@ -255,7 +256,7 @@
             TaskbarNavButtonController buttonController,
             ScopedUnfoldTransitionProgressProvider unfoldTransitionProgressProvider,
             boolean isPrimaryDisplay, SystemUiProxy sysUiProxy) {
-        super(windowContext);
+        super(windowContext, isPrimaryDisplay);
         mIsPrimaryDisplay = isPrimaryDisplay;
         mNavigationBarPanelContext = navigationBarPanelContext;
         mSysUiProxy = sysUiProxy;
@@ -386,13 +387,6 @@
         onViewCreated();
     }
 
-    /**
-     * Returns whether this is a primary display.
-     */
-    public boolean isPrimaryDisplay() {
-        return mIsPrimaryDisplay;
-    }
-
     /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
     public void updateDeviceProfile(DeviceProfile launcherDp) {
         applyDeviceProfile(launcherDp);
@@ -411,7 +405,7 @@
 
     /** Returns whether current taskbar is transient. */
     public boolean isTransientTaskbar() {
-        return DisplayController.isTransientTaskbar(this) && !isPhoneMode();
+        return super.isTransientTaskbar() && !isPhoneMode();
     }
 
     /**
@@ -432,7 +426,7 @@
         mDeviceProfile = originDeviceProfile.toBuilder(this)
                 .withDimensionsOverride(overrideProvider).build();
 
-        if (DisplayController.isTransientTaskbar(this)) {
+        if (isTransientTaskbar()) {
             mTransientTaskbarDeviceProfile = mDeviceProfile;
             mPersistentTaskbarDeviceProfile = mDeviceProfile
                     .toBuilder(this)
@@ -450,7 +444,6 @@
         mNavMode = (DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
                 && !mIsPrimaryDisplay) ? NavigationMode.THREE_BUTTONS
                 : DisplayController.getNavigationMode(this);
-
     }
 
     /** Called when the visibility of the bubble bar changed. */
@@ -661,8 +654,7 @@
         int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_SLIPPERY
                 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
-        boolean watchOutside = DisplayController.isTransientTaskbar(this)
-                || isThreeButtonNav();
+        boolean watchOutside = isTransientTaskbar() || isThreeButtonNav();
         if (watchOutside && !isRunningInTestHarness()) {
             windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
@@ -1223,8 +1215,8 @@
                 bubbleControllers.bubbleBarViewController.getBubbleBarWithFlyoutMaximumHeight()
         ).orElse(0);
         int taskbarWindowSize;
-        boolean shouldTreatAsTransient = DisplayController.isTransientTaskbar(this)
-                || (enableTaskbarPinning() && !isThreeButtonNav());
+        boolean shouldTreatAsTransient =
+                isTransientTaskbar() || (enableTaskbarPinning() && !isThreeButtonNav());
 
         int extraHeightForTaskbarTooltips = enableCursorHoverStates()
                 ? resources.getDimensionPixelSize(R.dimen.arrow_toast_arrow_height)
@@ -1297,7 +1289,7 @@
      * Applies forcibly show flag to taskbar window iff transient taskbar is unstashed.
      */
     public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) {
-        if (!DisplayController.isTransientTaskbar(this) || isPhoneMode()) {
+        if (!isTransientTaskbar() || isPhoneMode()) {
             return;
         }
         if (shouldForceShow) {
@@ -1344,7 +1336,7 @@
         mControllers.uiController.startSplitSelection(splitSelectSource);
     }
 
-    boolean areDesktopTasksVisible() {
+    boolean isInDesktopMode() {
         return mControllers != null
                 && mControllers.taskbarDesktopModeController.isInDesktopMode(getDisplayId());
     }
@@ -1360,23 +1352,23 @@
         // TODO: b/316004172, b/343289567: Handle `DesktopTask` and `SplitTask`.
         if (tag instanceof SingleTask singleTask) {
             RemoteTransition remoteTransition =
-                    (areDesktopTasksVisible() && canUnminimizeDesktopTask(
+                    (isInDesktopMode() && canUnminimizeDesktopTask(
                             singleTask.getTask().key.id))
                             ? createDesktopAppLaunchRemoteTransition(AppLaunchType.UNMINIMIZE,
                             Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
                             : null;
-            if (areDesktopTasksVisible() && mControllers.uiController.isInOverviewUi()) {
+            if (isInDesktopMode() && mControllers.uiController.isInOverviewUi()) {
                 RunnableList runnableList = recents.launchRunningDesktopTaskView();
                 // Wrapping it in runnable so we post after DW is ready for the app
                 // launch.
                 if (runnableList != null) {
                     runnableList.add(() -> UI_HELPER_EXECUTOR.execute(
                             () -> handleGroupTaskLaunch(singleTask, remoteTransition,
-                                    areDesktopTasksVisible(),
+                                    isInDesktopMode(),
                                     DesktopTaskToFrontReason.TASKBAR_TAP)));
                 }
             } else {
-                handleGroupTaskLaunch(singleTask, remoteTransition, areDesktopTasksVisible(),
+                handleGroupTaskLaunch(singleTask, remoteTransition, isInDesktopMode(),
                         DesktopTaskToFrontReason.TASKBAR_TAP);
             }
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1402,7 +1394,7 @@
                     : null;
 
 
-            if (areDesktopTasksVisible() && mControllers.uiController.isInOverviewUi()) {
+            if (isInDesktopMode() && mControllers.uiController.isInOverviewUi()) {
                 RunnableList runnableList = recents.launchRunningDesktopTaskView();
                 if (runnableList != null) {
                     runnableList.add(() ->
@@ -1676,7 +1668,7 @@
                                                 .launchAppPair((AppPairIcon) launchingIconView,
                                                         -1 /*cuj*/)));
                     } else {
-                        if (areDesktopTasksVisible()
+                        if (isInDesktopMode()
                                 && mControllers.uiController.isInOverviewUi()) {
                             RunnableList runnableList = recents.launchRunningDesktopTaskView();
                             // Wrapping it in runnable so we post after DW is ready for the app
@@ -1722,7 +1714,7 @@
                     return;
                 }
             }
-            if (areDesktopTasksVisible()
+            if (isInDesktopMode()
                     && DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue()) {
                 launchDesktopApp(intent, info, displayId);
             } else {
@@ -1887,7 +1879,7 @@
      */
     @VisibleForTesting
     public void unstashTaskbarIfStashed() {
-        if (DisplayController.isTransientTaskbar(this)) {
+        if (isTransientTaskbar()) {
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
         }
     }
@@ -1950,6 +1942,8 @@
                 // Override the alpha updates in the icon alignment animation.
                 allAppsButton.setAlpha(0);
             });
+            alphaOverride.addListener(AnimatorListeners.forSuccessCallback(
+                    () -> allAppsButton.setAlpha(1f)));
             fullAnimation.play(alphaOverride);
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index 89cc991..5e02d81 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -30,7 +30,6 @@
 import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
 import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT
 import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT
-import com.android.launcher3.util.DisplayController
 import kotlin.math.min
 
 /** Helps draw the taskbar background, made up of a rectangle plus two inverted rounded corners. */
@@ -43,7 +42,7 @@
     private val maxPersistentTaskbarHeight =
         context.persistentTaskbarDeviceProfile.taskbarHeight.toFloat()
     var backgroundProgress =
-        if (DisplayController.isTransientTaskbar(context)) {
+        if (context.isTransientTaskbar) {
             PINNING_TRANSIENT
         } else {
             PINNING_PERSISTENT
@@ -102,7 +101,7 @@
 
         fullCornerRadius = context.cornerRadius.toFloat()
         cornerRadius = fullCornerRadius
-        if (!context.areDesktopTasksVisible()) {
+        if (!context.isInDesktopMode()) {
             setCornerRoundness(MAX_ROUNDNESS)
         }
     }
@@ -124,7 +123,7 @@
      * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
      */
     fun setCornerRoundness(cornerRoundness: Float) {
-        if (DisplayController.isTransientTaskbar(context) && !transientBackgroundBounds.isEmpty) {
+        if (context.isTransientTaskbar && !transientBackgroundBounds.isEmpty) {
             return
         }
 
@@ -146,7 +145,7 @@
     /** Draws the background with the given paint and height, on the provided canvas. */
     fun draw(canvas: Canvas) {
         if (isInSetup) return
-        val isTransientTaskbar = DisplayController.isTransientTaskbar(context)
+        val isTransientTaskbar = context.isTransientTaskbar
         canvas.save()
         if (!isTransientTaskbar || transientBackgroundBounds.isEmpty || isAnimatingPinning) {
             drawPersistentBackground(canvas)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 58606de..a2b6423 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -260,6 +260,7 @@
         mAreAllControllersInitialized = false;
         mSharedState = null;
 
+        taskbarDragController.onDestroy();
         navbarButtonsViewController.onDestroy();
         uiController.onDestroy();
         rotationButtonController.onDestroy();
@@ -280,7 +281,6 @@
         taskbarStashController.onDestroy();
         bubbleControllers.ifPresent(controllers -> controllers.onDestroy());
         taskbarDesktopModeController.onDestroy();
-
         mControllersToLog = null;
         mBackgroundRendererControllers = null;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index 3f6ebe2..e3e7499 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -35,7 +35,6 @@
 import com.android.launcher3.R
 import com.android.launcher3.popup.ArrowPopup
 import com.android.launcher3.popup.RoundedArrowDrawable
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Themes
 import com.android.launcher3.views.ActivityContext
 import kotlin.math.max
@@ -64,12 +63,17 @@
                     false,
                 ) as TaskbarDividerPopupView<*>
 
-            return taskMenuViewWithArrow.populateForView(view, horizontalPosition)
+            return taskMenuViewWithArrow.populateForView(
+                view,
+                horizontalPosition,
+                taskbarActivityContext,
+            )
         }
     }
 
     private lateinit var dividerView: View
     private var horizontalPosition = 0.0f
+    private lateinit var taskbarActivityContext: TaskbarActivityContext
 
     private val popupCornerRadius = Themes.getDialogCornerRadius(context)
     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
@@ -78,7 +82,7 @@
     private val minPaddingFromScreenEdge =
         resources.getDimension(R.dimen.taskbar_pinning_popup_menu_min_padding_from_screen_edge)
 
-    private var alwaysShowTaskbarOn = !DisplayController.isTransientTaskbar(context)
+    private var alwaysShowTaskbarOn = !taskbarActivityContext.isTransientTaskbar
     private var didPreferenceChange = false
     private var verticalOffsetForPopupView =
         resources.getDimensionPixelSize(R.dimen.taskbar_pinning_popup_menu_vertical_margin)
@@ -175,8 +179,13 @@
         return false
     }
 
-    private fun populateForView(view: View, horizontalPosition: Float): TaskbarDividerPopupView<*> {
+    private fun populateForView(
+        view: View,
+        horizontalPosition: Float,
+        taskbar: TaskbarActivityContext,
+    ): TaskbarDividerPopupView<*> {
         dividerView = view
+        taskbarActivityContext = taskbar
         this@TaskbarDividerPopupView.horizontalPosition = horizontalPosition
         tryUpdateBackground()
         return this
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 1b516be..142f458 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -80,7 +80,6 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.views.BubbleTextHolder;
@@ -132,6 +131,14 @@
 
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        mControllers.bubbleControllers.ifPresent(
+                c -> c.bubbleBarViewController.addBubbleBarDropTargets(this));
+    }
+
+    /** Called when the controller is destroyed. */
+    public void onDestroy() {
+        mControllers.bubbleControllers.ifPresent(
+                c -> c.bubbleBarViewController.removeBubbleBarDropTargets(this));
     }
 
     public void setDisallowGlobalDrag(boolean disallowGlobalDrag) {
@@ -463,7 +470,7 @@
             com.android.launcher3.logging.InstanceId launcherInstanceId = instanceIds.second;
 
             intent.putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, internalInstanceId);
-            if (DisplayController.isTransientTaskbar(mActivity)) {
+            if (mActivity.isTransientTaskbar()) {
                 // Tell WM Shell to ignore drag events in the provided transient taskbar region.
                 TaskbarDragLayer dragLayer = mControllers.taskbarActivityContext.getDragLayer();
                 int[] locationOnScreen = dragLayer.getLocationOnScreen();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 55ecc37..1e193f6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -34,7 +34,6 @@
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.util.DimensionUtils;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.TouchController;
 
@@ -108,19 +107,18 @@
 
         if (startAnimation != null) {
             // set taskbar background render animation boolean
-            if (DisplayController.isTransientTaskbar(mActivity)) {
+            if (mActivity.isTransientTaskbar()) {
                 mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(true);
             } else {
                 mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(true);
             }
 
-            float desiredValue = DisplayController.isTransientTaskbar(mActivity)
+            float desiredValue = mActivity.isTransientTaskbar()
                     ? PINNING_TRANSIENT
                     : PINNING_PERSISTENT;
 
-            float nonDesiredvalue = !DisplayController.isTransientTaskbar(mActivity)
-                    ? PINNING_TRANSIENT
-                    : PINNING_PERSISTENT;
+            float nonDesiredvalue =
+                    !mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT;
 
             ObjectAnimator objectAnimator = mTaskbarBackgroundProgress.animateToValue(
                     nonDesiredvalue, desiredValue);
@@ -133,9 +131,8 @@
             }));
 
         } else {
-            mTaskbarBackgroundProgress.updateValue(DisplayController.isTransientTaskbar(mActivity)
-                    ? PINNING_TRANSIENT
-                    : PINNING_PERSISTENT);
+            mTaskbarBackgroundProgress.updateValue(
+                    mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT);
         }
 
         mBgTaskbar.value = 1;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index d624413..7a23006 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -155,7 +155,7 @@
     fun maybeShowSwipeEdu() {
         if (
             !isTooltipEnabled ||
-                !DisplayController.isTransientTaskbar(activityContext) ||
+                !activityContext.isTransientTaskbar ||
                 tooltipStep > TOOLTIP_STEP_SWIPE
         ) {
             return
@@ -200,7 +200,7 @@
             suggestionsAnim.supportLightTheme()
             pinningAnim.supportLightTheme()
             handleEduAnimations(listOf(splitscreenAnim, suggestionsAnim, pinningAnim))
-            if (DisplayController.isTransientTaskbar(activityContext)) {
+            if (activityContext.isTransientTaskbar) {
                 splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient)
                 suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient)
                 pinningEdu.visibility = if (enableTaskbarPinning()) VISIBLE else GONE
@@ -230,7 +230,7 @@
             // Set up layout parameters.
             content.updateLayoutParams { width = MATCH_PARENT }
             updateLayoutParams<MarginLayoutParams> {
-                if (DisplayController.isTransientTaskbar(activityContext)) {
+                if (activityContext.isTransientTaskbar) {
                     width =
                         resources.getDimensionPixelSize(
                             if (enableTaskbarPinning())
@@ -263,7 +263,7 @@
         // for the original 2 edu steps) as a proxy to needing to show the separate pinning edu
         if (
             !enableTaskbarPinning() ||
-                !DisplayController.isTransientTaskbar(activityContext) ||
+                !activityContext.isTransientTaskbar ||
                 !isTooltipEnabled ||
                 tooltipStep > TOOLTIP_STEP_PINNING ||
                 tooltipStep < TOOLTIP_STEP_FEATURES
@@ -289,7 +289,7 @@
             pinningAnim.supportLightTheme()
             handleEduAnimations(listOf(pinningAnim))
             updateLayoutParams<BaseDragLayer.LayoutParams> {
-                if (DisplayController.isTransientTaskbar(activityContext)) {
+                if (activityContext.isTransientTaskbar) {
                     bottomMargin += activityContext.deviceProfile.taskbarHeight
                 }
                 // Unlike other tooltips, we want to align with taskbar divider rather than center.
@@ -344,7 +344,7 @@
 
             showDisclosureText(eduSubtitle)
             updateLayoutParams<BaseDragLayer.LayoutParams> {
-                if (DisplayController.isTransientTaskbar(activityContext)) {
+                if (activityContext.isTransientTaskbar) {
                     bottomMargin += activityContext.deviceProfile.taskbarHeight
                 }
                 // Unlike other tooltips, we want to align with the all apps button rather than
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index a8ce10f..3af2ab6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -235,8 +235,7 @@
                         context.resources,
                     )
                 val isPinnedTaskbar =
-                    context.deviceProfile.isTaskbarPresent &&
-                        !context.deviceProfile.isTransientTaskbar
+                    context.deviceProfile.isTaskbarPresent && !context.isTransientTaskbar
                 val mandatoryGestureHeight = if (isPinnedTaskbar) contentHeight else gestureHeight
                 provider.insetsSize =
                     getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, endRotation)
@@ -388,10 +387,7 @@
                 bubbleBarVisible
         ) {
             // Taskbar has some touchable elements, take over the full taskbar area
-            if (
-                controllers.uiController.isInOverviewUi &&
-                    DisplayController.isTransientTaskbar(context)
-            ) {
+            if (controllers.uiController.isInOverviewUi && context.isTransientTaskbar) {
                 val region =
                     controllers.taskbarActivityContext.dragLayer.lastDrawnTransientRect.toRegion()
                 val bubbleBarBounds =
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index cc340ce..e8e5b30 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -15,8 +15,8 @@
  */
 package com.android.launcher3.taskbar;
 
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
 import static android.content.Context.RECEIVER_EXPORTED;
+import static android.content.Context.RECEIVER_NOT_EXPORTED;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
@@ -25,6 +25,7 @@
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
+import static com.android.launcher3.taskbar.growth.GrowthConstants.BROADCAST_SHOW_NUDGE;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
@@ -32,7 +33,6 @@
 import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
-import static com.android.launcher3.taskbar.growth.GrowthConstants.BROADCAST_SHOW_NUDGE;
 import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
 
@@ -81,6 +81,7 @@
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.util.GroupTask;
@@ -628,7 +629,7 @@
     /** Creates a {@link TaskbarUIController} to use with non default displays. */
     private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) {
         debugPrimaryTaskbar("createTaskbarUIControllerForNonDefaultDisplay");
-        if (RecentsDisplayModel.enableOverviewInWindow()) {
+        if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
             RecentsViewContainer rvc = mRecentsDisplayModel.getRecentsWindowManager(displayId);
             if (rvc != null) {
                 return createTaskbarUIControllerForRecentsViewContainer(rvc);
@@ -732,8 +733,7 @@
                         displayId);
                 taskbar.updateDeviceProfile(dp);
             }
-            mSharedState.startTaskbarVariantIsTransient =
-                    DisplayController.isTransientTaskbar(taskbar);
+            mSharedState.startTaskbarVariantIsTransient = taskbar.isTransientTaskbar();
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
             taskbar.init(mSharedState, duration);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 4a7e4f0..bf73f02 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -31,7 +31,6 @@
 
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
-import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 
@@ -93,7 +92,8 @@
             // There is no scrim for the bar in the phone mode.
             return;
         }
-        if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) {
+        boolean isTransient = mActivity.isTransientTaskbar();
+        if (isBubbleBarEnabled() && isTransient) {
             // These scrims aren't used if bubble bar & transient taskbar are active.
             return;
         }
@@ -112,7 +112,7 @@
         boolean showScrimForBubbles = bubblesExpanded
                 && !mTaskbarVisible
                 && isBubbleControllersPresented
-                && !DisplayController.isTransientTaskbar(mActivity)
+                && !mActivity.isTransientTaskbar()
                 && !bubbleControllers.bubbleStashController.isBubblesShowingOnHome();
         return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible()
                 && !isShadeVisible
@@ -122,8 +122,8 @@
     }
 
     private float computeScrimAlpha() {
-        final boolean isPersistentTaskBarVisible =
-                mTaskbarVisible && !DisplayController.isTransientTaskbar(mScrimView.getContext());
+        boolean isTransient = mActivity.isTransientTaskbar();
+        final boolean isPersistentTaskBarVisible = mTaskbarVisible && !isTransient;
         final boolean manageMenuExpanded =
                 (mSysUiStateFlags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
         if (isPersistentTaskBarVisible && manageMenuExpanded) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java
index f87c21e..fa35a03 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java
@@ -27,7 +27,6 @@
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController;
-import com.android.launcher3.util.DisplayController;
 
 import java.io.PrintWriter;
 
@@ -47,7 +46,7 @@
 
     public TaskbarSpringOnStashController(TaskbarActivityContext context) {
         mContext = context;
-        mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
+        mIsTransientTaskbar = context.isTransientTaskbar();
         mStartVelocityPxPerS = context.getResources()
                 .getDimension(R.dimen.transient_taskbar_stash_spring_velocity_dp_per_s);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 95724ad..5284edd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -346,7 +346,7 @@
                 StashedHandleViewController.ALPHA_INDEX_STASHED);
         mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale();
 
-        boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
+        boolean isTransientTaskbar = mActivity.isTransientTaskbar();
         boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
         boolean isStashedInAppAuto =
                 isTransientTaskbar && !mTaskbarSharedState.getTaskbarWasPinned();
@@ -367,9 +367,7 @@
 
         // Hide the background while stashed so it doesn't show on fast swipes home
         boolean shouldHideTaskbarBackground = mActivity.isPhoneMode() ||
-                (enableScalingRevealHomeAnimation()
-                        && DisplayController.isTransientTaskbar(mActivity)
-                        && isStashed());
+                (enableScalingRevealHomeAnimation() && isTransientTaskbar && isStashed());
 
         mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1);
 
@@ -414,8 +412,7 @@
         if (DisplayController.isPinnedTaskbar(mActivity)) {
             return PINNED_TASKBAR_TRANSITION_DURATION;
         }
-        return DisplayController.isTransientTaskbar(mActivity)
-                ? TRANSIENT_TASKBAR_STASH_DURATION
+        return mActivity.isTransientTaskbar() ? TRANSIENT_TASKBAR_STASH_DURATION
                 : TASKBAR_STASH_DURATION;
     }
 
@@ -509,8 +506,8 @@
      * @see android.view.WindowInsets.Type#systemBars()
      */
     public int getContentHeightToReportToApps() {
-        if (mActivity.isUserSetupComplete() && (mActivity.isPhoneGestureNavMode()
-                || DisplayController.isTransientTaskbar(mActivity))) {
+        boolean isTransient = mActivity.isTransientTaskbar();
+        if (mActivity.isUserSetupComplete() && (mActivity.isPhoneGestureNavMode() || isTransient)) {
             return getStashedHeight();
         }
 
@@ -577,8 +574,7 @@
      */
     public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow,
             boolean delayTaskbarBackground) {
-        if (!DisplayController.isTransientTaskbar(mActivity)
-                || mActivity.isBubbleBarOnPhone()) {
+        if (!mActivity.isTransientTaskbar() || mActivity.isBubbleBarOnPhone()) {
             return;
         }
 
@@ -646,7 +642,7 @@
 
     /** Toggles the Taskbar's stash state. */
     public void toggleTaskbarStash() {
-        if (!DisplayController.isTransientTaskbar(mActivity) || !hasAnyFlag(FLAGS_IN_APP)) return;
+        if (!mActivity.isTransientTaskbar() || !hasAnyFlag(FLAGS_IN_APP)) return;
         updateAndAnimateTransientTaskbar(!hasAnyFlag(FLAG_STASHED_IN_APP_AUTO));
     }
 
@@ -697,8 +693,7 @@
         mAnimator = new AnimatorSet();
         addJankMonitorListener(
                 mAnimator, /* expanding= */ !isStashed, /* tag= */ jankTag);
-        boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
-        final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar
+        final float stashTranslation = mActivity.isPhoneMode() || mActivity.isTransientTaskbar()
                 ? 0
                 : (mUnstashedHeight - mStashedHeight);
 
@@ -722,7 +717,7 @@
             return;
         }
 
-        if (isTransientTaskbar) {
+        if (mActivity.isTransientTaskbar()) {
             createTransientAnimToIsStashed(mAnimator, isStashed, duration,
                     shouldDelayBackground, animationType);
         } else {
@@ -1144,7 +1139,7 @@
 
         boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW)
                 && hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED)
-                && DisplayController.isTransientTaskbar(mActivity);
+                && mActivity.isTransientTaskbar();
         updateStateForFlag(FLAG_STASHED_SYSUI,
                 hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING) || stashForBubbles);
         updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED,
@@ -1174,7 +1169,7 @@
      * <p>Do not stash if a system gesture is started.
      */
     private boolean shouldStashForIme() {
-        if (DisplayController.isTransientTaskbar(mActivity)) {
+        if (mActivity.isTransientTaskbar()) {
             return false;
         }
         // Do not stash if in small screen, with 3 button nav, and in landscape.
@@ -1270,7 +1265,7 @@
      */
     public void setUpTaskbarSystemAction(boolean visible) {
         UI_HELPER_EXECUTOR.execute(() -> {
-            if (!visible || !DisplayController.isTransientTaskbar(mActivity)
+            if (!visible || !mActivity.isTransientTaskbar()
                     || mActivity.isPhoneMode()) {
                 mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR);
                 mIsTaskbarSystemActionRegistered = false;
@@ -1321,7 +1316,7 @@
      *                            If false, attempts to re/start the timeout
      */
     public void updateTaskbarTimeout(boolean isAutohideSuspended) {
-        if (!DisplayController.isTransientTaskbar(mActivity)) {
+        if (!mActivity.isTransientTaskbar()) {
             return;
         }
         if (isAutohideSuspended) {
@@ -1335,9 +1330,7 @@
      * Attempts to start timer to auto hide the taskbar based on time.
      */
     private void tryStartTaskbarTimeout() {
-        if (!DisplayController.isTransientTaskbar(mActivity)
-                || mIsStashed
-                || mEnableBlockingTimeoutDuringTests) {
+        if (!mActivity.isTransientTaskbar() || mIsStashed || mEnableBlockingTimeoutDuringTests) {
             return;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
index deaf024..df10d24 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.touch.SingleAxisSwipeDetector
 import com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE
 import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.TouchController
 import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
 
@@ -39,7 +38,7 @@
 class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController {
 
     private val activity: TaskbarActivityContext = controllers.taskbarActivityContext
-    private val enabled = DisplayController.isTransientTaskbar(activity)
+    private val enabled = activity.isTransientTaskbar
     private val swipeDownDetector: SingleAxisSwipeDetector
     private val translationCallback = controllers.taskbarTranslationController.transitionCallback
     /** Interpolator to apply resistance as user swipes down to the bottom of the screen. */
@@ -67,7 +66,7 @@
         val gestureHeight: Int =
             ResourceUtils.getNavbarSize(
                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
-                activity.resources
+                activity.resources,
             )
         gestureHeightYThreshold = (activity.deviceProfile.heightPx - gestureHeight).toFloat()
     }
@@ -89,7 +88,7 @@
                         maxTouchDisplacement,
                         0f,
                         maxVisualDisplacement,
-                        displacementInterpolator
+                        displacementInterpolator,
                     )
                 )
                 return false
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
index 5a5d6d0..13fb296 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -29,7 +29,6 @@
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.util.DisplayController;
 
 import java.io.PrintWriter;
 
@@ -63,7 +62,7 @@
 
     public TaskbarTranslationController(TaskbarActivityContext context) {
         mContext = context;
-        mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
+        mIsTransientTaskbar = mContext.isTransientTaskbar();
         mCallback = new TransitionCallback();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index ea0b81e..061a5a1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -37,7 +37,6 @@
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.util.SplitTask;
 import com.android.quickstep.views.RecentsView;
@@ -116,9 +115,8 @@
      */
     public void hideOverlayWindow() {
         mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
-
-        if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext)
-                || mControllers.taskbarAllAppsController.isOpen()) {
+        boolean isTransientTaskbar = mControllers.taskbarActivityContext.isTransientTaskbar();
+        if (!isTransientTaskbar || 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 07b77c9..a6e05c9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -61,7 +61,6 @@
 import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
 import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
 import com.android.launcher3.uioverrides.PredictedAppIcon;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.util.GroupTask;
@@ -184,8 +183,9 @@
         setWillNotDraw(false);
 
         mAllAppsButtonContainer = new TaskbarAllAppsButtonContainer(context);
-        mAllAppsButtonTranslationOffset =  (int) getResources().getDimension(
-                mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(isTransientTaskbar()));
+        mAllAppsButtonTranslationOffset = (int) getResources().getDimension(
+                mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(
+                        mActivityContext.isTransientTaskbar()));
 
         if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
             mTaskbarDividerContainer = new TaskbarDividerContainer(context);
@@ -200,10 +200,7 @@
         // TODO: Disable touch events on QSB otherwise it can crash.
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
 
-        mNumStaticViews =
-                ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() && !mActivityContext.isPhoneMode()
-                        ? addStaticViews()
-                        : 0;
+        mNumStaticViews = ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() ? addStaticViews() : 0;
     }
 
     /**
@@ -243,7 +240,7 @@
                 enableTaskbarPinning() && !mActivityContext.isThreeButtonNav();
         availableWidth -= iconSize - (int) getResources().getDimension(
                 mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(
-                        forceTransientTaskbarSize || isTransientTaskbar()));
+                        forceTransientTaskbarSize || mActivityContext.isTransientTaskbar()));
         ++additionalIcons;
 
         return Math.floorDiv(availableWidth, iconSize) + additionalIcons;
@@ -478,7 +475,7 @@
         }
 
         // Recents divider takes priority.
-        if (!mAddedDividerForRecents && !mActivityContext.areDesktopTasksVisible()) {
+        if (!mAddedDividerForRecents && !mActivityContext.isInDesktopMode()) {
             updateAllAppsDivider();
         }
     }
@@ -1101,11 +1098,6 @@
         // Ignore, we just implement Insettable to draw behind system insets.
     }
 
-    private boolean isTransientTaskbar() {
-        return DisplayController.isTransientTaskbar(mActivityContext)
-                && !mActivityContext.isPhoneMode();
-    }
-
     public boolean areIconsVisible() {
         // Consider the overall visibility
         return getVisibility() == VISIBLE;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index a80e2c4..605171a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -267,9 +267,8 @@
                 : mActivity.getDeviceProfile().taskbarHeight;
 
         mTaskbarIconScaleForStash.updateValue(1f);
-        float pinningValue = DisplayController.isTransientTaskbar(mActivity)
-                ? PINNING_TRANSIENT
-                : PINNING_PERSISTENT;
+        float pinningValue =
+                mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT;
         mTaskbarIconScaleForPinning.updateValue(pinningValue);
         mTaskbarIconTranslationYForPinning.updateValue(pinningValue);
         mTaskbarIconTranslationXForPinning.updateValue(pinningValue);
@@ -936,7 +935,7 @@
         mOnControllerPreCreateCallback.run();
         DeviceProfile taskbarDp = mActivity.getDeviceProfile();
         Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
-        boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
+        boolean isTransientTaskbar = mActivity.isTransientTaskbar();
 
         float scaleUp = ((float) launcherDp.iconSizePx) / taskbarDp.taskbarIconSize;
         int borderSpacing = launcherDp.hotseatBorderSpace;
diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
index c380c8d..c7d42b1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
@@ -22,7 +22,6 @@
 import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.views.BaseDragLayer
 import com.android.systemui.animation.ViewRootSync
 import java.io.PrintWriter
@@ -41,8 +40,7 @@
 class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
     TaskbarControllers.LoggableTaskbarController, TaskbarControllers.BackgroundRendererController {
 
-    private val isSeparateBackgroundEnabled =
-        !DisplayController.isTransientTaskbar(context) && !context.isPhoneMode
+    private val isSeparateBackgroundEnabled = !context.isTransientTaskbar && !context.isPhoneMode
     private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context)
     private val nonTouchableInsetsComputer =
         ViewTreeObserver.OnComputeInternalInsetsListener {
@@ -97,7 +95,7 @@
         separateWindowLayoutParams =
             context.createDefaultWindowLayoutParams(
                 TYPE_APPLICATION_OVERLAY,
-                TEMP_BACKGROUND_WINDOW_TITLE
+                TEMP_BACKGROUND_WINDOW_TITLE,
             )
         separateWindowLayoutParams?.isSystemApplicationOverlay = true
     }
@@ -163,7 +161,7 @@
                 // First add the temporary window, then hide the overlapping taskbar background.
                 context.addWindowView(
                     separateWindowForTaskbarBackground,
-                    separateWindowLayoutParams
+                    separateWindowLayoutParams,
                 );
                 { controllers.taskbarDragLayerController.setIsBackgroundDrawnElsewhere(true) }
             } else {
@@ -179,7 +177,7 @@
                 ViewRootSync.synchronizeNextDraw(
                     separateWindowForTaskbarBackground!!,
                     context.dragLayer,
-                    onWindowsSynchronized
+                    onWindowsSynchronized,
                 )
             }
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
index 52f7176..f1ccd39 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
@@ -31,7 +31,6 @@
 import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
-import com.android.launcher3.util.DisplayController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 import java.util.Optional;
@@ -90,7 +89,7 @@
     }
 
     private void setUpTaskbarStashing() {
-        if (DisplayController.isTransientTaskbar(mContext)) {
+        if (mContext.isTransientTaskbar()) {
             mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, true);
             mTaskbarStashController.applyState();
         }
@@ -103,7 +102,7 @@
             AbstractFloatingView.closeOpenContainer(
                     mContext, AbstractFloatingView.TYPE_ACTION_POPUP);
 
-            if (DisplayController.isTransientTaskbar(mContext)) {
+            if (mContext.isTransientTaskbar()) {
                 mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false);
                 mTaskbarStashController.applyState();
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt
index 383f4d2..24e7d99 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt
@@ -60,8 +60,6 @@
 
     override fun onDrop(dragObject: DropTarget.DragObject, options: DragOptions) {
         val itemInfo = dragObject.dragInfo ?: return
-        // TODO(b/397459664) : fix task bar icon animation after drop
-        // TODO(b/397459664) : update bubble bar location
         bubbleBarDragListener.onLauncherItemDroppedOverBubbleBarDragZone(
             bubbleBarLocation,
             itemInfo,
@@ -77,8 +75,6 @@
     }
 
     override fun onDragExit(dragObject: DropTarget.DragObject) {
-        // TODO(b/397459664) : fix the issue for no bubbles, when moving task bar icon out of
-        // the bubble bar drag zone drag ends and swipes gesture swipes the overview
         if (!isShowingDropTarget) return
         isShowingDropTarget = false
         bubbleBarDragListener.onLauncherItemDraggedOutsideBubbleBarDropZone()
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 277dbbf..5b1d3fb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -41,15 +41,16 @@
 import androidx.annotation.Nullable;
 
 import com.android.app.animation.Interpolators;
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
-import com.android.launcher3.taskbar.TaskbarDragController;
 import com.android.launcher3.taskbar.TaskbarInsetsController;
 import com.android.launcher3.taskbar.TaskbarSharedState;
 import com.android.launcher3.taskbar.TaskbarStashController;
@@ -144,6 +145,7 @@
         @Override
         public void onLauncherItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location,
                 @NonNull ItemInfo itemInfo) {
+            AbstractFloatingView.closeAllOpenViews(mActivity);
             if (itemInfo instanceof WorkspaceItemInfo) {
                 ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) itemInfo).getDeepShortcutInfo();
                 if (shortcutInfo != null) {
@@ -193,13 +195,13 @@
     private boolean mShouldShowEducation;
     public boolean mOverflowAdded;
     private boolean mIsLocationUpdatedForDropTarget = false;
+    private boolean mWasStashedBeforeEnteringBubbleDragZone = false;
 
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
     private final FrameLayout mBubbleBarContainer;
     private BubbleBarFlyoutController mBubbleBarFlyoutController;
     private BubbleBarPinController mBubbleBarPinController;
     private TaskbarSharedState mTaskbarSharedState;
-    private TaskbarDragController mTaskbarDragController;
     private final BubbleBarLocationDropTarget mBubbleBarLeftDropTarget;
     private final BubbleBarLocationDropTarget mBubbleBarRightDropTarget;
     private final TimeSource mTimeSource = System::currentTimeMillis;
@@ -236,7 +238,6 @@
     /** Initializes controller. */
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers,
             TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) {
-        mTaskbarDragController = controllers.taskbarDragController;
         mTaskbarSharedState = controllers.getSharedState();
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
@@ -273,7 +274,7 @@
                         mBoundsChangeListener.onBoundsChanged();
                     }
                 });
-        float pinningValue = DisplayController.isTransientTaskbar(mActivity)
+        float pinningValue = mActivity.isTransientTaskbar()
                 ? PINNING_TRANSIENT
                 : PINNING_PERSISTENT;
         mBubbleBarPinning.updateValue(pinningValue);
@@ -338,8 +339,18 @@
                 mBubbleBarController.updateBubbleBarLocation(location, source);
             }
         };
-        mTaskbarDragController.addDropTarget(mBubbleBarLeftDropTarget);
-        mTaskbarDragController.addDropTarget(mBubbleBarRightDropTarget);
+    }
+
+    /** Adds bubble bar locations drop zones to the drag controller. */
+    public void addBubbleBarDropTargets(DragController<?> dragController) {
+        dragController.addDropTarget(mBubbleBarLeftDropTarget);
+        dragController.addDropTarget(mBubbleBarRightDropTarget);
+    }
+
+    /** Removes bubble bar locations drop zones to the drag controller. */
+    public void removeBubbleBarDropTargets(DragController<?> dragController) {
+        dragController.removeDropTarget(mBubbleBarLeftDropTarget);
+        dragController.removeDropTarget(mBubbleBarRightDropTarget);
     }
 
     /** Returns animated float property responsible for pinning transition animation. */
@@ -617,14 +628,30 @@
      * updated.
      */
     public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+        Log.w("BBAnimation", "onDragItemOverBubbleBarDragZone()");
         mBarView.showDropTarget(/* isDropTarget = */ true);
         boolean isRtl = mBarView.isLayoutRtl();
         mIsLocationUpdatedForDropTarget = getBubbleBarLocation().isOnLeft(isRtl)
                 != bubbleBarLocation.isOnLeft(isRtl);
-        if (mIsLocationUpdatedForDropTarget) {
-            animateBubbleBarLocation(bubbleBarLocation);
-        }
-        if (!hasBubbles()) {
+        mWasStashedBeforeEnteringBubbleDragZone = hasBubbles()
+            && mBubbleStashController.isStashed();
+        if (mWasStashedBeforeEnteringBubbleDragZone) {
+            if (mIsLocationUpdatedForDropTarget) {
+                // bubble bar is stashed an location updated
+                //TODO(b/399678274) add animation
+                mBubbleStashController.showBubbleBarImmediate();
+                animateBubbleBarLocation(bubbleBarLocation);
+            } else {
+                // bubble bar is stashed and location the same - just un-stash
+                mBubbleStashController.showBubbleBar(/* expandBubbles = */ false);
+            }
+        } else if (hasBubbles()) {
+            if (mIsLocationUpdatedForDropTarget) {
+                // bubble bar has bubbles and location is changed - animate bar to the opposite side
+                animateBubbleBarLocation(bubbleBarLocation);
+            }
+        } else {
+            // bubble bar has no bubbles flow just show the empty drop target
             mBubbleBarPinController.showDropTarget(bubbleBarLocation);
         }
     }
@@ -644,12 +671,27 @@
      * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false.
      */
     public void onItemDraggedOutsideBubbleBarDropZone() {
+        Log.w("BBAnimation", "onItemDraggedOutsideBubbleBarDropZone()");
         mBarView.showDropTarget(/* isDropTarget = */ false);
-        if (mIsLocationUpdatedForDropTarget) {
-            animateBubbleBarLocation(getBubbleBarLocation());
+        if (mWasStashedBeforeEnteringBubbleDragZone) {
+            if (mIsLocationUpdatedForDropTarget) {
+                // bubble bar was stashed and location updated
+                //TODO(b/399678274) add animation
+                animateBubbleBarLocation(getBubbleBarLocation());
+                mBubbleStashController.stashBubbleBarImmediate();
+            } else {
+                // bubble bar was stashed and location the same - just stash it back
+                mBubbleStashController.stashBubbleBar();
+            }
+        } else if (hasBubbles()) {
+            if (mIsLocationUpdatedForDropTarget) {
+                // bubble bar has bubbles and location was changed - return to the original location
+                animateBubbleBarLocation(getBubbleBarLocation());
+            }
         }
         mBubbleBarPinController.hideDropTarget();
         mIsLocationUpdatedForDropTarget = false;
+        mWasStashedBeforeEnteringBubbleDragZone = false;
     }
 
     /**
@@ -657,9 +699,11 @@
      * The controller will hide the drop target if there are no bubbles and exit drop target mode.
      */
     public void onItemDroppedInBubbleBarDragZone() {
+        Log.w("BBAnimation", "onItemDroppedInBubbleBarDragZone()");
         mBarView.showDropTarget(/* isDropTarget = */ false);
         mBubbleBarPinController.hideDropTarget();
         mIsLocationUpdatedForDropTarget = false;
+        mWasStashedBeforeEnteringBubbleDragZone = false;
     }
 
     /**
@@ -1356,8 +1400,6 @@
     /** Called when the controller is destroyed. */
     public void onDestroy() {
         adjustTaskbarAndHotseatToBubbleBarState(/*isBubbleBarExpanded = */false);
-        mTaskbarDragController.removeDropTarget(mBubbleBarLeftDropTarget);
-        mTaskbarDragController.removeDropTarget(mBubbleBarRightDropTarget);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index 4932654..82b1295 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -32,7 +32,6 @@
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarViewCallbacks
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.views.ActivityContext
 import com.android.launcher3.views.IconButtonView
@@ -69,7 +68,7 @@
             )
         backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
         setIconDrawable(drawable)
-        if (!DisplayController.isTransientTaskbar(context)) {
+        if (activityContext.isTransientTaskbar) {
             setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
         }
         setForegroundTint(activityContext.getColor(R.color.all_apps_button_color))
@@ -106,7 +105,7 @@
 
     @DimenRes
     fun getAllAppsButtonTranslationXOffset(isTransientTaskbar: Boolean): Int {
-        return if (isTransientTaskbar) {
+        return if (isTransientTaskbar && activityContext.isTransientTaskbar) {
             R.dimen.transient_taskbar_all_apps_button_translation_x_offset
         } else {
             R.dimen.taskbar_all_apps_search_button_translation_x_offset
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
index d5f72d5..060ce46 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
@@ -26,7 +26,6 @@
 import com.android.launcher3.Utilities.dpToPx
 import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.TaskbarViewCallbacks
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.views.ActivityContext
 import com.android.launcher3.views.IconButtonView
 
@@ -52,7 +51,7 @@
         backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
         val drawable = resources.getDrawable(R.drawable.taskbar_divider_button)
         setIconDrawable(drawable)
-        if (!DisplayController.isTransientTaskbar(context)) {
+        if (!activityContext.isTransientTaskbar) {
             setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index f130d29..a14c5a4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -19,7 +19,6 @@
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.launcher3.util.DisplayController
 
 /** Evaluates all the features taskbar can have. */
 class TaskbarFeatureEvaluator
@@ -36,7 +35,7 @@
         get() = enableTaskbarPinning() || isRecentsEnabled
 
     val isTransient: Boolean
-        get() = DisplayController.isTransientTaskbar(taskbarActivityContext)
+        get() = taskbarActivityContext.isTransientTaskbar
 
     val isLandscape: Boolean
         get() = taskbarActivityContext.deviceProfile.isLandscape
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java b/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java
index bbf08bf..844f1af 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java
@@ -194,6 +194,7 @@
                 event.offsetLocation(mTouchingChild.getWidth() / 2 - x,
                         mTouchingChild.getHeight() / 2 - y);
                 return mTouchingChild.getVisibility() == VISIBLE
+                        && mTouchingChild.isAttachedToWindow()
                         && mTouchingChild.dispatchTouchEvent(event);
             }
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
index 55bb0f9..dd91d17 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
@@ -55,7 +55,7 @@
             Context windowContext,
             TaskbarActivityContext taskbarContext,
             TaskbarControllers controllers) {
-        super(windowContext);
+        super(windowContext, taskbarContext.isPrimaryDisplay());
         mTaskbarContext = taskbarContext;
         mOverlayController = controllers.taskbarOverlayController;
         mDragController = new TaskbarDragController(this);
@@ -67,6 +67,11 @@
         onViewCreated();
     }
 
+    /** Called when the controller is destroyed. */
+    public void onDestroy() {
+        mDragController.onDestroy();
+    }
+
     public @Nullable TaskbarSearchSessionController getSearchSessionController() {
         return mSearchSessionController;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
index 79cb748..8b01e99 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
@@ -150,9 +150,13 @@
     /** Destroys the controller and any overlay window if present. */
     public void onDestroy() {
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
-        Optional.ofNullable(mOverlayContext)
-                .map(c -> c.getSystemService(WindowManager.class))
-                .ifPresent(m -> m.removeViewImmediate(mOverlayContext.getDragLayer()));
+        Optional.ofNullable(mOverlayContext).ifPresent(c -> {
+            c.onDestroy();
+            WindowManager wm = c.getSystemService(WindowManager.class);
+            if (wm != null) {
+                wm.removeViewImmediate(mOverlayContext.getDragLayer());
+            }
+        });
         mOverlayContext = null;
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index 669850c..41694ec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -34,7 +34,6 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
 
@@ -147,7 +146,7 @@
      * 2) Sets tappableInsets bottom inset to 0.
      */
     private WindowInsets updateInsetsDueToStashing(WindowInsets oldInsets) {
-        if (!DisplayController.isTransientTaskbar(mContainer)) {
+        if (!mContainer.isTransientTaskbar()) {
             return oldInsets;
         }
         WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets);
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 914855b..4280baf 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -82,6 +82,7 @@
                             taskKey.numActivities,
                             taskKey.isTopActivityNoDisplay,
                             taskKey.isActivityStackTransparent,
+                            taskKey.userId,
                         ) -> null
 
                         !taskContainer.task.isDockable -> null
diff --git a/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt
index f97cf9c..3b823f5 100644
--- a/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt
@@ -80,6 +80,7 @@
                             taskKey.numActivities,
                             taskKey.isTopActivityNoDisplay,
                             taskKey.isActivityStackTransparent,
+                            taskKey.userId,
                         ) -> null
 
                         else -> {
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 74aa8e2..c4ba2d5 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -38,10 +38,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
@@ -323,8 +323,7 @@
      */
     public boolean useSyntheticRecentsTransition() {
         return mRunningTask.isHomeTask()
-                && (Flags.enableFallbackOverviewInWindow()
-                        || Flags.enableLauncherOverviewInWindow());
+                && RecentsWindowFlags.Companion.getEnableOverviewInWindow();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 783ec2c..2ff42cd 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -220,7 +220,7 @@
             mHandler.post(() -> {
                 LauncherBackAnimationController controller = mControllerRef.get();
                 if (controller != null) {
-                    controller.startBack(backEvent);
+                    controller.initBackMotion(backEvent);
                     mProgressAnimator.onBackStarted(backEvent, event -> {
                         float backProgress = event.getProgress();
                         controller.mBackProgress =
@@ -270,6 +270,7 @@
                 }
             }
             controller.mAnimationFinishedCallback = finishedCallback;
+            controller.startBack();
         }
 
         @Override
@@ -294,34 +295,32 @@
         mBackCallback = null;
     }
 
-    private void startBack(BackMotionEvent backEvent) {
+    private void initBackMotion(BackMotionEvent backEvent) {
         // in case we're still animating an onBackCancelled event, let's remove the finish-
         // callback from the progress animator to prevent calling finishAnimation() before
         // restarting a new animation
-        // Side note: startBack is never called during the post-commit phase if the back gesture
-        // was committed (not cancelled). BackAnimationController prevents that. Therefore we
-        // don't have to handle that case.
+        // Side note: initBackMotion is never called during the post-commit phase if the back
+        // gesture was committed (not cancelled). BackAnimationController prevents that. Therefore
+        // we don't have to handle that case.
         mProgressAnimator.removeOnBackCancelledFinishCallback();
 
         mBackInProgress = true;
-        RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
-
-        if (appTarget == null || appTarget.leash == null || !appTarget.leash.isValid()) {
+        mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+    }
+    private void startBack() {
+        if (mBackTarget == null) {
             return;
         }
 
         mTransaction
-                .show(appTarget.leash)
+                .show(mBackTarget.leash)
                 .setAnimationTransaction();
-        mBackTarget = appTarget;
-        mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
-        mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
+        mStartRect.set(mBackTarget.windowConfiguration.getMaxBounds());
 
         // inset bottom in case of taskbar being present
         if (!predictiveBackThreeButtonNav() || mLauncher.getDeviceProfile().isTaskbarPresent
                 || DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) {
-            mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+            mStartRect.inset(0, 0, 0, mBackTarget.contentInsets.bottom);
         }
 
         mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 984f390..bf87291 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -30,9 +30,7 @@
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj
 import com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays
-import com.android.launcher3.Flags.enableFallbackOverviewInWindow
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
-import com.android.launcher3.Flags.enableLauncherOverviewInWindow
 import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
 import com.android.launcher3.PagedView
 import com.android.launcher3.logger.LauncherAtom
@@ -53,6 +51,7 @@
 import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
 import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
 import com.android.quickstep.fallback.window.RecentsDisplayModel
+import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.ActiveGestureProtoLogProxy
 import com.android.quickstep.views.RecentsView
@@ -299,7 +298,7 @@
 
         val focusedDisplayId = focusState.focusedDisplayId
         val focusedDisplayUIController: TaskbarUIController? =
-            if (RecentsDisplayModel.enableOverviewInWindow()) {
+            if (enableOverviewInWindow) {
                 Log.d(
                     TAG,
                     "Querying RecentsDisplayModel for TaskbarUIController for display: $focusedDisplayId",
@@ -392,9 +391,7 @@
             return false
         }
 
-        val recentsInWindowFlagSet =
-            enableFallbackOverviewInWindow() || enableLauncherOverviewInWindow()
-        if (!recentsInWindowFlagSet) {
+        if (!enableOverviewInWindow) {
             containerInterface.getCreatedContainer()?.rootView?.let { view ->
                 InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
             }
@@ -425,7 +422,7 @@
                     transitionInfo: TransitionInfo?,
                 ) {
                     Log.d(TAG, "recents animation started: $command")
-                    if (recentsInWindowFlagSet) {
+                    if (enableOverviewInWindow) {
                         containerInterface.getCreatedContainer()?.rootView?.let { view ->
                             InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
                         }
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 7eacef3..7d3a1da 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays;
 import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.fallback.window.RecentsWindowFlags.enableLauncherOverviewInWindow;
 import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
 
 import android.content.ActivityNotFoundException;
@@ -41,7 +42,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.dagger.LauncherAppComponent;
@@ -50,6 +50,7 @@
 import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
@@ -180,7 +181,7 @@
             mDefaultDisplayContainerInterface.onAssistantVisibilityChanged(0.f);
         }
 
-        if (SEPARATE_RECENTS_ACTIVITY.get() || Flags.enableLauncherOverviewInWindow()) {
+        if (SEPARATE_RECENTS_ACTIVITY.get() || enableLauncherOverviewInWindow.isTrue()) {
             mIsDefaultHome = false;
             if (defaultHome == null) {
                 defaultHome = mMyHomeIntent.getComponent();
@@ -203,7 +204,7 @@
             unregisterOtherHomeAppUpdateReceiver();
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
-            if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
+            if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
                 mDefaultDisplayContainerInterface =
                         mRecentsDisplayModel.getFallbackWindowInterface(DEFAULT_DISPLAY);
             } else {
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index ac88e5a..cc5b2da 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -38,8 +38,10 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.ExternalDisplaysKt;
 import com.android.quickstep.util.GroupTask;
@@ -70,7 +72,10 @@
 /**
  * Manages the recent task list from the system, caching it as necessary.
  */
-public class RecentTasksList {
+// TODO: b/401602554 - Consider letting [DesktopTasksController] notify [RecentTasksController] of
+//  desk changes to trigger [IRecentTasksListener.onRecentTasksChanged()], instead of implementing
+//  [DesktopVisibilityListener].
+public class RecentTasksList implements WindowManagerProxy.DesktopVisibilityListener {
 
     private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
 
@@ -78,6 +83,7 @@
     private final KeyguardManager mKeyguardManager;
     private final LooperExecutor mMainThreadExecutor;
     private final SystemUiProxy mSysUiProxy;
+    private final DesktopVisibilityController mDesktopVisibilityController;
 
     // The list change id, increments as the task list changes in the system
     private int mChangeId;
@@ -95,13 +101,16 @@
 
     public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
             KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
-            TopTaskTracker topTaskTracker) {
+            TopTaskTracker topTaskTracker,
+            DesktopVisibilityController desktopVisibilityController,
+            DaggerSingletonTracker tracker) {
         mContext = context;
         mMainThreadExecutor = mainThreadExecutor;
         mKeyguardManager = keyguardManager;
         mChangeId = 1;
         mSysUiProxy = sysUiProxy;
-        sysUiProxy.registerRecentTasksListener(new IRecentTasksListener.Stub() {
+        mDesktopVisibilityController = desktopVisibilityController;
+        final IRecentTasksListener recentTasksListener = new IRecentTasksListener.Stub() {
             @Override
             public void onRecentTasksChanged() throws RemoteException {
                 mMainThreadExecutor.execute(RecentTasksList.this::onRecentTasksChanged);
@@ -147,7 +156,19 @@
                     topTaskTracker.onVisibleTasksChanged(visibleTasks);
                 });
             }
-        });
+        };
+
+        mSysUiProxy.registerRecentTasksListener(recentTasksListener);
+        tracker.addCloseable(
+                () -> mSysUiProxy.unregisterRecentTasksListener(recentTasksListener));
+
+        if (DesktopModeStatus.enableMultipleDesktops(mContext)) {
+            mDesktopVisibilityController.registerDesktopVisibilityListener(
+                    this);
+            tracker.addCloseable(
+                    () -> mDesktopVisibilityController.unregisterDesktopVisibilityListener(this));
+        }
+
         // We may receive onRunningTaskAppeared events later for tasks which have already been
         // included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
         // onRunningTaskVanished for tasks not included in the returned list. These cases will be
@@ -286,6 +307,27 @@
         return mRunningTasks;
     }
 
+    @Override
+    public void onDeskAdded(int displayId, int deskId) {
+        onRecentTasksChanged();
+    }
+
+    @Override
+    public void onDeskRemoved(int displayId, int deskId) {
+        onRecentTasksChanged();
+    }
+
+    @Override
+    public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+        // Should desk activation changes lead to the invalidation of the loaded tasks? The cases
+        // are:
+        // - Switching from one active desk to another.
+        // - Switching from out of a desk session into an active desk.
+        // - Switching from an active desk to a non-desk session.
+        // These changes don't affect the list of desks, nor their contents, so let's ignore them
+        // for now.
+    }
+
     private void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
         // Make sure this task is not already in the list
         for (RunningTaskInfo existingTask : mRunningTasks) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index d7152b5..ecde37b 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -34,9 +34,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -113,8 +113,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()
-                || Flags.enableLauncherOverviewInWindow()) || isOpeningHome)) {
+        if (appCount == 0 && (!RecentsWindowFlags.Companion.getEnableOverviewInWindow()
+                || 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/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index cf7e499..0deb1ca 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -59,6 +59,10 @@
         if (!DesktopModeStatus.canEnterDesktopMode(context)) {
             return false;
         }
+        // TODO: b/400866688 - Check if we need to update this such that for an empty desk, we
+        //  receive a list of apps that contain only the Launcher and the `DesktopWallpaperActivity`
+        //  and both are fullscreen windowing mode. A desk can also have transparent modals and
+        //  immersive apps which may not have a "freeform" windowing mode.
         for (RemoteAnimationTarget target : apps) {
             if (target.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
                 return true;
diff --git a/quickstep/src/com/android/quickstep/RecentsFilterState.java b/quickstep/src/com/android/quickstep/RecentsFilterState.java
index c4b0f25..1808a97 100644
--- a/quickstep/src/com/android/quickstep/RecentsFilterState.java
+++ b/quickstep/src/com/android/quickstep/RecentsFilterState.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.quickstep.util.DesksUtils;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
@@ -117,37 +118,43 @@
      * Returns a predicate for filtering out GroupTasks by package name.
      *
      * @param packageName package name to filter GroupTasks by
-     *                    if null, Predicate filters out desktop tasks with no non-minimized tasks.
+     *                    if null, Predicate filters out desktop tasks with no non-minimized tasks,
+     *                    unless the multiple desks feature is enabled, which allows empty desks.
      */
     public static Predicate<GroupTask> getFilter(@Nullable String packageName) {
         if (packageName == null) {
-            return getEmptyDesktopTaskFilter();
+            return getDesktopTaskFilter();
         }
 
         return (groupTask) -> (groupTask.containsPackage(packageName)
-                && !isDestopTaskWithMinimizedTasksOnly(groupTask));
+                && shouldKeepGroupTask(groupTask));
     }
 
     /**
-     * Returns a predicate that filters out desk tasks that contain no non-minimized desktop tasks.
+     * Returns a predicate that filters out desk tasks that contain no non-minimized desktop tasks,
+     * unless the multiple desks feature is enabled, which allows empty desks.
      */
-    public static Predicate<GroupTask> getEmptyDesktopTaskFilter() {
-        return (groupTask -> !isDestopTaskWithMinimizedTasksOnly(groupTask));
+    public static Predicate<GroupTask> getDesktopTaskFilter() {
+        return (groupTask -> shouldKeepGroupTask(groupTask));
     }
 
     /**
-     * Whether the provided task is a desktop task with no non-minimized tasks - returns true if the
-     * desktop task has no tasks at all.
+     * Returns true if the given `groupTask` should be kept, and false if it should be filtered out.
+     * Desks will be filtered out if they are empty unless the multiple desks feature is enabled.
      *
      * @param groupTask The group task to check.
      */
-    static boolean isDestopTaskWithMinimizedTasksOnly(GroupTask groupTask) {
+    private static boolean shouldKeepGroupTask(GroupTask groupTask) {
         if (groupTask.taskViewType != TaskViewType.DESKTOP) {
-            return false;
+            return true;
         }
+
+        if (DesksUtils.areMultiDesksFlagsEnabled()) {
+            return true;
+        }
+
         return groupTask.getTasks().stream()
-                .filter(task -> !task.isMinimized)
-                .toList().isEmpty();
+                .anyMatch(task -> !task.isMinimized);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 1d83d42..e1adf3d 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
@@ -61,6 +62,8 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -72,8 +75,6 @@
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 /**
  * Singleton class to load and manage recents model.
  */
@@ -104,12 +105,14 @@
             DisplayController displayController,
             LockedUserState lockedUserState,
             Lazy<ThemeManager> themeManagerLazy,
+            DesktopVisibilityController desktopVisibilityController,
             DaggerSingletonTracker tracker
             ) {
         // Lazily inject the ThemeManager and access themeManager once the device is
         // unlocked. See b/393248495 for details.
         this(context, new IconProvider(context), systemUiProxy, topTaskTracker,
-                displayController, lockedUserState,themeManagerLazy, tracker);
+                displayController, lockedUserState, themeManagerLazy, desktopVisibilityController,
+                tracker);
     }
 
     @SuppressLint("VisibleForTests")
@@ -120,6 +123,7 @@
             DisplayController displayController,
             LockedUserState lockedUserState,
             Lazy<ThemeManager> themeManagerLazy,
+            DesktopVisibilityController desktopVisibilityController,
             DaggerSingletonTracker tracker) {
         this(context,
                 new RecentTasksList(
@@ -127,7 +131,7 @@
                         MAIN_EXECUTOR,
                         context.getSystemService(KeyguardManager.class),
                         systemUiProxy,
-                        topTaskTracker),
+                        topTaskTracker, desktopVisibilityController, tracker),
                 new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider, displayController),
                 new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
                 iconProvider,
@@ -205,7 +209,7 @@
     @Override
     public int getTasks(@Nullable Consumer<List<GroupTask>> callback) {
         return mTaskList.getTasks(false /* loadKeysOnly */, callback,
-                RecentsFilterState.getEmptyDesktopTaskFilter());
+                RecentsFilterState.getDesktopTaskFilter());
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index d6f6540..506f85d 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -1096,18 +1096,26 @@
     // Desktop Mode
     //
     /** Calls shell to create a new desk (if possible) on the display whose ID is `displayId`. */
-    fun createDesktop(displayId: Int) =
+    fun createDesk(displayId: Int) =
         executeWithErrorLog({ "Failed call createDesk" }) { desktopMode?.createDesk(displayId) }
 
     /**
      * Calls shell to activate the desk whose ID is `deskId` on whatever display it exists on. This
      * will bring all tasks on this desk to the front.
      */
-    fun activateDesktop(deskId: Int, transition: RemoteTransition?) =
+    fun activateDesk(deskId: Int, transition: RemoteTransition?) =
         executeWithErrorLog({ "Failed call activateDesk" }) {
             desktopMode?.activateDesk(deskId, transition)
         }
 
+    /** Calls shell to remove the desk whose ID is `deskId`. */
+    fun removeDesk(deskId: Int) =
+        executeWithErrorLog({ "Failed call removeDesk" }) { desktopMode?.removeDesk(deskId) }
+
+    /** Calls shell to remove all the available desks on all displays. */
+    fun removeAllDesks() =
+        executeWithErrorLog({ "Failed call removeAllDesks" }) { desktopMode?.removeAllDesks() }
+
     /** Call shell to show all apps active on the desktop */
     fun showDesktopApps(displayId: Int, transition: RemoteTransition?) =
         executeWithErrorLog({ "Failed call showDesktopApps" }) {
@@ -1159,9 +1167,9 @@
         }
 
     /** Call shell to remove the desktop that is on given `displayId` */
-    fun removeDesktop(displayId: Int) =
-        executeWithErrorLog({ "Failed call removeDesktop" }) {
-            desktopMode?.removeDesktop(displayId)
+    fun removeDefaultDeskInDisplay(displayId: Int) =
+        executeWithErrorLog({ "Failed call removeDefaultDeskInDisplay" }) {
+            desktopMode?.removeDefaultDeskInDisplay(displayId)
         }
 
     /** Call shell to move a task with given `taskId` to external display. */
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 64a8c25..1c7f23c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -42,12 +42,12 @@
 import androidx.annotation.UiThread;
 
 import com.android.internal.util.ArrayUtils;
-import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.SystemUiFlagUtils;
@@ -63,7 +63,6 @@
 public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
     public static final boolean SHELL_TRANSITIONS_ROTATION =
             SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
-
     private final Context mCtx;
     private RecentsAnimationController mController;
     private RecentsAnimationCallbacks mCallbacks;
@@ -313,8 +312,7 @@
         }
 
         if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
-                && (Flags.enableFallbackOverviewInWindow()
-                        || Flags.enableLauncherOverviewInWindow())) {
+                && RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
             mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent, options,
                     mCallbacks, gestureState.useSyntheticRecentsTransition());
             RecentsDisplayModel.getINSTANCE().get(mCtx)
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ba662c4..741ae7d 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -94,6 +94,7 @@
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsDisplayModel.RecentsDisplayResource;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
 import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
@@ -1081,10 +1082,9 @@
     }
 
     public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
-        boolean recentsInWindow =
-                Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
         return mOverviewComponentObserver.isHomeAndOverviewSame()
-                ? mLauncherSwipeHandlerFactory : (recentsInWindow
+                ? mLauncherSwipeHandlerFactory
+                : (RecentsWindowFlags.Companion.getEnableOverviewInWindow()
                 ? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
     }
 
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d8662f2..8ec97ed 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -30,7 +30,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Flags;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -46,6 +45,7 @@
 import com.android.quickstep.GestureState;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SingleTask;
 import com.android.quickstep.util.SplitSelectStateController;
@@ -80,9 +80,10 @@
 
     @Override
     public BaseContainerInterface<RecentsState, ?> getContainerInterface(int displayId) {
-        return (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow())
+        return RecentsWindowFlags.Companion.getEnableOverviewInWindow()
                 ? RecentsDisplayModel.getINSTANCE().get(mContext)
-                .getFallbackWindowInterface(displayId) : FallbackActivityInterface.INSTANCE;
+                        .getFallbackWindowInterface(displayId)
+                : FallbackActivityInterface.INSTANCE;
     }
 
     @Override
@@ -290,8 +291,7 @@
         }
 
         // disabling this so app icons aren't drawn on top of recent tasks.
-        if (isOverlayEnabled && !(Flags.enableFallbackOverviewInWindow()
-                || Flags.enableLauncherOverviewInWindow())) {
+        if (isOverlayEnabled && !RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
             runActionOnRemoteHandles(remoteTargetHandle ->
                     remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 58c6c50..12dc177 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import android.view.Display
 import androidx.core.util.valueIterator
-import com.android.launcher3.Flags
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.DaggerSingletonObject
@@ -29,6 +28,7 @@
 import com.android.quickstep.FallbackWindowInterface
 import com.android.quickstep.dagger.QuickstepBaseAppComponent
 import com.android.quickstep.fallback.window.RecentsDisplayModel.RecentsDisplayResource
+import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -50,14 +50,10 @@
             DaggerSingletonObject<RecentsDisplayModel>(
                 QuickstepBaseAppComponent::getRecentsDisplayModel
             )
-
-        @JvmStatic
-        fun enableOverviewInWindow() =
-            Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()
     }
 
     init {
-        if (enableOverviewInWindow()) {
+        if (enableOverviewInWindow) {
             registerDisplayListener()
             tracker.addCloseable { destroy() }
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
index 333571c..d70d7eb 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -18,7 +18,7 @@
 
 import android.content.Context
 import android.graphics.PixelFormat
-import android.view.Display.DEFAULT_DISPLAY
+import android.view.Display
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
@@ -53,7 +53,7 @@
 
     fun initDeviceProfile() {
         deviceProfile =
-            if (displayId == DEFAULT_DISPLAY)
+            if (displayId == Display.DEFAULT_DISPLAY)
                 InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
             else InvariantDeviceProfile.INSTANCE[this].createDeviceProfileForSecondaryDisplay(this)
     }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt
new file mode 100644
index 0000000..9953154
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.fallback.window
+
+import android.window.DesktopModeFlags.DesktopModeFlag
+import com.android.launcher3.Flags
+
+class RecentsWindowFlags {
+    companion object {
+        @JvmField
+        val enableLauncherOverviewInWindow: DesktopModeFlag =
+            DesktopModeFlag(Flags::enableLauncherOverviewInWindow, false)
+
+        @JvmField
+        val enableFallbackOverviewInWindow: DesktopModeFlag =
+            DesktopModeFlag(Flags::enableFallbackOverviewInWindow, false)
+
+        @JvmStatic
+        val enableOverviewInWindow
+            get() = enableLauncherOverviewInWindow.isTrue || enableFallbackOverviewInWindow.isTrue
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 7cae5b8..c8cf58c 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -42,7 +42,6 @@
 
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.TestLogging;
@@ -59,6 +58,7 @@
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
@@ -438,10 +438,8 @@
             notifyGestureStarted(true /*isLikelyToStartNewTask*/);
         } else {
             // todo differentiate intent based on if we are on home or in app for overview in window
-            boolean useHomeIntentForWindow = Flags.enableFallbackOverviewInWindow()
-                    || Flags.enableLauncherOverviewInWindow();
-            Intent intent = new Intent(useHomeIntentForWindow ? mInteractionHandler.getHomeIntent()
-                : mInteractionHandler.getLaunchIntent());
+            Intent intent = new Intent(RecentsWindowFlags.Companion.getEnableOverviewInWindow()
+                    ? mInteractionHandler.getHomeIntent() : mInteractionHandler.getLaunchIntent());
             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
                     mInteractionHandler);
diff --git a/quickstep/src/com/android/quickstep/util/DesksUtils.kt b/quickstep/src/com/android/quickstep/util/DesksUtils.kt
new file mode 100644
index 0000000..ccfdbb9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/DesksUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.content.Context
+import android.window.DesktopExperienceFlags
+import com.android.systemui.shared.recents.model.Task
+
+class DesksUtils {
+    companion object {
+        @JvmStatic
+        fun areMultiDesksFlagsEnabled() =
+            DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() &&
+                DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_FRONTEND.isTrue()
+
+        /** Returns true if this [task] contains the [DesktopWallpaperActivity]. */
+        @JvmStatic
+        fun isDesktopWallpaperTask(context: Context, task: Task): Boolean {
+            val sysUiPackage =
+                context.getResources().getString(com.android.internal.R.string.config_systemUi)
+            val component = task.key.component
+            if (component != null) {
+                return component.className.contains("DesktopWallpaperActivity") &&
+                    component.packageName.contains(sysUiPackage)
+            }
+            return false
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 1d035e9..27657b4 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -35,6 +35,7 @@
 import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.testing.TestLogging
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.RunnableList
@@ -68,6 +69,8 @@
         type = TaskViewType.DESKTOP,
         thumbnailFullscreenParams = DesktopFullscreenDrawParams(context),
     ) {
+    var deskId = DesktopVisibilityController.INACTIVE_DESK_ID
+
     private val contentViewFullscreenParams = FullscreenDrawParams(context)
 
     private val taskContentViewPool =
@@ -281,6 +284,7 @@
         orientedState: RecentsOrientedState,
         taskOverlayFactory: TaskOverlayFactory,
     ) {
+        deskId = desktopTask.deskId
         // TODO(b/370495260): Minimized tasks should not be filtered with desktop exploded view
         // support.
         // Minimized tasks should not be shown in Overview.
@@ -332,12 +336,18 @@
 
     override fun onRecycle() {
         super.onRecycle()
+        deskId = DesktopVisibilityController.INACTIVE_DESK_ID
         explodeProgress = 0.0f
         viewModel = null
         visibility = VISIBLE
         taskContainers.forEach { removeAndRecycleThumbnailView(it) }
     }
 
+    override fun setOrientationState(orientationState: RecentsOrientedState) {
+        super.setOrientationState(orientationState)
+        iconView.setIconOrientation(orientationState, isGridTask)
+    }
+
     @SuppressLint("RtlHardcoded")
     override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) {
         super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index c282e77..1f3fbf9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -56,6 +56,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
+import static com.android.launcher3.statehandlers.DesktopVisibilityController.INACTIVE_DESK_ID;
 import static com.android.launcher3.testing.shared.TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE;
 import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -64,6 +65,7 @@
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
 import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.util.DesksUtils.areMultiDesksFlagsEnabled;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
@@ -200,6 +202,7 @@
 import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.recents.data.RecentTasksRepository;
 import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
@@ -253,11 +256,11 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
-
 /**
  * A list of recent tasks.
  *
@@ -1924,12 +1927,16 @@
             return;
         }
 
-        int[] currentTaskIds;
         TaskView currentTaskView = getTaskViewAt(mCurrentPage);
-        if (currentTaskView != null) {
+        int[] currentTaskIds = null;
+        // Track the current DesktopTaskView through [deskId] as a desk can be empty without any
+        // tasks.
+        int currentTaskViewDeskId = INACTIVE_DESK_ID;
+        if (areMultiDesksFlagsEnabled()
+                && currentTaskView instanceof DesktopTaskView desktopTaskView) {
+            currentTaskViewDeskId = desktopTaskView.getDeskId();
+        } else if (currentTaskView != null) {
             currentTaskIds = currentTaskView.getTaskIds();
-        } else {
-            currentTaskIds = new int[0];
         }
 
         // Unload existing visible task data
@@ -1941,9 +1948,19 @@
 
         // Save running task ID if it exists before rebinding all taskViews, otherwise the task from
         // the runningTaskView currently bound could get assigned to another TaskView
-        int[] runningTaskIds = getTaskIdsForTaskViewId(mRunningTaskViewId);
-        int[] focusedTaskIds = getTaskIdsForTaskViewId(mFocusedTaskViewId);
+        TaskView runningTaskView = getRunningTaskView();
+        int[] runningTaskIds = null;
 
+        // Track the running TaskView through [deskId] as a desk can be empty without any tasks.
+        int runningTaskViewDeskId = INACTIVE_DESK_ID;
+        if (areMultiDesksFlagsEnabled()
+                && runningTaskView instanceof DesktopTaskView desktopTaskView) {
+            runningTaskViewDeskId = desktopTaskView.getDeskId();
+        } else if (runningTaskView != null) {
+            runningTaskIds = runningTaskView.getTaskIds();
+        }
+
+        int[] focusedTaskIds = getTaskIdsForTaskViewId(mFocusedTaskViewId);
         // Reset the focused task to avoiding initializing TaskViews layout as focused task during
         // binding. The focused task view will be updated after all the TaskViews are bound.
         setFocusedTaskViewId(INVALID_TASK_ID);
@@ -2050,24 +2067,24 @@
         updateTaskSize();
         updateChildTaskOrientations();
 
-        TaskView newRunningTaskView = null;
-        if (hasAllValidTaskIds(runningTaskIds)) {
+        TaskView newRunningTaskView = mUtils.getDesktopTaskViewForDeskId(runningTaskViewDeskId);
+        if (newRunningTaskView == null) {
             // Update mRunningTaskViewId to be the new TaskView that was assigned by binding
             // the full list of tasks to taskViews
             newRunningTaskView = getTaskViewByTaskIds(runningTaskIds);
-            if (newRunningTaskView != null) {
-                setRunningTaskViewId(newRunningTaskView.getTaskViewId());
+        }
+        if (newRunningTaskView != null) {
+            setRunningTaskViewId(newRunningTaskView.getTaskViewId());
+        } else {
+            if (mActiveGestureRunningTasks != null) {
+                // This will update mRunningTaskViewId and create a stub view if necessary.
+                // We try to avoid this because it can cause a scroll jump, but it is needed
+                // for cases where the running task isn't included in this load plan (e.g. if
+                // the current running task is excludedFromRecents.)
+                showCurrentTask(mActiveGestureRunningTasks, "applyLoadPlan");
+                newRunningTaskView = getRunningTaskView();
             } else {
-                if (mActiveGestureRunningTasks != null) {
-                    // This will update mRunningTaskViewId and create a stub view if necessary.
-                    // We try to avoid this because it can cause a scroll jump, but it is needed
-                    // for cases where the running task isn't included in this load plan (e.g. if
-                    // the current running task is excludedFromRecents.)
-                    showCurrentTask(mActiveGestureRunningTasks, "applyLoadPlan");
-                    newRunningTaskView = getRunningTaskView();
-                } else {
-                    setRunningTaskViewId(INVALID_TASK_ID);
-                }
+                setRunningTaskViewId(INVALID_TASK_ID);
             }
         }
 
@@ -2075,11 +2092,12 @@
         if (mNextPage != INVALID_PAGE) {
             // Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll.
             mCurrentPage = previousCurrentPage;
-            if (hasAllValidTaskIds(currentTaskIds)) {
+            currentTaskView = mUtils.getDesktopTaskViewForDeskId(currentTaskViewDeskId);
+            if (currentTaskView == null) {
                 currentTaskView = getTaskViewByTaskIds(currentTaskIds);
-                if (currentTaskView != null) {
-                    targetPage = indexOfChild(currentTaskView);
-                }
+            }
+            if (currentTaskView != null) {
+                targetPage = indexOfChild(currentTaskView);
             }
         } else if (previousFocusedPage != INVALID_PAGE) {
             targetPage = previousFocusedPage;
@@ -2826,9 +2844,13 @@
     /**
      * Called when a gesture from an app is starting.
      */
+    // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity` from being
+    //  considered in Overview.
     public void onGestureAnimationStart(Task[] runningTasks) {
         Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
         mActiveGestureRunningTasks = runningTasks;
+
+
         // This needs to be called before the other states are set since it can create the task view
         if (mOrientationState.setGestureActive(true)) {
             reapplyActiveRotation();
@@ -3062,6 +3084,21 @@
     }
 
     /**
+     * Creates a `DesktopTaskView` for the currently active desk on this display, which contains the
+     * gievn `runningTasks`.
+     */
+    private DesktopTaskView createDesktopTaskViewForActiveDesk(Task[] runningTasks) {
+        final int activeDeskId = mUtils.getActiveDeskIdOnThisDisplay();
+        final var desktopTaskView = (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+
+        // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity`.
+        desktopTaskView.bind(
+                new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
+                mOrientationState, mTaskOverlayFactory);
+        return desktopTaskView;
+    }
+
+    /**
      * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
      *
      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
@@ -3075,20 +3112,14 @@
         }
 
         int runningTaskViewId = -1;
-        boolean needGroupTaskView = runningTasks.length > 1;
-        boolean needDesktopTask = hasDesktopTask(runningTasks);
         if (shouldAddStubTaskView(runningTasks)) {
             boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView;
+            final boolean needGroupTaskView = runningTasks.length > 1;
+            final boolean needDesktopTask = hasDesktopTask(runningTasks);
             if (needDesktopTask) {
-                final int activeDeskId =
-                        DesktopVisibilityController.INSTANCE.get(mContext).getActiveDeskId(
-                                mContainer.getDisplay().getDisplayId());
-                taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
-                ((DesktopTaskView) taskView).bind(
-                        new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
-                        mOrientationState, mTaskOverlayFactory);
+                taskView = createDesktopTaskViewForActiveDesk(runningTasks);
             } else if (needGroupTaskView) {
                 taskView = getTaskViewFromPool(TaskViewType.GROUPED);
                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
@@ -3114,8 +3145,11 @@
             measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
                     makeMeasureSpec(getMeasuredHeight(), EXACTLY));
             layout(getLeft(), getTop(), getRight(), getBottom());
-        } else if (getTaskViewByTaskId(runningTasks[0].key.id) != null) {
-            runningTaskViewId = getTaskViewByTaskId(runningTasks[0].key.id).getTaskViewId();
+        } else {
+            var runningTaskView = getTaskViewByTaskId(runningTasks[0].key.id);
+            if (runningTaskView != null) {
+                runningTaskViewId = runningTaskView.getTaskViewId();
+            }
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -3154,6 +3188,10 @@
                 return true;
             }
         }
+
+        // A running empty desk will have a single running app for the `DesktopWallpaperActivity`.
+        // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity`.
+
         return false;
     }
 
@@ -4525,24 +4563,33 @@
         return lastVisibleTaskView;
     }
 
-  private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
-    UI_HELPER_EXECUTOR
-        .getHandler()
-        .post(
-            () -> {
-              if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
-                  && dismissedTaskView instanceof DesktopTaskView) {
-                // TODO: b/362720497 - Use the api with desktop id instead.
-                SystemUiProxy.INSTANCE
+    private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
+        UI_HELPER_EXECUTOR
+                .getHandler()
+                .post(
+                        () -> {
+                            if (dismissedTaskView instanceof DesktopTaskView desktopTaskView) {
+                                removeDesktopTaskView(desktopTaskView);
+                            } else {
+                                for (int taskId : dismissedTaskView.getTaskIds()) {
+                                    ActivityManagerWrapper.getInstance().removeTask(taskId);
+                                }
+                            }
+                        });
+    }
+
+    private void removeDesktopTaskView(DesktopTaskView desktopTaskView) {
+        if (areMultiDesksFlagsEnabled()) {
+            SystemUiProxy.INSTANCE
                     .get(getContext())
-                    .removeDesktop(mContainer.getDisplay().getDisplayId());
-              } else {
-                for (int taskId : dismissedTaskView.getTaskIds()) {
-                    ActivityManagerWrapper.getInstance().removeTask(taskId);
-                }
-              }
-            });
-  }
+                    .removeDesk(desktopTaskView.getDeskId());
+        } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+            SystemUiProxy.INSTANCE
+                    .get(getContext())
+                    .removeDefaultDeskInDisplay(
+                            mContainer.getDisplay().getDisplayId());
+        }
+    }
 
     protected void onDismissAnimationEnds() {
         AccessibilityManagerCompat.sendTestProtocolEventToTest(getContext(),
@@ -4562,6 +4609,12 @@
         mPendingAnimation = anim;
         mPendingAnimation.addEndListener(isSuccess -> {
             if (isSuccess) {
+                // Remove desktops first, since desks can be empty (so they have no recent tasks),
+                // and closing all tasks on a desk doesn't always necessarily mean that the desk
+                // will be removed. So, there are no guarantees that the below call to
+                // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough.
+                SystemUiProxy.INSTANCE.get(getContext()).removeAllDesks();
+
                 // Remove all the task views now
                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
                     UI_HELPER_EXECUTOR.getHandler().post(
@@ -4691,7 +4744,7 @@
     private void createDesk(View view) {
         SystemUiProxy.INSTANCE
                 .get(getContext())
-                .createDesktop(mContainer.getDisplay().getDisplayId());
+                .createDesk(mContainer.getDisplay().getDisplayId());
     }
 
     @Override
@@ -6026,7 +6079,7 @@
         // mSyncTransactionApplier doesn't get transferred over
         runActionOnRemoteHandles(remoteTargetHandle -> {
             final TransformParams params = remoteTargetHandle.getTransformParams();
-            if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+            if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
                 params.setHomeBuilderProxy((builder, app, transformParams) -> {
                     mTmpMatrix.setScale(
                             1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
@@ -6924,6 +6977,58 @@
         // TODO: b/389209338 - update the AddDesktopButton's visibility on this.
     }
 
+    @Override
+    public void onDeskAdded(int displayId, int deskId) {
+        // Ignore desk changes that don't belong to this display.
+        if (displayId != mContainer.getDisplay().getDisplayId()) {
+            return;
+        }
+
+        if (mUtils.getDesktopTaskViewForDeskId(deskId) != null) {
+            Log.e(TAG, "A task view for this desk has already been added.");
+            return;
+        }
+
+        // We assume that a newly added desk is always empty and gets added to the left of the
+        // `AddNewDesktopButton`.
+        DesktopTaskView desktopTaskView =
+                (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+        desktopTaskView.bind(new DesktopTask(deskId, new ArrayList<>()),
+                mOrientationState, mTaskOverlayFactory);
+
+        Objects.requireNonNull(mAddDesktopButton);
+        final int insertionIndex = 1 + indexOfChild(mAddDesktopButton);
+        addView(desktopTaskView, insertionIndex);
+
+        updateTaskSize();
+        updateChildTaskOrientations();
+
+        // TODO: b/401002178 - Recalculate the new current page such that the addition of the new
+        //  desk does not result in a change in the current scroll page.
+    }
+
+    @Override
+    public void onDeskRemoved(int displayId, int deskId) {
+        // Ignore desk changes that don't belong to this display.
+        if (displayId != mContainer.getDisplay().getDisplayId()) {
+            return;
+        }
+
+        // We need to distinguish between desk removals that are triggered from outside of overview
+        // vs. the ones that were initiated from overview by dismissing the corresponding desktop
+        // task view.
+        var taskView = mUtils.getDesktopTaskViewForDeskId(deskId);
+        if (taskView != null) {
+            dismissTaskView(taskView, true, true);
+        }
+    }
+
+    @Override
+    public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+        // TODO: b/400870600 - We may need to add code here to special case when an empty desk gets
+        // activated, since `RemoteDesktopLaunchTransitionRunner` doesn't always get triggered.
+    }
+
     /** Get the color used for foreground scrimming the RecentsView for sharing. */
     public static int getForegroundScrimDimColor(Context context) {
         return context.getColor(R.color.overview_foreground_scrim_color);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 1c37986..29601ef 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -22,7 +22,11 @@
 import androidx.core.view.children
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID
 import com.android.launcher3.util.IntArray
+import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
+import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
 import com.android.quickstep.util.isExternalDisplay
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
@@ -52,7 +56,12 @@
      * @return Sorted list of GroupTasks to be used in the RecentsView.
      */
     fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
-        val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+        var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+        if (areMultiDesksFlagsEnabled()) {
+            // Desk IDs of newer desks are larger than those of older desks, hence we can use them
+            // to sort desks from old to new.
+            desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId }
+        }
         return otherTasks + desktopTasks
     }
 
@@ -114,6 +123,22 @@
             it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
         }
 
+    /**
+     * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist.
+     */
+    fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? {
+        if (deskId == INACTIVE_DESK_ID) {
+            return null
+        }
+        return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId }
+            as? DesktopTaskView
+    }
+
+    /** Returns the active desk ID of the display that contains the [recentsView] instance. */
+    fun getActiveDeskIdOnThisDisplay(): Int =
+        DesktopVisibilityController.INSTANCE.get(recentsView.context)
+            .getActiveDeskId(recentsView.mContainer.display.displayId)
+
     /** Returns the expected focus task. */
     fun getFirstNonDesktopTaskView(): TaskView? =
         if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
@@ -140,6 +165,17 @@
     private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile
 
     fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int {
+        if (areMultiDesksFlagsEnabled() && runningTaskView is DesktopTaskView) {
+            // Use the [deskId] to keep desks in the order of their creation, as a newer desk
+            // always has a larger [deskId] than the older desks.
+            val desktopTaskView =
+                taskViews.firstOrNull {
+                    it is DesktopTaskView &&
+                        it.deskId != INACTIVE_DESK_ID &&
+                        it.deskId <= runningTaskView.deskId
+                }
+            if (desktopTaskView != null) return recentsView.indexOfChild(desktopTaskView)
+        }
         val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView())
         return if (getDeviceProfile().isTablet) {
             var index = firstTaskViewIndex
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
index 7c762f4..6bc0666 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
@@ -22,7 +22,6 @@
 import android.content.Context
 import android.graphics.Outline
 import android.graphics.Rect
-import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.ShapeDrawable
 import android.graphics.drawable.shapes.RectShape
 import android.util.AttributeSet
@@ -165,7 +164,18 @@
             recentsViewContainer.layoutInflater.inflate(R.layout.task_view_menu_option, this, false)
                 as LinearLayout
         if (enableOverviewIconMenu()) {
-            (menuOptionView.background as GradientDrawable).cornerRadius = 0f
+            menuOptionView.background =
+                ResourcesCompat.getDrawable(
+                    resources,
+                    R.drawable.app_chip_menu_item_bg,
+                    context.theme,
+                )
+            menuOptionView.foreground =
+                ResourcesCompat.getDrawable(
+                    resources,
+                    R.drawable.app_chip_menu_item_fg,
+                    context.theme,
+                )
         }
         menuOption.setIconAndLabelFor(
             menuOptionView.findViewById(R.id.icon),
@@ -366,13 +376,10 @@
             deviceProfile = recentsViewContainer.deviceProfile,
             taskMenuX = translationX,
             taskMenuY =
-                when {
-                    !enableOverviewIconMenu() -> translationY
-                    // Bottom menu can translate up to show more options. So we use the min
-                    // translation allowed to calculate its max height.
-                    taskView.isOnGridBottomRow() -> minMenuTop
-                    else -> menuTranslationYBeforeOpen
-                },
+                // Bottom menu can translate up to show more options. So we use the min
+                // translation allowed to calculate its max height.
+                if (enableOverviewIconMenu() && taskView.isOnGridBottomRow()) minMenuTop
+                else translationY,
         )
 
     private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index b7f1d1d..29793ad 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -33,7 +33,6 @@
 import android.view.Display
 import android.view.MotionEvent
 import android.view.View
-import android.view.View.OnClickListener
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.view.accessibility.AccessibilityNodeInfo
diff --git a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
index c319cb1..cb7254f 100644
--- a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
@@ -16,22 +16,25 @@
 
 package com.android.launcher3.util;
 
-import static com.android.launcher3.Flags.enableStateManagerProtoLog;
 import static com.android.quickstep.util.QuickstepProtoLogGroup.LAUNCHER_STATE_MANAGER;
 import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized;
 
+import android.window.DesktopModeFlags.DesktopModeFlag;
+
 import androidx.annotation.NonNull;
 
 import com.android.internal.protolog.ProtoLog;
+import com.android.launcher3.Flags;
 
 /**
  * Proxy class used for StateManager ProtoLog support.
  */
 public class StateManagerProtoLogProxy {
-
+    private static final DesktopModeFlag ENABLE_STATE_MANAGER_PROTO_LOG =
+            new DesktopModeFlag(Flags::enableStateManagerProtoLog, true);
     public static void logGoToState(
             @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) {
-        if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(LAUNCHER_STATE_MANAGER,
                 "StateManager.goToState: fromState: %s, toState: %s, partial trace:\n%s",
                 fromState,
@@ -41,7 +44,7 @@
 
     public static void logCreateAtomicAnimation(
             @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) {
-        if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.createAtomicAnimation: "
                         + "fromState: %s, toState: %s, partial trace:\n%s",
                 fromState,
@@ -50,17 +53,17 @@
     }
 
     public static void logOnStateTransitionStart(@NonNull Object state) {
-        if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionStart: state: %s", state);
     }
 
     public static void logOnStateTransitionEnd(@NonNull Object state) {
-        if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionEnd: state: %s", state);
     }
 
     public static void logCancelAnimation(boolean animationOngoing, @NonNull String trace) {
-        if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(LAUNCHER_STATE_MANAGER,
                 "StateManager.cancelAnimation: animation ongoing: %b, partial trace:\n%s",
                 animationOngoing,
diff --git a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
index 2c9ae33..99888fb 100644
--- a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
@@ -16,14 +16,16 @@
 
 package com.android.quickstep.util;
 
-import static com.android.launcher3.Flags.enableRecentsWindowProtoLog;
 import static com.android.quickstep.util.QuickstepProtoLogGroup.RECENTS_WINDOW;
 import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized;
 
+import android.window.DesktopModeFlags;
+
 import androidx.annotation.NonNull;
 
 import com.android.internal.protolog.ProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.launcher3.Flags;
 
 /**
  * Proxy class used for Recents Window ProtoLog support.
@@ -35,19 +37,20 @@
  * method. Or, if an existing entry needs to be modified, simply update it here.
  */
 public class RecentsWindowProtoLogProxy {
-
+    private static final DesktopModeFlags.DesktopModeFlag ENABLE_RECENTS_WINDOW_PROTO_LOG =
+            new DesktopModeFlags.DesktopModeFlag(Flags::enableRecentsWindowProtoLog, true);
     public static void logOnStateSetStart(@NonNull String stateName) {
-        if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(RECENTS_WINDOW, "onStateSetStart: %s", stateName);
     }
 
     public static void logOnStateSetEnd(@NonNull String stateName) {
-        if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(RECENTS_WINDOW, "onStateSetEnd: %s", stateName);
     }
 
     public static void logStartRecentsWindow(boolean isShown, boolean windowViewIsNull) {
-        if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(RECENTS_WINDOW,
                 "Starting recents window: isShow= %b, windowViewIsNull=%b",
                 isShown,
@@ -55,7 +58,7 @@
     }
 
     public static void logCleanup(boolean isShown) {
-        if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+        if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
         ProtoLog.d(RECENTS_WINDOW, "Cleaning up recents window: isShow= %b", isShown);
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
index 09c62aa..1c7af14 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
@@ -64,7 +64,7 @@
         underTest.mHotseatState.predictor = hotseatPredictor
         underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor
         underTest.mModel = modelHelper.model
-        underTest.mDataModel = BgDataModel()
+        underTest.mDataModel = BgDataModel(WidgetsModel(modelHelper.sandboxContext))
     }
 
     @After
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index 70bf6b4..9722e9d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -49,6 +49,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.TaskViewType;
@@ -102,7 +104,9 @@
                 .thenReturn(true);
 
         mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
-                mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
+                mockKeyguardManager, mSystemUiProxy, mTopTaskTracker,
+                mock(DesktopVisibilityController.class),
+                mock(DaggerSingletonTracker.class));
     }
 
     @Test
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cc740a5..3c1a9c6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -323,6 +323,8 @@
     <string name="edit_home_screen">Edit Home Screen</string>
     <!-- Text for settings button [CHAR LIMIT=20]-->
     <string name="settings_button_text">Home settings</string>
+    <!-- Text for app menu button [CHAR LIMIT=30]-->
+    <string name="all_apps_home_screen">Apps</string>
     <!-- Message shown when a feature is disabled by the administrator -->
     <string name="msg_disabled_by_admin">Disabled by your admin</string>
 
@@ -469,6 +471,12 @@
     <!-- Accessibility action to show quick actions menu for an icon. [CHAR_LIMIT=30] -->
     <string name="action_deep_shortcut">Shortcut Menu</string>
 
+    <!-- Accessibility name for the app widget resize frame. -->
+    <string name="widget_frame_name">Widget Resize Frame for <xliff:g id="string" example="Clock">%1$s</xliff:g></string>
+
+    <!-- Accessibility action to close the widget resize frame. [CHAR_LIMIT=30] -->
+    <string name="action_close">Close</string>
+
     <!-- Accessibility action to dismiss a notification in the shortcuts menu for an icon. [CHAR_LIMIT=30] -->
     <string name="action_dismiss_notification">Dismiss</string>
 
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index b51e850..213d88f 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -216,6 +216,13 @@
         AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater()
                 .inflate(R.layout.app_widget_resize_frame, dl, false);
         frame.setupForWidget(widget, cellLayout, dl);
+        // Save widget item info as tag on resize frame; so that, the accessibility delegate can
+        // attach actions that typically happen on widget (e.g. resize, move) also on the resize
+        // frame.
+        frame.setTag(widget.getTag());
+        frame.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
+        frame.setContentDescription(launcher.asContext().getString(R.string.widget_frame_name,
+                widget.getContentDescription()));
         ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true;
 
         dl.addView(frame);
@@ -235,6 +242,13 @@
         }
     }
 
+    /**
+     *  Retrieves the view where accessibility actions happen.
+     */
+    public View getViewForAccessibility() {
+        return mWidgetView;
+    }
+
     private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
             DragLayer dragLayer) {
         mCellLayout = cellLayout;
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 783e82c..30e3a2b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED;
 import static com.android.launcher3.Flags.enableContrastTiles;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.allapps.AlphabeticalAppsList.PRIVATE_SPACE_PACKAGE;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE;
@@ -104,6 +105,7 @@
 import java.text.NumberFormat;
 import java.util.HashMap;
 import java.util.Locale;
+import java.util.Objects;
 
 /**
  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
@@ -589,7 +591,9 @@
     }
 
     private void setNonPendingIcon(ItemInfoWithIcon info) {
-        int flags = shouldUseTheme() ? FLAG_THEMED : info.bitmap.creationFlags;
+        // Set nonPendingIcon acts as a restart which should refresh the flag state when applicable.
+        int flags = Objects.equals(info.getTargetPackage(), PRIVATE_SPACE_PACKAGE)
+                ? info.bitmap.creationFlags : shouldUseTheme() ? FLAG_THEMED : 0;
         // Remove badge on icons smaller than 48dp.
         if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
             flags |= FLAG_NO_BADGE;
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index add0ad8..02d70ae 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -28,11 +28,12 @@
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.AddWorkspaceItemsTask
 import com.android.launcher3.model.AllAppsList
-import com.android.launcher3.model.BaseLauncherBinder
+import com.android.launcher3.model.BaseLauncherBinder.BaseLauncherBinderFactory
 import com.android.launcher3.model.BgDataModel
 import com.android.launcher3.model.CacheDataUpdatedTask
 import com.android.launcher3.model.ItemInstallQueue
 import com.android.launcher3.model.LoaderTask
+import com.android.launcher3.model.LoaderTask.LoaderTaskFactory
 import com.android.launcher3.model.ModelDbController
 import com.android.launcher3.model.ModelDelegate
 import com.android.launcher3.model.ModelInitializer
@@ -43,6 +44,8 @@
 import com.android.launcher3.model.ReloadStringCacheTask
 import com.android.launcher3.model.ShortcutsChangedTask
 import com.android.launcher3.model.UserLockStateChangedTask
+import com.android.launcher3.model.UserManagerState
+import com.android.launcher3.model.WorkspaceItemSpaceFinder
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.pm.UserCache
@@ -70,28 +73,23 @@
 @Inject
 constructor(
     @ApplicationContext private val context: Context,
-    private val appProvider: Provider<LauncherAppState>,
+    private val taskControllerProvider: Provider<ModelTaskController>,
     private val iconCache: IconCache,
     private val prefs: LauncherPrefs,
     private val installQueue: ItemInstallQueue,
-    appFilter: AppFilter,
     @Named("ICONS_DB") dbFileName: String?,
     initializer: ModelInitializer,
     lifecycle: DaggerSingletonTracker,
     val modelDelegate: ModelDelegate,
+    private val mBgAllAppsList: AllAppsList,
+    private val mBgDataModel: BgDataModel,
+    private val loaderFactory: LoaderTaskFactory,
+    private val binderFactory: BaseLauncherBinderFactory,
+    private val spaceFinderFactory: Provider<WorkspaceItemSpaceFinder>,
 ) {
 
     private val mCallbacksList = ArrayList<BgDataModel.Callbacks>(1)
 
-    // < only access in worker thread >
-    private val mBgAllAppsList = AllAppsList(iconCache, appFilter)
-
-    /**
-     * All the static data should be accessed on the background thread, A lock should be acquired on
-     * this object when accessing any data from this model.
-     */
-    private val mBgDataModel = BgDataModel()
-
     val modelDbController = ModelDbController(context)
 
     private val mLock = Any()
@@ -139,7 +137,7 @@
     /** Adds the provided items to the workspace. */
     fun addAndBindAddedWorkspaceItems(itemList: List<Pair<ItemInfo?, Any?>?>) {
         callbacks.forEach { it.preAddApps() }
-        enqueueModelUpdateTask(AddWorkspaceItemsTask(itemList))
+        enqueueModelUpdateTask(AddWorkspaceItemsTask(itemList, spaceFinderFactory.get()))
     }
 
     fun getWriter(
@@ -299,13 +297,7 @@
                 // Clear any pending bind-runnables from the synchronized load process.
                 callbacksList.forEach { MAIN_EXECUTOR.execute(it::clearPendingBinds) }
 
-                val launcherBinder =
-                    BaseLauncherBinder(
-                        appProvider.get(),
-                        mBgDataModel,
-                        mBgAllAppsList,
-                        callbacksList,
-                    )
+                val launcherBinder = binderFactory.createBinder(callbacksList)
                 if (bindDirectly) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
@@ -317,14 +309,7 @@
                     launcherBinder.bindWidgets()
                     return true
                 } else {
-                    val task =
-                        LoaderTask(
-                            appProvider.get(),
-                            mBgAllAppsList,
-                            mBgDataModel,
-                            this.modelDelegate,
-                            launcherBinder,
-                        )
+                    val task = loaderFactory.newLoaderTask(launcherBinder, UserManagerState())
                     mLoaderTask = task
 
                     // Always post the loader task, instead of running directly
@@ -425,7 +410,7 @@
     /** Called when the labels for the widgets has updated in the icon cache. */
     fun onWidgetLabelsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) {
         enqueueModelUpdateTask { taskController, dataModel, _ ->
-            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, appProvider.get())
+            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user)
             taskController.bindUpdatedWidgets(dataModel)
         }
     }
@@ -439,17 +424,7 @@
                 // Loader has not yet run.
                 return@execute
             }
-            task.execute(
-                ModelTaskController(
-                    appProvider.get(),
-                    mBgDataModel,
-                    mBgAllAppsList,
-                    this,
-                    MAIN_EXECUTOR,
-                ),
-                mBgDataModel,
-                mBgAllAppsList,
-            )
+            task.execute(taskControllerProvider.get(), mBgDataModel, mBgAllAppsList)
         }
     }
 
@@ -476,7 +451,7 @@
 
     fun refreshAndBindWidgetsAndShortcuts(packageUser: PackageUserKey?) {
         enqueueModelUpdateTask { taskController, dataModel, _ ->
-            dataModel.widgetsModel.update(taskController.app, packageUser)
+            dataModel.widgetsModel.update(packageUser)
             taskController.bindUpdatedWidgets(dataModel)
         }
     }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index cb3a0bc..d6ae3a6 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
+import static com.android.launcher3.graphics.ShapeDelegate.DEFAULT_PATH_SIZE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
@@ -39,6 +40,7 @@
 import android.graphics.LightingColorFilter;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.Path;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -98,6 +100,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -274,8 +277,12 @@
      */
     public static void mapCoordInSelfToDescendant(View descendant, View root, float[] coord) {
         sMatrix.reset();
+        //TODO(b/307488755) when implemented this check should be removed
+        if (!Objects.equals(descendant.getWindowId(), root.getWindowId())) {
+            return;
+        }
         View v = descendant;
-        while(v != root) {
+        while (v != root) {
             sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY());
             sMatrix.postConcat(v.getMatrix());
             sMatrix.postTranslate(v.getLeft(), v.getTop());
@@ -632,7 +639,7 @@
 
         Drawable badge = null;
         if ((info instanceof ItemInfoWithIcon iiwi) && !iiwi.getMatchingLookupFlag().useLowRes()) {
-            badge = iiwi.bitmap.getBadgeDrawable(context, useTheme);
+            badge = iiwi.bitmap.getBadgeDrawable(context, useTheme, getIconShapeOrNull(context));
         }
 
         if (info instanceof PendingAddShortcutInfo) {
@@ -659,8 +666,11 @@
                 // Only fetch badge if the icon is on workspace
                 if (info.id != ItemInfo.NO_ID && badge == null) {
                     badge = appState.getIconCache().getShortcutInfoBadge(si).newIcon(
-                            context, ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
-                                    ? FLAG_THEMED : 0);
+                            context,
+                            ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
+                                    ? FLAG_THEMED : 0,
+                            getIconShapeOrNull(context)
+                    );
                 }
             }
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
@@ -706,10 +716,11 @@
 
         if (badge == null) {
             badge = BitmapInfo.LOW_RES_INFO.withFlags(
-                            UserCache.INSTANCE.get(context)
-                                    .getUserInfo(info.user)
-                                    .applyBitmapInfoFlags(FlagOp.NO_OP))
-                    .getBadgeDrawable(context, useTheme);
+                    UserCache.INSTANCE.get(context)
+                            .getUserInfo(info.user)
+                            .applyBitmapInfoFlags(FlagOp.NO_OP)
+                    )
+                    .getBadgeDrawable(context, useTheme, getIconShapeOrNull(context));
             if (badge == null) {
                 badge = new ColorDrawable(Color.TRANSPARENT);
             }
@@ -939,4 +950,18 @@
         }
         return null;
     }
+
+    /**
+     * Returns current icon shape to use for badges if flag is on, otherwise null.
+     */
+    @Nullable
+    public static Path getIconShapeOrNull(Context context) {
+        if (Flags.enableLauncherIconShapes()) {
+            return ThemeManager.INSTANCE.get(context)
+                    .getIconShape()
+                    .getPath(DEFAULT_PATH_SIZE);
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 78b53a9..cd91f8e 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -25,6 +25,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.ButtonDropTarget;
 import com.android.launcher3.CellLayout;
@@ -82,6 +83,7 @@
     protected static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace;
     protected static final int RESIZE = R.id.action_resize;
     public static final int DEEP_SHORTCUTS = R.id.action_deep_shortcuts;
+    public static final int CLOSE = R.id.action_close;
 
     public LauncherAccessibilityDelegate(Launcher launcher) {
         super(launcher);
@@ -104,6 +106,8 @@
                 RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R));
         mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS,
                 R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
+        mActions.put(CLOSE, new LauncherAction(CLOSE,
+                R.string.action_close, KeyEvent.KEYCODE_X));
     }
 
     private static boolean isNotInShortcutMenu(@Nullable View view) {
@@ -137,6 +141,10 @@
             }
         }
 
+        if (host instanceof AppWidgetResizeFrame) {
+            out.add(mActions.get(CLOSE));
+        }
+
         if (supportAddToWorkSpace(item)) {
             out.add(mActions.get(ADD_TO_WORKSPACE));
         }
@@ -183,22 +191,28 @@
             }
             return dragCondition != null;
         } else if (action == MOVE) {
-            return beginAccessibleDrag(host, item, fromKeyboard);
+            final View itemView = (host instanceof AppWidgetResizeFrame)
+                    ? ((AppWidgetResizeFrame) host).getViewForAccessibility()
+                    : host;
+            return beginAccessibleDrag(itemView, item, fromKeyboard);
         } else if (action == ADD_TO_WORKSPACE) {
             return addToWorkspace(item, true /*accessibility*/, null /*finishCallback*/);
         } else if (action == MOVE_TO_WORKSPACE) {
             return moveToWorkspace(item);
         } else if (action == RESIZE) {
+            final View itemView = (host instanceof AppWidgetResizeFrame)
+                    ? ((AppWidgetResizeFrame) host).getViewForAccessibility()
+                    : host;
             final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
-            List<OptionItem> actions = getSupportedResizeActions(host, info);
+            List<OptionItem> actions = getSupportedResizeActions(itemView, info);
             Rect pos = new Rect();
-            mContext.getDragLayer().getDescendantRectRelativeToSelf(host, pos);
+            mContext.getDragLayer().getDescendantRectRelativeToSelf(itemView, pos);
             ArrowPopup popup = OptionsPopupView.show(mContext, new RectF(pos), actions, false);
             popup.requestFocus();
             popup.addOnCloseCallback(() -> {
-                host.requestFocus();
-                host.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
-                host.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
+                itemView.requestFocus();
+                itemView.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
+                itemView.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
                 AbstractFloatingView.closeOpenViews(mContext, /* animate= */ false,
                         AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME);
             });
@@ -208,6 +222,11 @@
                     : (host instanceof BubbleTextHolder
                             ? ((BubbleTextHolder) host).getBubbleText() : null);
             return btv != null && PopupContainerWithArrow.showForIcon(btv) != null;
+        } else if (action == CLOSE) {
+            if (host instanceof AppWidgetResizeFrame) {
+                AbstractFloatingView.closeOpenViews(mContext, /* animate= */ false,
+                        AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME);
+            }
         } else {
             for (ButtonDropTarget dropTarget : mContext.getDropTargetBar().getDropTargets()) {
                 if (dropTarget.supportsAccessibilityDrop(item, host)
@@ -222,6 +241,10 @@
 
     private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
         List<OptionItem> actions = new ArrayList<>();
+        if (host instanceof AppWidgetResizeFrame) {
+            return getSupportedResizeActions(
+                    ((AppWidgetResizeFrame) host).getViewForAccessibility(), info);
+        }
         AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
         if (providerInfo == null) {
             return actions;
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 5f632fe..870c891 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -64,7 +64,7 @@
         AllAppsStore.OnUpdateListener {
 
     public static final String TAG = "AlphabeticalAppsList";
-    private static final String PRIVATE_SPACE_PACKAGE = "com.android.privatespace";
+    public static final String PRIVATE_SPACE_PACKAGE = "com.android.privatespace";
 
     private final WorkProfileManager mWorkProviderManager;
 
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 06643d3..c499097 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.LauncherIcons.IconPool;
 import com.android.launcher3.model.ItemInstallQueue;
+import com.android.launcher3.model.LoaderCursor.LoaderCursorFactory;
 import com.android.launcher3.model.WidgetsFilterDataProvider;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.UserCache;
@@ -87,6 +88,8 @@
     GridCustomizationsProxy getGridCustomizationsProxy();
     WidgetsFilterDataProvider getWidgetsFilterDataProvider();
 
+    LoaderCursorFactory getLoaderCursorFactory();
+
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
         @BindsInstance Builder appContext(@ApplicationContext Context context);
diff --git a/src/com/android/launcher3/debug/TestEventEmitter.java b/src/com/android/launcher3/debug/TestEventEmitter.java
index ed3b4bb..db69bc2 100644
--- a/src/com/android/launcher3/debug/TestEventEmitter.java
+++ b/src/com/android/launcher3/debug/TestEventEmitter.java
@@ -34,7 +34,9 @@
         RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
         WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
         SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
-        SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED");
+        SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED"),
+
+        LAUNCHER_STATE_COMPLETED("LAUNCHER_STATE_COMPLETED");
 
         TestEvent(String event) {
         }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 613b430..4127dd1 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -540,7 +540,7 @@
                 accepted = true;
             }
         }
-        final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
+        final View dropTargetAsView = dropTarget.getDropView();
         dispatchDropComplete(dropTargetAsView, accepted);
     }
 
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index b80238c..e4e4b90 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -80,8 +80,10 @@
 import com.android.launcher3.dagger.LauncherAppModule;
 import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.BaseLauncherBinder.BaseLauncherBinderFactory;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.LoaderTask.LoaderTaskFactory;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.FolderInfo;
@@ -605,6 +607,10 @@
     @Component(modules = LauncherAppModule.class)
     public interface PreviewAppComponent extends LauncherAppComponent {
 
+        LoaderTaskFactory getLoaderTaskFactory();
+        BaseLauncherBinderFactory getBaseLauncherBinderFactory();
+        BgDataModel getDataModel();
+
         /** Builder for NexusLauncherAppComponent. */
         @Component.Builder
         interface Builder extends LauncherAppComponent.Builder {
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index d425f03..5a9b9c2 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -58,19 +58,21 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.dagger.LauncherComponentProvider;
+import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewAppComponent;
 import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
-import com.android.launcher3.model.BaseLauncherBinder;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.model.UserManagerState;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.widget.LocalColorExtractor;
 import com.android.systemui.shared.Flags;
 
-import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -338,38 +340,32 @@
             // Start the migration
             PreviewContext previewContext =
                     new PreviewContext(inflationContext, mGridName, mShapeKey);
+            PreviewAppComponent appComponent =
+                    (PreviewAppComponent) LauncherComponentProvider.get(previewContext);
 
-            BgDataModel bgModel = new BgDataModel();
-            new LoaderTask(
-                    LauncherAppState.getInstance(previewContext),
-                    /* bgAllAppsList= */ null,
-                    bgModel,
-                    LauncherAppState.getInstance(previewContext).getModel().getModelDelegate(),
-                    new BaseLauncherBinder(LauncherAppState.getInstance(previewContext), bgModel,
-                            /* bgAllAppsList= */ null, new Callbacks[0])) {
+            LoaderTask task = appComponent.getLoaderTaskFactory().newLoaderTask(
+                    appComponent.getBaseLauncherBinderFactory().createBinder(new Callbacks[0]),
+                    new UserManagerState());
 
-                @Override
-                public void run() {
-                    InvariantDeviceProfile idp = LauncherAppState.getIDP(previewContext);
-                    DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
-                    String query =
-                            LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
-                                    + " or " + LauncherSettings.Favorites.CONTAINER + " = "
-                                    + LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-                    if (deviceProfile.isTwoPanels) {
-                        query += " or " + LauncherSettings.Favorites.SCREEN + " = "
-                                + Workspace.SECOND_SCREEN_ID;
-                    }
-                    loadWorkspace(new ArrayList<>(), query, null, null);
+            InvariantDeviceProfile idp = appComponent.getIDP();
+            DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
+            String query =
+                    LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
+                            + " or " + LauncherSettings.Favorites.CONTAINER + " = "
+                            + LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+            if (deviceProfile.isTwoPanels) {
+                query += " or " + LauncherSettings.Favorites.SCREEN + " = "
+                        + Workspace.SECOND_SCREEN_ID;
+            }
 
-                    final SparseArray<Size> spanInfo = getLoadedLauncherWidgetInfo();
-                    MAIN_EXECUTOR.execute(() -> {
-                        renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo,
-                                idp);
-                        mLifeCycleTracker.add(previewContext::onDestroy);
-                    });
-                }
-            }.run();
+            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap = new HashMap<>();
+            task.loadWorkspaceForPreview(query, widgetProviderInfoMap);
+            final SparseArray<Size> spanInfo = getLoadedLauncherWidgetInfo();
+            MAIN_EXECUTOR.execute(() -> {
+                renderView(previewContext, appComponent.getDataModel(), widgetProviderInfoMap,
+                        spanInfo, idp);
+                mLifeCycleTracker.add(previewContext::onDestroy);
+            });
         } else {
             LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
                 if (dataModel != null) {
@@ -420,7 +416,6 @@
             view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
         }
 
-
         if (!Flags.newCustomizationPickerUi()) {
             view.setAlpha(mSkipAnimations ? 1 : 0);
             view.animate().alpha(1)
@@ -477,5 +472,4 @@
             MAIN_EXECUTOR.execute(mLifecycleTracker::executeAllAndDestroy);
         }
     }
-
 }
diff --git a/src/com/android/launcher3/graphics/ShapeDelegate.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
index 9033eac..7c04292 100644
--- a/src/com/android/launcher3/graphics/ShapeDelegate.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -203,7 +203,11 @@
                     start =
                         poly.transformed(
                             Matrix().apply {
-                                setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(startRect), FILL)
+                                setRectToRect(
+                                    RectF(0f, 0f, DEFAULT_PATH_SIZE, DEFAULT_PATH_SIZE),
+                                    RectF(startRect),
+                                    FILL,
+                                )
                             }
                         ),
                     end =
@@ -281,7 +285,10 @@
                     PathParser.createPathFromPathData(shapeStr).apply {
                         transform(
                             Matrix().apply {
-                                setScale(AREA_CALC_SIZE / 100f, AREA_CALC_SIZE / 100f)
+                                setScale(
+                                    AREA_CALC_SIZE / DEFAULT_PATH_SIZE,
+                                    AREA_CALC_SIZE / DEFAULT_PATH_SIZE,
+                                )
                             }
                         )
                     }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 74d5098..4715132 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -166,6 +166,9 @@
         @UiEvent(doc = "User tapped or long pressed on settings icon inside launcher settings.")
         LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS(463),
 
+        @UiEvent(doc = "User tapped or long pressed on apps icon inside launcher settings.")
+        LAUNCHER_ALL_APPS_TAP_OR_LONGPRESS(2204),
+
         @UiEvent(doc = "User tapped or long pressed on widget tray icon inside launcher settings.")
         LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS(464),
 
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index ddbbdc7..dfba4bb 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -66,13 +66,6 @@
 
     /**
      * @param itemList items to add on the workspace
-     */
-    public AddWorkspaceItemsTask(@NonNull final List<Pair<ItemInfo, Object>> itemList) {
-        this(itemList, new WorkspaceItemSpaceFinder());
-    }
-
-    /**
-     * @param itemList items to add on the workspace
      * @param itemSpaceFinder inject WorkspaceItemSpaceFinder dependency for testing
      */
     public AddWorkspaceItemsTask(@NonNull final List<Pair<ItemInfo, Object>> itemList,
@@ -91,7 +84,7 @@
 
         final ArrayList<ItemInfo> addedItemsFinal = new ArrayList<>();
         final IntArray addedWorkspaceScreensFinal = new IntArray();
-        final Context context = taskController.getApp().getContext();
+        final Context context = taskController.getContext();
 
         synchronized (dataModel) {
             IntArray workspaceScreens = dataModel.collectWorkspaceScreens();
@@ -133,7 +126,7 @@
 
             for (ItemInfo item : filteredItems) {
                 // Find appropriate space for the item.
-                int[] coords = mItemSpaceFinder.findSpaceForItem(taskController.getApp(), dataModel,
+                int[] coords = mItemSpaceFinder.findSpaceForItem(
                         workspaceScreens, addedWorkspaceScreensFinal, item.spanX, item.spanY);
                 int screenId = coords[0];
 
@@ -192,7 +185,7 @@
                             continue;
                         }
 
-                        IconCache cache = taskController.getApp().getIconCache();
+                        IconCache cache = taskController.getIconCache();
                         WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
                         wii.title = "";
                         wii.bitmap = cache.getDefaultIcon(item.user);
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index 98f9afd..2311239 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -34,6 +34,7 @@
 
 import com.android.launcher3.AppFilter;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.data.AppInfo;
@@ -53,11 +54,13 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
+import javax.inject.Inject;
+
 
 /**
  * Stores the list of all applications for the all apps view.
  */
-@SuppressWarnings("NewApi")
+@LauncherAppSingleton
 public class AllAppsList {
 
     private static final String TAG = "AllAppsList";
@@ -70,10 +73,10 @@
     public final ArrayList<AppInfo> data = new ArrayList<>(DEFAULT_APPLICATIONS_NUMBER);
 
     @NonNull
-    private IconCache mIconCache;
+    private final IconCache mIconCache;
 
     @NonNull
-    private AppFilter mAppFilter;
+    private final AppFilter mAppFilter;
 
     private boolean mDataChanged = false;
     private Consumer<AppInfo> mRemoveListener = NO_OP_CONSUMER;
@@ -92,6 +95,7 @@
     /**
      * Boring constructor.
      */
+    @Inject
     public AllAppsList(IconCache iconCache, AppFilter appFilter) {
         mIconCache = iconCache;
         mAppFilter = appFilter;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 262bf67..c4bbae4 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import android.content.Context;
 import android.os.Trace;
 import android.util.Log;
 import android.util.Pair;
@@ -34,11 +35,12 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
@@ -55,6 +57,10 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -76,7 +82,9 @@
 
     protected final LooperExecutor mUiExecutor;
 
-    protected final LauncherAppState mApp;
+    private final Context mContext;
+    private final InvariantDeviceProfile mIDP;
+    private final LauncherModel mModel;
     protected final BgDataModel mBgDataModel;
     private final AllAppsList mBgAllAppsList;
 
@@ -84,10 +92,18 @@
 
     private int mMyBindingId;
 
-    public BaseLauncherBinder(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, Callbacks[] callbacksList) {
+    @AssistedInject
+    public BaseLauncherBinder(
+            @ApplicationContext Context context,
+            InvariantDeviceProfile idp,
+            LauncherModel model,
+            BgDataModel dataModel,
+            AllAppsList allAppsList,
+            @Assisted Callbacks[] callbacksList) {
         mUiExecutor = MAIN_EXECUTOR;
-        mApp = app;
+        mContext = context;
+        mIDP = idp;
+        mModel = model;
         mBgDataModel = dataModel;
         mBgAllAppsList = allAppsList;
         mCallbacksList = callbacksList;
@@ -110,15 +126,14 @@
                 mBgDataModel.extraItems.forEach(extraItems::add);
                 if (incrementBindId) {
                     mBgDataModel.lastBindId++;
-                    mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
+                    mBgDataModel.lastLoadId = mModel.getLastLoadId();
                 }
                 mMyBindingId = mBgDataModel.lastBindId;
                 workspaceItemCount = mBgDataModel.itemsIdMap.size();
             }
 
             for (Callbacks cb : mCallbacksList) {
-                new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                        itemsIdMap, extraItems, orderedScreenIds)
+                new UnifiedWorkspaceBinder(cb, itemsIdMap, extraItems, orderedScreenIds)
                         .bind(isBindSync, workspaceItemCount);
             }
         } finally {
@@ -162,9 +177,8 @@
         if (!WIDGETS_ENABLED) {
             return;
         }
-        List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
+        List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mContext)
                 .build(mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker());
-
         executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
     }
 
@@ -181,10 +195,9 @@
     /**
      * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right)
      */
-    protected void sortWorkspaceItemsSpatially(InvariantDeviceProfile profile,
-            ArrayList<ItemInfo> workspaceItems) {
-        final int screenCols = profile.numColumns;
-        final int screenCellCount = profile.numColumns * profile.numRows;
+    protected void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) {
+        final int screenCols = mIDP.numColumns;
+        final int screenCellCount = mIDP.numColumns * mIDP.numRows;
         Collections.sort(workspaceItems, (lhs, rhs) -> {
             if (lhs.container == rhs.container) {
                 // Within containers, order by their spatial position in that container
@@ -240,30 +253,18 @@
 
     private class UnifiedWorkspaceBinder {
 
-        private final Executor mUiExecutor;
         private final Callbacks mCallbacks;
 
-        private final LauncherAppState mApp;
-        private final BgDataModel mBgDataModel;
-
-        private final int mMyBindingId;
         private final IntSparseArrayMap<ItemInfo> mItemIdMap;
         private final IntArray mOrderedScreenIds;
         private final ArrayList<FixedContainerItems> mExtraItems;
 
-        UnifiedWorkspaceBinder(Callbacks callbacks,
-                Executor uiExecutor,
-                LauncherAppState app,
-                BgDataModel bgDataModel,
-                int myBindingId,
+        UnifiedWorkspaceBinder(
+                Callbacks callbacks,
                 IntSparseArrayMap<ItemInfo> itemIdMap,
                 ArrayList<FixedContainerItems> extraItems,
                 IntArray orderedScreenIds) {
             mCallbacks = callbacks;
-            mUiExecutor = uiExecutor;
-            mApp = app;
-            mBgDataModel = bgDataModel;
-            mMyBindingId = myBindingId;
             mItemIdMap = itemIdMap;
             mExtraItems = extraItems;
             mOrderedScreenIds = orderedScreenIds;
@@ -289,9 +290,8 @@
                     (WIDGET_FILTER.test(item) ? otherAppWidgets : otherWorkspaceItems).add(item);
                 }
             });
-            final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
-            sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
-            sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
+            sortWorkspaceItemsSpatially(currentWorkspaceItems);
+            sortWorkspaceItemsSpatially(otherWorkspaceItems);
 
             // Tell the workspace that we're about to start binding items
             executeCallbacksTask(c -> {
@@ -352,7 +352,7 @@
             executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
 
             executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);
-            pendingExecutor.execute(() -> ItemInstallQueue.INSTANCE.get(mApp.getContext())
+            pendingExecutor.execute(() -> ItemInstallQueue.INSTANCE.get(mContext)
                     .resumeModelPush(FLAG_LOADER_RUNNING));
         }
 
@@ -367,8 +367,8 @@
                 return;
             }
 
-            ModelWriter writer = mApp.getModel()
-                    .getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null);
+            ModelWriter writer = mModel.getWriter(
+                    false /* verifyChanges */, CellPosMapper.DEFAULT, null);
             List<Pair<ItemInfo, View>> bindItems = items.stream()
                     .map(i -> Pair.create(i, inflater.inflateItem(i, writer, null)))
                     .collect(Collectors.toList());
@@ -398,4 +398,9 @@
             });
         }
     }
+
+    @AssistedFactory
+    public interface BaseLauncherBinderFactory {
+        BaseLauncherBinder createBinder(Callbacks[] callbacks);
+    }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index d9eccaf..ea9f36c 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.CollectionInfo;
@@ -79,9 +80,15 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import javax.inject.Inject;
+
 /**
  * All the data stored in-memory and managed by the LauncherModel
+ *
+ * All the static data should be accessed on the background thread, A lock should be acquired on
+ * this object when accessing any data from this model.
  */
+@LauncherAppSingleton
 public class BgDataModel {
 
     private static final String TAG = "BgDataModel";
@@ -105,7 +112,7 @@
     /**
      * Entire list of widgets.
      */
-    public final WidgetsModel widgetsModel = new WidgetsModel();
+    public final WidgetsModel widgetsModel;
 
     /**
      * Cache for strings used in launcher
@@ -124,6 +131,11 @@
     public boolean isFirstPagePinnedItemEnabled = QSB_ON_FIRST_SCREEN
             && !enableSmartspaceRemovalToggle();
 
+    @Inject
+    public BgDataModel(WidgetsModel widgetsModel) {
+        this.widgetsModel = widgetsModel;
+    }
+
     /**
      * Clears all the data
      */
diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
index 48934e2..f740b49 100644
--- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java
+++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
@@ -59,7 +59,7 @@
     @Override
     public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
             @NonNull AllAppsList apps) {
-        IconCache iconCache = taskController.getApp().getIconCache();
+        IconCache iconCache = taskController.getIconCache();
         ArrayList<ItemInfo> updatedItems = new ArrayList<>();
 
         synchronized (dataModel) {
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java
index f443f81..829e0fe 100644
--- a/src/com/android/launcher3/model/FirstScreenBroadcast.java
+++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java
@@ -45,9 +45,9 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -76,9 +76,9 @@
 
     private static final String VERIFICATION_TOKEN_EXTRA = "verificationToken";
 
-    private final HashMap<PackageUserKey, SessionInfo> mSessionInfoForPackage;
+    private final Map<PackageUserKey, SessionInfo> mSessionInfoForPackage;
 
-    public FirstScreenBroadcast(HashMap<PackageUserKey, SessionInfo> sessionInfoForPackage) {
+    public FirstScreenBroadcast(Map<PackageUserKey, SessionInfo> sessionInfoForPackage) {
         mSessionInfoForPackage = sessionInfoForPackage;
     }
 
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index bd8c36b..77d0d76 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -45,13 +45,14 @@
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
@@ -70,6 +71,10 @@
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.UserIconInfo;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.net.URISyntaxException;
 import java.security.InvalidParameterException;
 
@@ -82,8 +87,8 @@
 
     private final LongSparseArray<UserHandle> allUsers;
 
-    private final LauncherAppState mApp;
     private final Context mContext;
+    private final LauncherModel mModel;
     private final PackageManagerHelper mPmHelper;
     private final IconCache mIconCache;
     private final InvariantDeviceProfile mIDP;
@@ -130,17 +135,24 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState,
-            PackageManagerHelper pmHelper,
-            @Nullable LauncherRestoreEventLogger restoreEventLogger) {
+    @AssistedInject
+    public LoaderCursor(
+            @Assisted Cursor cursor,
+            @Assisted UserManagerState userManagerState,
+            @Assisted @Nullable LauncherRestoreEventLogger restoreEventLogger,
+            @ApplicationContext Context context,
+            IconCache iconCache,
+            InvariantDeviceProfile idp,
+            LauncherModel model,
+            PackageManagerHelper pmHelper) {
         super(cursor);
-
-        mApp = app;
-        allUsers = userManagerState.allUsers;
-        mContext = app.getContext();
-        mIconCache = app.getIconCache();
+        mContext = context;
+        mIconCache = iconCache;
+        mIDP = idp;
+        mModel = model;
         mPmHelper = pmHelper;
-        mIDP = app.getInvariantDeviceProfile();
+
+        allUsers = userManagerState.allUsers;
         mRestoreEventLogger = restoreEventLogger;
 
         // Init column indices
@@ -432,7 +444,7 @@
      */
     public ContentWriter updater() {
        return new ContentWriter(mContext, new ContentWriter.CommitParams(
-               mApp.getModel().getModelDbController(),
+               mModel.getModelDbController(),
                BaseColumns._ID + "= ?", new String[]{Integer.toString(id)}));
     }
 
@@ -454,7 +466,7 @@
     public boolean commitDeleted() {
         if (mItemsToRemove.size() > 0) {
             // Remove dead items
-            mApp.getModel().getModelDbController().delete(TABLE_NAME,
+            mModel.getModelDbController().delete(TABLE_NAME,
                     Utilities.createDbSelectionQuery(Favorites._ID, mItemsToRemove), null);
             return true;
         }
@@ -480,7 +492,7 @@
             // Update restored items that no longer require special handling
             ContentValues values = new ContentValues();
             values.put(Favorites.RESTORED, 0);
-            mApp.getModel().getModelDbController().update(TABLE_NAME, values,
+            mModel.getModelDbController().update(TABLE_NAME, values,
                     Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null);
         }
         if (mRestoreEventLogger != null) {
@@ -645,4 +657,12 @@
             return false;
         }
     }
+
+    @AssistedFactory
+    public interface LoaderCursorFactory {
+
+        LoaderCursor createLoaderCursor(Cursor cursor,
+                UserManagerState userManagerState,
+                @Nullable LauncherRestoreEventLogger restoreEventLogger);
+    }
 }
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index fb1ebaf..73af6a2 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -53,7 +53,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
 
@@ -63,13 +62,14 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.Flags;
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderNameInfos;
@@ -82,6 +82,7 @@
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.LoaderCursor.LoaderCursorFactory;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.FolderInfo;
@@ -106,6 +107,10 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.widget.WidgetInflater;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -117,6 +122,8 @@
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 
+import javax.inject.Named;
+
 /**
  * Runnable for the thread that loads the contents of the launcher:
  *   - workspace icons
@@ -131,10 +138,14 @@
 
     private static final boolean DEBUG = true;
 
-    @NonNull
-    protected final LauncherAppState mApp;
+    private final Context mContext;
+    private final LauncherModel mModel;
+    private final InvariantDeviceProfile mIDP;
+    private final boolean mIsSafeModeEnabled;
     private final AllAppsList mBgAllAppsList;
     protected final BgDataModel mBgDataModel;
+    private final LoaderCursorFactory mLoaderCursorFactory;
+
     private final ModelDelegate mModelDelegate;
     private boolean mIsRestoreFromBackup;
 
@@ -152,7 +163,6 @@
     private final IconCache mIconCache;
 
     private final UserManagerState mUserManagerState;
-    protected final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap = new ArrayMap<>();
     private Map<ShortcutKey, ShortcutInfo> mShortcutKeyToPinnedShortcuts;
     private HashMap<PackageUserKey, SessionInfo> mInstallingPkgsCached;
 
@@ -164,26 +174,36 @@
     private boolean mItemsDeleted = false;
     private String mDbName;
 
-    public LoaderTask(@NonNull LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel bgModel,
-            ModelDelegate modelDelegate, @NonNull BaseLauncherBinder launcherBinder) {
-        this(app, bgAllAppsList, bgModel, modelDelegate, launcherBinder, new UserManagerState());
-    }
-
-    @VisibleForTesting
-    LoaderTask(@NonNull LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel bgModel,
-            ModelDelegate modelDelegate, @NonNull BaseLauncherBinder launcherBinder,
-            UserManagerState userManagerState) {
-        mApp = app;
+    @AssistedInject
+    LoaderTask(
+            @ApplicationContext Context context,
+            InvariantDeviceProfile idp,
+            LauncherModel model,
+            UserCache userCache,
+            PackageManagerHelper pmHelper,
+            InstallSessionHelper sessionHelper,
+            IconCache iconCache,
+            AllAppsList bgAllAppsList,
+            BgDataModel bgModel,
+            LoaderCursorFactory loaderCursorFactory,
+            @Named("SAFE_MODE") boolean isSafeModeEnabled,
+            @Assisted @NonNull BaseLauncherBinder launcherBinder,
+            @Assisted UserManagerState userManagerState) {
+        mContext = context;
+        mIDP = idp;
+        mModel = model;
+        mIsSafeModeEnabled = isSafeModeEnabled;
         mBgAllAppsList = bgAllAppsList;
         mBgDataModel = bgModel;
-        mModelDelegate = modelDelegate;
+        mModelDelegate = model.getModelDelegate();
         mLauncherBinder = launcherBinder;
-        mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
-        mUserManager = mApp.getContext().getSystemService(UserManager.class);
-        mUserCache = UserCache.INSTANCE.get(mApp.getContext());
-        mPmHelper = PackageManagerHelper.INSTANCE.get(mApp.getContext());
-        mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
-        mIconCache = mApp.getIconCache();
+        mLoaderCursorFactory = loaderCursorFactory;
+        mLauncherApps = mContext.getSystemService(LauncherApps.class);
+        mUserManager = mContext.getSystemService(UserManager.class);
+        mUserCache = userCache;
+        mPmHelper = pmHelper;
+        mSessionHelper = sessionHelper;
+        mIconCache = iconCache;
         mUserManagerState = userManagerState;
         mInstallingPkgsCached = null;
     }
@@ -215,7 +235,7 @@
                         .filter(currentScreenContentFilter(firstScreens))
                         .toList();
         final int disableArchivingLauncherBroadcast = Settings.Secure.getInt(
-                mApp.getContext().getContentResolver(),
+                mContext.getContentResolver(),
                 "disable_launcher_broadcast_installed_apps",
                 /* default */ 0);
         boolean shouldAttachArchivingExtras = mIsRestoreFromBackup
@@ -230,10 +250,10 @@
                             mBgDataModel.itemsIdMap.stream().filter(WIDGET_FILTER).toList()
                     );
             logASplit("Sending first screen broadcast with additional archiving Extras");
-            FirstScreenBroadcastHelper.sendBroadcastsForModels(mApp.getContext(), broadcastModels);
+            FirstScreenBroadcastHelper.sendBroadcastsForModels(mContext, broadcastModels);
         } else {
             logASplit("Sending first screen broadcast");
-            mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
+            mFirstScreenBroadcast.sendBroadcasts(mContext, firstScreenItems);
         }
     }
 
@@ -249,16 +269,14 @@
         MODEL_EXECUTOR.elevatePriority(CALLER_LOADER_TASK);
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         mIsRestoreFromBackup =
-                LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
+                LauncherPrefs.get(mContext).get(IS_FIRST_LOAD_AFTER_RESTORE);
         LauncherRestoreEventLogger restoreEventLogger = null;
         if (enableLauncherBrMetricsFixed()) {
-            restoreEventLogger = LauncherRestoreEventLogger.Companion
-                    .newInstance(mApp.getContext());
+            restoreEventLogger = LauncherRestoreEventLogger.Companion.newInstance(mContext);
         }
-        try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
-
+        try (LauncherModel.LoaderTransaction transaction = mModel.beginLoader(this)) {
             List<CacheableShortcutInfo> allShortcuts = new ArrayList<>();
-            loadWorkspace(allShortcuts, "", memoryLogger, restoreEventLogger);
+            loadWorkspace(allShortcuts, "", new HashMap<>(), memoryLogger, restoreEventLogger);
 
             // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
             // sanitizeData should not be invoked if the workspace is loaded from a db different
@@ -267,7 +285,7 @@
             // TODO(b/384731096): Write Unit Test to make sure sanitizeWidgetsShortcutsAndPackages
             //  actually re-pins shortcuts that are in model but not in ShortcutManager, if possible
             //  after a simulated restore.
-            if (Objects.equals(mApp.getInvariantDeviceProfile().dbFile, mDbName)) {
+            if (Objects.equals(mIDP.dbFile, mDbName)) {
                 verifyNotStopped();
                 sanitizeFolders(mItemsDeleted);
                 sanitizeAppPairs();
@@ -307,13 +325,13 @@
             setIgnorePackages(updateHandler);
             updateHandler.updateIcons(allActivityList,
                     LauncherActivityCachingLogic.INSTANCE,
-                    mApp.getModel()::onPackageIconsUpdated);
+                    mModel::onPackageIconsUpdated);
             logASplit("update AllApps icon cache finished");
 
             verifyNotStopped();
             logASplit("saving all shortcuts in icon cache");
             updateHandler.updateIcons(allShortcuts, CacheableShortcutCachingLogic.INSTANCE,
-                    mApp.getModel()::onPackageIconsUpdated);
+                    mModel::onPackageIconsUpdated);
 
             // Take a break
             waitForIdle();
@@ -342,14 +360,14 @@
 
             // fourth step
             WidgetsModel widgetsModel = mBgDataModel.widgetsModel;
-            List<CachedObject> allWidgetsList = widgetsModel.update(mApp, /*packageUser=*/null);
+            List<CachedObject> allWidgetsList = widgetsModel.update(/*packageUser=*/null);
             logASplit("load widgets finished");
 
             verifyNotStopped();
             mLauncherBinder.bindWidgets();
             logASplit("bindWidgets finished");
             verifyNotStopped();
-            LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext());
+            LauncherPrefs prefs = LauncherPrefs.get(mContext);
 
             if (enableSmartspaceAsAWidget() && prefs.get(SHOULD_SHOW_SMARTSPACE)) {
                 mLauncherBinder.bindSmartspaceWidget();
@@ -366,7 +384,7 @@
             logASplit("saving all widgets in icon cache");
             updateHandler.updateIcons(allWidgetsList,
                     CachedObjectCachingLogic.INSTANCE,
-                    mApp.getModel()::onWidgetLabelsUpdated);
+                    mModel::onWidgetLabelsUpdated);
 
             // fifth step
             loadFolderNames();
@@ -380,7 +398,7 @@
             memoryLogger.clearLogs();
             if (mIsRestoreFromBackup) {
                 mIsRestoreFromBackup = false;
-                LauncherPrefs.get(mApp.getContext()).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(false));
+                LauncherPrefs.get(mContext).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(false));
                 if (restoreEventLogger != null) {
                     restoreEventLogger.reportLauncherRestoreResults();
                 }
@@ -402,35 +420,42 @@
         this.notify();
     }
 
-    protected void loadWorkspace(
+    public void loadWorkspaceForPreview(String selection,
+            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
+        loadWorkspace(new ArrayList<>(), selection, widgetProviderInfoMap, null, null);
+    }
+
+    private void loadWorkspace(
             List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
+            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
             @Nullable LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger
     ) {
         Trace.beginSection("LoadWorkspace");
         try {
-            loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger, restoreEventLogger);
+            loadWorkspaceImpl(allDeepShortcuts, selection, widgetProviderInfoMap,
+                    memoryLogger, restoreEventLogger);
         } finally {
             Trace.endSection();
         }
         logASplit("loadWorkspace finished");
 
         mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN
-                && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(
-                mApp.getContext()).getBoolean(SMARTSPACE_ON_HOME_SCREEN, true));
+                && (!enableSmartspaceRemovalToggle()
+                || LauncherPrefs.getPrefs(mContext).getBoolean(SMARTSPACE_ON_HOME_SCREEN, true));
     }
 
     private void loadWorkspaceImpl(
             List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
+            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
             @Nullable LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger) {
-        final Context context = mApp.getContext();
         final boolean isSdCardReady = Utilities.isBootCompleted();
-        final WidgetInflater widgetInflater = new WidgetInflater(context);
+        final WidgetInflater widgetInflater = new WidgetInflater(mContext, mIsSafeModeEnabled);
 
-        ModelDbController dbController = mApp.getModel().getModelDbController();
+        ModelDbController dbController = mModel.getModelDbController();
         if (Flags.gridMigrationRefactor()) {
             try {
                 dbController.attemptMigrateDb(restoreEventLogger, mModelDelegate);
@@ -452,28 +477,29 @@
             if (Flags.enableSupportForArchiving()) {
                 mInstallingPkgsCached = installingPkgs;
             }
-            installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
+            installingPkgs.forEach(mIconCache::updateSessionCache);
             FileLog.d(TAG, "loadWorkspace: Packages with active install/update sessions: "
                     + installingPkgs.keySet().stream().map(info -> info.mPackageName).toList());
 
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
 
             mShortcutKeyToPinnedShortcuts = new HashMap<>();
-            final LoaderCursor c = new LoaderCursor(
+            final LoaderCursor c = mLoaderCursorFactory.createLoaderCursor(
                     dbController.query(TABLE_NAME, null, selection, null, null),
-                    mApp, mUserManagerState, mPmHelper,
+                    mUserManagerState,
                     mIsRestoreFromBackup ? restoreEventLogger : null);
             final Bundle extras = c.getExtras();
             mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME);
             try {
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
-                queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers);
+                queryPinnedShortcutsForUnlockedUsers(mContext, unlockedUsers);
 
                 mWorkspaceIconRequestInfos = new ArrayList<>();
                 WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger,
                         mUserCache, mUserManagerState, mLauncherApps, mPendingPackages,
-                        mShortcutKeyToPinnedShortcuts, mApp, mBgDataModel,
-                        mWidgetProvidersMap, installingPkgs, isSdCardReady,
+                        mShortcutKeyToPinnedShortcuts, mContext, mIDP, mIconCache,
+                        mIsSafeModeEnabled, mBgDataModel,
+                        widgetProviderInfoMap, installingPkgs, isSdCardReady,
                         widgetInflater, mPmHelper, mWorkspaceIconRequestInfos, unlockedUsers,
                         allDeepShortcuts);
 
@@ -577,7 +603,7 @@
     @WorkerThread
     private void processFolderItems() {
         // Sort the folder items, update ranks, and make sure all preview items are high res.
-        List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
+        List<FolderGridOrganizer> verifiers = mIDP.supportedProfiles
                 .stream().map(FolderGridOrganizer::createFolderGridOrganizer).toList();
         for (ItemInfo itemInfo : mBgDataModel.itemsIdMap) {
             if (!(itemInfo instanceof FolderInfo folder)) {
@@ -617,7 +643,7 @@
                 if (mIconCache.isDefaultIcon(wai.bitmap, wai.user)) {
                     logASplit("tryLoadWorkspaceIconsInBulk: default icon found for "
                             + wai.getTargetComponent() + ", will attempt to load from iconBlob");
-                    iconRequestInfo.loadIconFromDbBlob(mApp.getContext());
+                    iconRequestInfo.loadIconFromDbBlob(mContext);
                 }
             }
         } finally {
@@ -649,7 +675,7 @@
     private void sanitizeFolders(boolean itemsDeleted) {
         if (itemsDeleted) {
             // Remove any empty folder
-            IntArray deletedFolderIds = mApp.getModel().getModelDbController().deleteEmptyFolders();
+            IntArray deletedFolderIds = mModel.getModelDbController().deleteEmptyFolders();
             synchronized (mBgDataModel) {
                 for (int folderId : deletedFolderIds) {
                     mBgDataModel.itemsIdMap.remove(folderId);
@@ -660,8 +686,8 @@
 
     /** Cleans up app pairs if they don't have the right number of member apps (2). */
     private void sanitizeAppPairs() {
-        IntArray deletedAppPairIds = mApp.getModel().getModelDbController().deleteBadAppPairs();
-        IntArray deletedAppIds = mApp.getModel().getModelDbController().deleteUnparentedApps();
+        IntArray deletedAppPairIds = mModel.getModelDbController().deleteBadAppPairs();
+        IntArray deletedAppIds = mModel.getModelDbController().deleteUnparentedApps();
 
         IntArray deleted = new IntArray();
         deleted.addAll(deletedAppPairIds);
@@ -675,17 +701,15 @@
     }
 
     private void sanitizeWidgetsShortcutsAndPackages() {
-        Context context = mApp.getContext();
-
         // Remove any ghost widgets
-        mApp.getModel().getModelDbController().removeGhostWidgets();
+        mModel.getModelDbController().removeGhostWidgets();
 
         // Update pinned state of model shortcuts
-        mBgDataModel.updateShortcutPinnedState(context);
+        mBgDataModel.updateShortcutPinnedState(mContext);
 
         if (!Utilities.isBootCompleted() && !mPendingPackages.isEmpty()) {
-            context.registerReceiver(
-                    new SdCardAvailableReceiver(mApp, mPendingPackages),
+            mContext.registerReceiver(
+                    new SdCardAvailableReceiver(mContext, mModel, mPendingPackages),
                     new IntentFilter(Intent.ACTION_BOOT_COMPLETED),
                     null,
                     MODEL_EXECUTOR.getHandler());
@@ -722,7 +746,7 @@
             for (int i = 0; i < apps.size(); i++) {
                 LauncherActivityInfo app = apps.get(i);
                 AppInfo appInfo = new AppInfo(app, mUserCache.getUserInfo(user),
-                        ApiWrapper.INSTANCE.get(mApp.getContext()), mPmHelper, quietMode);
+                        ApiWrapper.INSTANCE.get(mContext), mPmHelper, quietMode);
                 if (Flags.enableSupportForArchiving() && app.getApplicationInfo().isArchived) {
                     // For archived apps, include progress info in case there is a pending
                     // install session post restart of device.
@@ -751,7 +775,7 @@
             for (PackageInstaller.SessionInfo info :
                     mSessionHelper.getAllVerifiedSessions()) {
                 AppInfo promiseAppInfo = mBgAllAppsList.addPromiseApp(
-                        mApp.getContext(),
+                        mContext,
                         PackageInstallInfo.fromInstallingState(info),
                         false);
 
@@ -776,7 +800,7 @@
                                 + appInfo.getTargetComponent()
                                 + ", will attempt to load from iconBlob: "
                                 + Arrays.toString(iconRequestInfo.iconBlob));
-                        iconRequestInfo.loadIconFromDbBlob(mApp.getContext());
+                        iconRequestInfo.loadIconFromDbBlob(mContext);
                     }
                 }
             }
@@ -794,9 +818,9 @@
                     mUserManagerState.isAnyProfileQuietModeEnabled());
         }
         mBgAllAppsList.setFlags(FLAG_HAS_SHORTCUT_PERMISSION,
-                hasShortcutsPermission(mApp.getContext()));
+                hasShortcutsPermission(mContext));
         mBgAllAppsList.setFlags(FLAG_QUIET_MODE_CHANGE_PERMISSION,
-                mApp.getContext().checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
+                mContext.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
                         == PackageManager.PERMISSION_GRANTED);
 
         mBgAllAppsList.getAndResetChangeFlag();
@@ -827,7 +851,7 @@
                         workspaceIconRequest.get().iconBlob,
                         false /* useLowResIcon= */
                 );
-                if (!iconRequestInfo.loadIconFromDbBlob(mApp.getContext())) {
+                if (!iconRequestInfo.loadIconFromDbBlob(mContext)) {
                     Log.d(TAG, "AppInfo Icon failed to load from blob, using cache.");
                     mIconCache.getTitleAndIcon(
                             appInfo,
@@ -853,7 +877,7 @@
         if (mBgAllAppsList.hasShortcutHostPermission()) {
             for (UserHandle user : mUserCache.getUserProfiles()) {
                 if (mUserManager.isUserUnlocked(user)) {
-                    List<ShortcutInfo> shortcuts = new ShortcutRequest(mApp.getContext(), user)
+                    List<ShortcutInfo> shortcuts = new ShortcutRequest(mContext, user)
                             .query(ShortcutRequest.ALL);
                     allShortcuts.addAll(shortcuts);
                     mBgDataModel.updateDeepShortcutCounts(null, user, shortcuts);
@@ -864,7 +888,7 @@
     }
 
     private void loadFolderNames() {
-        FolderNameProvider provider = FolderNameProvider.newInstance(mApp.getContext(),
+        FolderNameProvider provider = FolderNameProvider.newInstance(mContext,
                 mBgAllAppsList.data, FolderNameProvider.getCollectionForSuggestions(mBgDataModel));
 
         synchronized (mBgDataModel) {
@@ -874,7 +898,7 @@
                     .forEach(info -> {
                         FolderInfo fi = (FolderInfo) info;
                         FolderNameInfos suggestionInfos = new FolderNameInfos();
-                        provider.getSuggestedFolderName(mApp.getContext(), fi.getAppContents(),
+                        provider.getSuggestedFolderName(mContext, fi.getAppContents(),
                                 suggestionInfos);
                         fi.suggestedFolderNames = suggestionInfos;
                     });
@@ -891,4 +915,10 @@
             Log.d(TAG, label);
         }
     }
+
+    @AssistedFactory
+    public interface LoaderTaskFactory {
+
+        LoaderTask newLoaderTask(BaseLauncherBinder binder, UserManagerState userState);
+    }
 }
diff --git a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
index 7ba2dad..8b6c369 100644
--- a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -114,7 +114,7 @@
     override fun onUpdateSessionDisplay(key: PackageUserKey, info: SessionInfo) {
         /** Updates the icons and label of all pending icons for the provided package name. */
         taskExecutor.accept { controller, _, _ ->
-            controller.app.iconCache.updateSessionCache(key, info)
+            controller.iconCache.updateSessionCache(key, info)
         }
         taskExecutor.accept(
             CacheDataUpdatedTask(
@@ -128,7 +128,7 @@
     override fun onInstallSessionCreated(sessionInfo: PackageInstallInfo) {
         if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
             taskExecutor.accept { taskController, _, apps ->
-                apps.addPromiseApp(taskController.app.context, sessionInfo)
+                apps.addPromiseApp(taskController.context, sessionInfo)
                 taskController.bindApplicationsIfNeeded()
             }
         }
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 5566482..f17ca32 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -16,27 +16,34 @@
 
 package com.android.launcher3.model
 
-import com.android.launcher3.LauncherAppState
+import android.content.Context
 import com.android.launcher3.LauncherModel
 import com.android.launcher3.LauncherModel.CallbackTask
 import com.android.launcher3.celllayout.CellPosMapper
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.BgDataModel.FixedContainerItems
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder
 import java.util.Objects
-import java.util.concurrent.Executor
 import java.util.function.Predicate
+import javax.inject.Inject
 
 /** Class with utility methods and properties for running a LauncherModel Task */
-class ModelTaskController(
-    val app: LauncherAppState,
+class ModelTaskController
+@Inject
+constructor(
+    @ApplicationContext val context: Context,
+    val iconCache: IconCache,
     val dataModel: BgDataModel,
     val allAppsList: AllAppsList,
-    private val model: LauncherModel,
-    private val uiExecutor: Executor,
+    val model: LauncherModel,
 ) {
 
+    private val uiExecutor = MAIN_EXECUTOR
+
     /** Schedules a {@param task} to be executed on the current callbacks. */
     fun scheduleCallbackTask(task: CallbackTask) {
         for (cb in model.callbacks) {
@@ -78,7 +85,7 @@
 
     fun bindUpdatedWidgets(dataModel: BgDataModel) {
         val allWidgets =
-            WidgetsListBaseEntriesBuilder(app.context)
+            WidgetsListBaseEntriesBuilder(context)
                 .build(dataModel.widgetsModel.widgetsByPackageItemForPicker)
         scheduleCallbackTask { it.bindAllWidgets(allWidgets) }
     }
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index a216042..a3561ed 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -52,11 +52,11 @@
             try {
                 // For instant apps we do not get package-add. Use setting events to update
                 // any pinned icons.
-                Context context = taskController.getApp().getContext();
+                Context context = taskController.getContext();
                 ApplicationInfo ai = context
                         .getPackageManager().getApplicationInfo(mInstallInfo.packageName, 0);
                 if (InstantAppResolver.newInstance(context).isInstantApp(ai)) {
-                    taskController.getApp().getModel().newModelCallbacks()
+                    taskController.getModel().newModelCallbacks()
                             .onPackageAdded(ai.packageName, mInstallInfo.user);
                 }
             } catch (PackageManager.NameNotFoundException e) {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 3cdb250..04f3faa 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -38,7 +38,6 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.Flags;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
@@ -106,9 +105,8 @@
     @Override
     public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
             @NonNull AllAppsList appsList) {
-        final LauncherAppState app = taskController.getApp();
-        final Context context = app.getContext();
-        final IconCache iconCache = app.getIconCache();
+        final Context context = taskController.getContext();
+        final IconCache iconCache = taskController.getIconCache();
 
         final String[] packages = mPackages;
         final int packageCount = packages.length;
@@ -433,7 +431,7 @@
             // Load widgets for the new package. Changes due to app updates are handled through
             // AppWidgetHost events, this is just to initialize the long-press options.
             for (int i = 0; i < packageCount; i++) {
-                dataModel.widgetsModel.update(app, new PackageUserKey(packages[i], mUser));
+                dataModel.widgetsModel.update(new PackageUserKey(packages[i], mUser));
             }
             taskController.bindUpdatedWidgets(dataModel);
         }
diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
index 9e3f0e1..9ae8092 100644
--- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java
+++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
@@ -22,7 +22,6 @@
 import android.content.pm.LauncherApps;
 import android.os.UserHandle;
 
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.PackageUserKey;
@@ -43,9 +42,10 @@
     private final Context mContext;
     private final Set<PackageUserKey> mPackages;
 
-    public SdCardAvailableReceiver(LauncherAppState app, Set<PackageUserKey> packages) {
-        mModel = app.getModel();
-        mContext = app.getContext();
+    public SdCardAvailableReceiver(
+            Context context, LauncherModel model, Set<PackageUserKey> packages) {
+        mContext = context;
+        mModel = model;
         mPackages = packages;
     }
 
diff --git a/src/com/android/launcher3/model/SessionFailureTask.kt b/src/com/android/launcher3/model/SessionFailureTask.kt
index 8baf568..6ed5178 100644
--- a/src/com/android/launcher3/model/SessionFailureTask.kt
+++ b/src/com/android/launcher3/model/SessionFailureTask.kt
@@ -33,9 +33,9 @@
         dataModel: BgDataModel,
         apps: AllAppsList,
     ) {
-        val iconCache = taskController.app.iconCache
+        val iconCache = taskController.iconCache
         val isAppArchived =
-            ApplicationInfoWrapper(taskController.app.context, packageName, user).isArchived()
+            ApplicationInfoWrapper(taskController.context, packageName, user).isArchived()
         synchronized(dataModel) {
             if (isAppArchived) {
                 val updatedItems = mutableListOf<WorkspaceItemInfo>()
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.kt b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
index 56e9e43..d6759e2 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.kt
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
@@ -40,8 +40,7 @@
         dataModel: BgDataModel,
         apps: AllAppsList,
     ) {
-        val app = taskController.app
-        val context = app.context
+        val context = taskController.context
         // Find WorkspaceItemInfo's that have changed on the workspace.
         val matchingWorkspaceItems = ArrayList<WorkspaceItemInfo>()
 
@@ -88,7 +87,7 @@
                     .filter { itemInfo: WorkspaceItemInfo -> shortcutId == itemInfo.deepShortcutId }
                     .forEach { workspaceItemInfo: WorkspaceItemInfo ->
                         workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context)
-                        app.iconCache.getShortcutIcon(
+                        taskController.iconCache.getShortcutIcon(
                             workspaceItemInfo,
                             CacheableShortcutInfo(fullDetails, infoWrapper),
                         )
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 3dc5ff3..4d28ccb 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -23,7 +23,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -55,8 +54,7 @@
     @Override
     public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
             @NonNull AllAppsList apps) {
-        LauncherAppState app = taskController.getApp();
-        Context context = app.getContext();
+        Context context = taskController.getContext();
 
         HashMap<ShortcutKey, ShortcutInfo> pinnedShortcuts = new HashMap<>();
         if (mIsUserUnlocked) {
@@ -92,7 +90,7 @@
                         }
                         si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER;
                         si.updateFromDeepShortcutInfo(shortcut, context);
-                        app.getIconCache().getShortcutIcon(si, shortcut);
+                        taskController.getIconCache().getShortcutIcon(si, shortcut);
                     } else {
                         si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
                     }
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index 52b142d..f2144c0 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.data.PackageItemInfo;
@@ -54,6 +55,8 @@
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
+import javax.inject.Inject;
+
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
  *
@@ -68,6 +71,29 @@
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
     @Nullable private WidgetValidityCheckForPicker mWidgetValidityCheckForPicker = null;
 
+    private final Context mContext;
+    private final InvariantDeviceProfile mIdp;
+    private final IconCache mIconCache;
+    private final AppFilter mAppFilter;
+
+    @Inject
+    public WidgetsModel(
+            @ApplicationContext Context context,
+            InvariantDeviceProfile idp,
+            IconCache iconCache,
+            AppFilter appFilter) {
+        mContext = context;
+        mIdp = idp;
+        mIconCache = iconCache;
+        mAppFilter = appFilter;
+    }
+
+    public WidgetsModel(Context context) {
+        this(context,
+                LauncherAppState.getIDP(context),
+                LauncherAppState.getInstance(context).getIconCache(), new AppFilter(context));
+    }
+
     /**
      * Returns all widgets keyed by their component key.
      */
@@ -128,36 +154,33 @@
      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
      *                    only widgets and shortcuts associated with the package/user are.
      */
-    public List<CachedObject> update(
-            LauncherAppState app, @Nullable PackageUserKey packageUser) {
+    public List<CachedObject> update(@Nullable PackageUserKey packageUser) {
         if (!WIDGETS_ENABLED) {
             return new ArrayList<>();
         }
         Preconditions.assertWorkerThread();
 
-        Context context = app.getContext();
         final ArrayList<WidgetItem> widgetsAndShortcuts = new ArrayList<>();
         List<CachedObject> updatedItems = new ArrayList<>();
         try {
-            InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
             // Widgets
-            WidgetManagerHelper widgetManager = new WidgetManagerHelper(context);
+            WidgetManagerHelper widgetManager = new WidgetManagerHelper(mContext);
             for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(packageUser)) {
                 LauncherAppWidgetProviderInfo launcherWidgetInfo =
-                        LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
+                        LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo);
 
                 widgetsAndShortcuts.add(new WidgetItem(
-                        launcherWidgetInfo, idp, app.getIconCache(), app.getContext()));
+                        launcherWidgetInfo, mIdp, mIconCache, mContext));
                 updatedItems.add(launcherWidgetInfo);
             }
 
             // Shortcuts
             for (ShortcutConfigActivityInfo info :
-                    queryList(context, packageUser)) {
-                widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache()));
+                    queryList(mContext, packageUser)) {
+                widgetsAndShortcuts.add(new WidgetItem(info, mIconCache));
                 updatedItems.add(info);
             }
-            setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);
+            setWidgetsAndShortcuts(widgetsAndShortcuts, packageUser);
         } catch (Exception e) {
             if (!FeatureFlags.IS_STUDIO_BUILD && Utilities.isBinderSizeError(e)) {
                 // the returned value may be incomplete and will not be refreshed until the next
@@ -172,14 +195,14 @@
         return updatedItems;
     }
 
-    private synchronized void setWidgetsAndShortcuts(ArrayList<WidgetItem> rawWidgetsShortcuts,
-            LauncherAppState app, @Nullable PackageUserKey packageUser) {
+    private synchronized void setWidgetsAndShortcuts(
+            ArrayList<WidgetItem> rawWidgetsShortcuts, @Nullable PackageUserKey packageUser) {
         if (DEBUG) {
             Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
         }
 
         // Refresh the validity checker with latest app state.
-        mWidgetValidityCheckForPicker = new WidgetValidityCheckForPicker(app);
+        mWidgetValidityCheckForPicker = new WidgetValidityCheckForPicker(mIdp, mAppFilter);
 
         // Temporary cache for {@link PackageItemInfos} to avoid having to go through
         // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
@@ -196,19 +219,17 @@
         // add and update.
         mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream()
                 .filter(new WidgetFlagCheck())
-                .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
+                .flatMap(widgetItem -> getPackageUserKeys(mContext, widgetItem).stream()
                         .map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem)))
                 .collect(groupingBy(pair -> pair.first, mapping(pair -> pair.second, toList()))));
 
         // Update each package entry
-        IconCache iconCache = app.getIconCache();
         for (PackageItemInfo p : packageItemInfoCache.values()) {
-            iconCache.getTitleAndIconForApp(p, DEFAULT_LOOKUP_FLAG.withUseLowRes());
+            mIconCache.getTitleAndIconForApp(p, DEFAULT_LOOKUP_FLAG.withUseLowRes());
         }
     }
 
-    public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user,
-            LauncherAppState app) {
+    public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user) {
         if (!WIDGETS_ENABLED) {
             return;
         }
@@ -220,11 +241,10 @@
                     WidgetItem item = items.get(i);
                     if (item.user.equals(user)) {
                         if (item.activityInfo != null) {
-                            items.set(i, new WidgetItem(item.activityInfo, app.getIconCache()));
+                            items.set(i, new WidgetItem(item.activityInfo, mIconCache));
                         } else {
-                            items.set(i, new WidgetItem(item.widgetInfo,
-                                    app.getInvariantDeviceProfile(), app.getIconCache(),
-                                    app.getContext()));
+                            items.set(i, new WidgetItem(
+                                    item.widgetInfo, mIdp, mIconCache, mContext));
                         }
                     }
                 }
@@ -277,9 +297,9 @@
         private final InvariantDeviceProfile mIdp;
         private final AppFilter mAppFilter;
 
-        WidgetValidityCheckForPicker(LauncherAppState app) {
-            mIdp = app.getInvariantDeviceProfile();
-            mAppFilter = new AppFilter(app.getContext());
+        WidgetValidityCheckForPicker(InvariantDeviceProfile idp, AppFilter appFilter) {
+            mIdp = idp;
+            mAppFilter = appFilter;
         }
 
         @Override
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 99f2837..7b8f218 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -18,6 +18,7 @@
 import android.annotation.SuppressLint
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
 import android.content.pm.LauncherApps
 import android.content.pm.LauncherApps.ShortcutQuery
@@ -29,10 +30,10 @@
 import android.util.LongSparseArray
 import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
 import com.android.launcher3.icons.CacheableShortcutInfo
+import com.android.launcher3.icons.IconCache
 import com.android.launcher3.icons.cache.CacheLookupFlag.Companion.DEFAULT_LOOKUP_FLAG
 import com.android.launcher3.logging.FileLog
 import com.android.launcher3.model.data.AppInfo
@@ -70,7 +71,10 @@
     private val launcherApps: LauncherApps,
     private val pendingPackages: MutableSet<PackageUserKey>,
     private val shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo>,
-    private val app: LauncherAppState,
+    private val context: Context,
+    private val idp: InvariantDeviceProfile,
+    private val iconCache: IconCache,
+    private val isSafeMode: Boolean,
     private val bgDataModel: BgDataModel,
     private val widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?>,
     private val installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo>,
@@ -82,9 +86,7 @@
     private val allDeepShortcuts: MutableList<CacheableShortcutInfo>,
 ) {
 
-    private val isSafeMode = app.isSafeModeEnabled
     private val tempPackageKey = PackageUserKey(null, null)
-    private val iconCache = app.iconCache
 
     /**
      * This is the entry point for processing 1 workspace item. This method is like the midfielder
@@ -159,7 +161,7 @@
             )
             return
         }
-        val appInfoWrapper = ApplicationInfoWrapper(app.context, targetPkg, c.user)
+        val appInfoWrapper = ApplicationInfoWrapper(context, targetPkg, c.user)
         var validTarget = launcherApps.isPackageEnabled(targetPkg, c.user)
 
         // If it's a deep shortcut, we'll use pinned shortcuts to restore it
@@ -306,7 +308,7 @@
                         )
                         return
                     }
-                    info = WorkspaceItemInfo(pinnedShortcut, app.context)
+                    info = WorkspaceItemInfo(pinnedShortcut, context)
                     // If the pinned deep shortcut is no longer published,
                     // use the last saved icon instead of the default.
                     val csi = CacheableShortcutInfo(pinnedShortcut, appInfoWrapper)
@@ -369,7 +371,7 @@
                     info,
                     activityInfo,
                     userCache.getUserInfo(c.user),
-                    ApiWrapper.INSTANCE[app.context],
+                    ApiWrapper.INSTANCE[context],
                     pmHelper,
                 )
             }
@@ -529,7 +531,7 @@
                         (si == null) &&
                         (lapi == null) &&
                         !(Flags.enableSupportForArchiving() &&
-                            ApplicationInfoWrapper(app.context, component.packageName, c.user)
+                            ApplicationInfoWrapper(context, component.packageName, c.user)
                                 .isArchived())
                 ) {
                     // Restore never started
@@ -553,7 +555,7 @@
                     if (si == null) 0 else (si.getProgress() * 100).toInt()
                 appWidgetInfo.pendingItemInfo =
                     WidgetsModel.newPendingItemInfo(
-                        app.context,
+                        context,
                         appWidgetInfo.providerName,
                         appWidgetInfo.user,
                     )
@@ -563,7 +565,7 @@
                 WidgetSizes.updateWidgetSizeRangesAsync(
                     appWidgetInfo.appWidgetId,
                     lapi,
-                    app.context,
+                    context,
                     appWidgetInfo.spanX,
                     appWidgetInfo.spanY,
                 )
@@ -586,7 +588,7 @@
                         " appWidgetId: ${c.appWidgetId}," +
                         " component=${component}",
                 )
-                logWidgetInfo(app.invariantDeviceProfile, lapi)
+                logWidgetInfo(idp, lapi)
             }
         }
         c.checkAndAddItem(appWidgetInfo, bgDataModel, memoryLogger)
diff --git a/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java b/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java
index 1a6d178..17f1615 100644
--- a/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java
+++ b/src/com/android/launcher3/model/WorkspaceItemSpaceFinder.java
@@ -21,7 +21,7 @@
 import android.util.LongSparseArray;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
@@ -31,23 +31,37 @@
 
 import java.util.ArrayList;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to help find space for new workspace items
  */
 public class WorkspaceItemSpaceFinder {
 
+    private BgDataModel mDataModel;
+    private InvariantDeviceProfile mIDP;
+    private LauncherModel mModel;
+
+    @Inject
+    WorkspaceItemSpaceFinder(
+            BgDataModel dataModel, InvariantDeviceProfile idp, LauncherModel model) {
+        mDataModel = dataModel;
+        mIDP = idp;
+        mModel = model;
+    }
+
     /**
      * Find a position on the screen for the given size or adds a new screen.
      *
      * @return screenId and the coordinates for the item in an int array of size 3.
      */
-    public int[] findSpaceForItem(LauncherAppState app, BgDataModel dataModel,
+    public int[] findSpaceForItem(
             IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) {
         LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
 
         // Use sBgItemsIdMap as all the items are already loaded.
-        synchronized (dataModel) {
-            for (ItemInfo info : dataModel.itemsIdMap) {
+        synchronized (mDataModel) {
+            for (ItemInfo info : mDataModel.itemsIdMap) {
                 if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
                     ArrayList<ItemInfo> items = screenItems.get(info.screenId);
                     if (items == null) {
@@ -75,7 +89,7 @@
         for (int screen = 0; screen < screenCount; screen++) {
             screenId = workspaceScreens.get(screen);
             if (!screensToExclude.contains(screenId) && findNextAvailableIconSpaceInScreen(
-                    app, screenItems.get(screenId), coordinates, spanX, spanY)) {
+                    screenItems.get(screenId), coordinates, spanX, spanY)) {
                 // We found a space for it
                 found = true;
                 break;
@@ -84,7 +98,7 @@
 
         if (!found) {
             // Still no position found. Add a new screen to the end.
-            screenId = app.getModel().getModelDbController().getNewScreenId();
+            screenId = mModel.getModelDbController().getNewScreenId();
 
             // Save the screen id for binding in the workspace
             workspaceScreens.add(screenId);
@@ -92,7 +106,7 @@
 
             // If we still can't find an empty space, then God help us all!!!
             if (!findNextAvailableIconSpaceInScreen(
-                    app, screenItems.get(screenId), coordinates, spanX, spanY)) {
+                    screenItems.get(screenId), coordinates, spanX, spanY)) {
                 throw new RuntimeException("Can't find space to add the item");
             }
         }
@@ -100,11 +114,8 @@
     }
 
     private boolean findNextAvailableIconSpaceInScreen(
-            LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
-            int[] xy, int spanX, int spanY) {
-        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
-
-        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
+            ArrayList<ItemInfo> occupiedPos, int[] xy, int spanX, int spanY) {
+        GridOccupancy occupied = new GridOccupancy(mIDP.numColumns, mIDP.numRows);
         if (occupiedPos != null) {
             for (ItemInfo r : occupiedPos) {
                 occupied.markCells(r, true);
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index b60b8cc..f5e5e16 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -26,6 +26,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Flags;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
@@ -325,10 +326,12 @@
      * Returns a FastBitmapDrawable with the icon and context theme applied
      */
     public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
-        if (!ThemeManager.INSTANCE.get(context).isIconThemeEnabled()) {
+        ThemeManager themeManager = ThemeManager.INSTANCE.get(context);
+        if (!themeManager.isIconThemeEnabled()) {
             creationFlags &= ~FLAG_THEMED;
         }
-        FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags);
+        FastBitmapDrawable drawable = bitmap.newIcon(
+                context, creationFlags, Utilities.getIconShapeOrNull(context));
         drawable.setIsDisabled(isDisabled());
         return drawable;
     }
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index f0c9bd9..c861d0b 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -58,6 +58,9 @@
 /**
  * {@link PageIndicator} which shows dots per page. The active page is shown with the current
  * accent color.
+ * <p>
+ * TODO(b/402258632): Split PageIndicatorDots into 2 different classes: FolderPageIndicator &
+ * WorkspacePageIndicator. A lot of the functionality in this class is only used by one UI purpose.
  */
 public class PageIndicatorDots extends View implements Insettable, PageIndicator {
 
@@ -91,6 +94,7 @@
 
     // This is used to optimize the onDraw method by not constructing a new RectF each draw.
     private static final RectF sTempRect = new RectF();
+    private static final RectF sLastActiveRect = new RectF();
 
     private static final FloatProperty<PageIndicatorDots> CURRENT_POSITION =
             new FloatProperty<PageIndicatorDots>("current_position") {
@@ -553,6 +557,9 @@
                             }
                         }
                     }
+                    if (Math.round(mCurrentPosition) == i) {
+                        sLastActiveRect.set(sTempRect);
+                    }
                     canvas.drawRoundRect(sTempRect, mDotRadius, mDotRadius, mPaginationPaint);
 
                     // TODO(b/394355070) Verify RTL experience works correctly with visual updates
@@ -668,8 +675,8 @@
         @Override
         public void getOutline(View view, Outline outline) {
             if (mEntryAnimationRadiusFactors == null) {
-                // TODO(b/394355070): Verify Outline works correctly with visual updates
-                RectF activeRect = getActiveRect();
+                RectF activeRect = enableLauncherVisualRefresh()
+                        ? sLastActiveRect : getActiveRect();
                 outline.setRoundRect(
                         (int) activeRect.left,
                         (int) activeRect.top,
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 20c0ecc..98a3882 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -219,6 +219,6 @@
     public static UserBadgeDrawable getBadgeDrawable(Context context, UserHandle userHandle) {
         return (UserBadgeDrawable) BitmapInfo.LOW_RES_INFO.withFlags(UserCache.getInstance(context)
                         .getUserInfo(userHandle).applyBitmapInfoFlags(FlagOp.NO_OP))
-                .getBadgeDrawable(context, false /* isThemed */);
+                .getBadgeDrawable(context, false /* isThemed */, null);
     }
 }
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
index c92328d..3641371 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.kt
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -26,19 +26,17 @@
 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.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
 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 {
@@ -155,12 +153,11 @@
                 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 lc = context.appComponent.loaderCursorFactory.createLoaderCursor(c, ums, null)
         val deletedShortcuts = IntSet()
 
         while (lc.moveToNext()) {
diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt
index ebf4656..acb3c4e 100644
--- a/src/com/android/launcher3/util/ItemInflater.kt
+++ b/src/com/android/launcher3/util/ItemInflater.kt
@@ -24,6 +24,7 @@
 import android.view.View.OnFocusChangeListener
 import android.view.ViewGroup
 import com.android.launcher3.BubbleTextView
+import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.R
 import com.android.launcher3.apppairs.AppPairIcon
@@ -46,10 +47,11 @@
     private val widgetHolder: LauncherWidgetHolder,
     private val clickListener: OnClickListener,
     private val focusListener: OnFocusChangeListener,
-    private val defaultParent: ViewGroup
+    private val defaultParent: ViewGroup,
 ) where T : Context, T : ActivityContext {
 
-    private val widgetInflater = WidgetInflater(context)
+    private val widgetInflater =
+        WidgetInflater(context, LauncherAppState.getInstance(context).isSafeModeEnabled)
 
     @JvmOverloads
     fun inflateItem(item: ItemInfo, writer: ModelWriter, nullableParent: ViewGroup? = null): View? {
@@ -75,7 +77,7 @@
                     R.layout.folder_icon,
                     context,
                     parent,
-                    item as FolderInfo
+                    item as FolderInfo,
                 )
             Favorites.ITEM_TYPE_APP_PAIR ->
                 return AppPairIcon.inflateIcon(
@@ -83,7 +85,7 @@
                     context,
                     parent,
                     item as AppPairInfo,
-                    BubbleTextView.DISPLAY_WORKSPACE
+                    BubbleTextView.DISPLAY_WORKSPACE,
                 )
             Favorites.ITEM_TYPE_APPWIDGET,
             Favorites.ITEM_TYPE_CUSTOM_APPWIDGET ->
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 11f0bc2..68e0324 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -520,6 +520,33 @@
          */
         default void onCanCreateDesksChanged(boolean canCreateDesks) {
         }
+
+        /**
+         * Called when a new desk is added.
+         *
+         * @param displayId The ID of the display on which the desk was added.
+         * @param deskId The ID of the newly added desk.
+         */
+        default void onDeskAdded(int displayId, int deskId) {}
+
+        /**
+         * Called when an existing desk is removed.
+         *
+         * @param displayId The ID of the display on which the desk was removed.
+         * @param deskId The ID of the desk that was removed.
+         */
+        default void onDeskRemoved(int displayId, int deskId) {}
+
+        /**
+         * Called when the active desk changes.
+         *
+         * @param displayId The ID of the display on which the desk activation change is happening.
+         * @param newActiveDesk The ID of the new active desk or -1 if no desk is active anymore
+         *                      (i.e. exit desktop mode).
+         * @param oldActiveDesk The ID of the desk that was previously active, or -1 if no desk was
+         *                      active before.
+         */
+        default void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {}
     }
 
 }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 82cc40d..8c01c59 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS;
 
@@ -219,6 +220,11 @@
                     OptionsPopupView::enterHomeGardening));
         }
         options.add(new OptionItem(launcher,
+                R.string.all_apps_home_screen,
+                R.drawable.ic_apps,
+                LAUNCHER_ALL_APPS_TAP_OR_LONGPRESS,
+                OptionsPopupView::enterAllApps));
+        options.add(new OptionItem(launcher,
                 R.string.settings_button_text,
                 R.drawable.ic_setting,
                 LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
@@ -226,6 +232,20 @@
         return options;
     }
 
+    /**
+     * Used by the options to open All Apps, uses an intent as to not tie the implementation of
+     * opening All Apps with OptionsPopup, instead it uses the public API to open All Apps.
+     */
+    public static boolean enterAllApps(View view) {
+        Launcher launcher = Launcher.getLauncher(view.getContext());
+        launcher.startActivity(
+                new Intent(Intent.ACTION_ALL_APPS)
+                .setComponent(launcher.getComponentName())
+                .setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+        );
+        return true;
+    }
+
     private static boolean enterHomeGardening(View view) {
         Launcher launcher = Launcher.getLauncher(view.getContext());
         launcher.getStateManager().goToState(EDIT_MODE);
diff --git a/src/com/android/launcher3/widget/WidgetInflater.kt b/src/com/android/launcher3/widget/WidgetInflater.kt
index d6cadc7..03f680a 100644
--- a/src/com/android/launcher3/widget/WidgetInflater.kt
+++ b/src/com/android/launcher3/widget/WidgetInflater.kt
@@ -19,14 +19,21 @@
 import android.content.Context
 import com.android.launcher3.BuildConfig
 import com.android.launcher3.Launcher
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
+import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.logging.FileLog
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.qsb.QsbContainerView
+import javax.inject.Inject
+import javax.inject.Named
 
 /** Utility class for handling widget inflation taking into account all the restore state updates */
-class WidgetInflater(private val context: Context) {
+class WidgetInflater
+@Inject
+constructor(
+    @ApplicationContext private val context: Context,
+    @Named("SAFE_MODE") private val isSafeModeEnabled: Boolean,
+) {
 
     private val widgetHelper = WidgetManagerHelper(context)
 
@@ -41,9 +48,8 @@
                 )
             }
         }
-        if (LauncherAppState.INSTANCE.get(context).isSafeModeEnabled) {
-            return InflationResult(TYPE_PENDING)
-        }
+        if (isSafeModeEnabled) return InflationResult(TYPE_PENDING)
+
         val appWidgetInfo: LauncherAppWidgetProviderInfo?
         var removalReason = ""
         @RestoreError var logReason = RestoreError.OTHER_WIDGET_INFLATION_FAIL
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 4ccf16b..ec91622 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY;
 import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering;
 
 import android.content.ComponentName;
@@ -55,6 +56,7 @@
 public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
     private @Px float mAvailableHeight = Float.MAX_VALUE;
     private @Px float mAvailableWidth = 0;
+    private int mLastUiMode = -1;
     private static final String INITIALLY_DISPLAYED_WIDGETS_STATE_KEY =
             "widgetRecommendationsView:mDisplayedWidgets";
     private static final int MAX_CATEGORIES = 3;
@@ -151,41 +153,7 @@
         mPageSwitchListeners.add(pageChangeListener);
     }
 
-    /**
-     * Displays all the provided recommendations in a single table if they fit.
-     *
-     * @param recommendedWidgets list of widgets to be displayed in recommendation section.
-     * @param deviceProfile      the current {@link DeviceProfile}
-     * @param availableHeight    height in px that can be used to display the recommendations;
-     *                           recommendations that don't fit in this height won't be shown
-     * @param availableWidth     width in px that the recommendations should display in
-     * @param cellPadding        padding in px that should be applied to each widget in the
-     *                           recommendations
-     * @return number of recommendations that could fit in the available space.
-     */
-    public int setRecommendations(
-            List<WidgetItem> recommendedWidgets, DeviceProfile deviceProfile,
-            final @Px float availableHeight, final @Px int availableWidth,
-            final @Px int cellPadding) {
-        this.mAvailableHeight = availableHeight;
-        this.mAvailableWidth = availableWidth;
-        clear();
-
-        Set<ComponentName> displayedWidgets = maybeDisplayInTable(recommendedWidgets,
-                deviceProfile,
-                availableWidth, cellPadding);
-
-        if (mDisplayedWidgets.isEmpty()) {
-            // Save the widgets shown for the first time user opened the picker; so that, they can
-            // be maintained across orientation changes.
-            mDisplayedWidgets = displayedWidgets;
-        }
-
-        updateTitleAndIndicator(/* requestedPage= */ 0);
-        return displayedWidgets.size();
-    }
-
-    private boolean shouldShowFullPageView(
+    private boolean shouldShowSinglePageView(
             Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations) {
         if (mShowFullPageViewIfLowDensity) {
             boolean hasLessCategories = recommendations.size() < MAX_CATEGORIES;
@@ -213,63 +181,82 @@
      * @param cellPadding     padding in px that should be applied to each widget in the
      *                        recommendations
      * @param requestedPage   page number to display initially.
+     * @param forceUpdate     whether to re-render even if available space didn't change
      * @return number of recommendations that could fit in the available space.
      */
     public int setRecommendations(
             Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
             DeviceProfile deviceProfile, final @Px float availableHeight,
-            final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
-        if (shouldShowFullPageView(recommendations)) {
-            // Show all widgets in single page with unlimited available height.
-            return setRecommendations(
-                    recommendations.values().stream().flatMap(Collection::stream)
-                            .collect(Collectors.toList()),
-                    deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth,
-                    cellPadding);
+            final @Px int availableWidth, final @Px int cellPadding, final int requestedPage,
+            final boolean forceUpdate) {
+        if (forceUpdate || shouldUpdate(availableWidth, availableHeight)) {
+            Context context = getContext();
+            this.mAvailableHeight = availableHeight;
+            this.mAvailableWidth = availableWidth;
+            this.mLastUiMode = context.getResources().getConfiguration().uiMode;
 
-        }
-        this.mAvailableHeight = availableHeight;
-        this.mAvailableWidth = availableWidth;
-        Context context = getContext();
-        // For purpose of recommendations section, we don't want paging dots to be halved in two
-        // pane display, so, we always provide isTwoPanels = "false".
-        mPageIndicator.setPauseScroll(/*pause=*/true, /*isTwoPanels=*/ false);
-        clear();
-
-        int displayedCategories = 0;
-        Set<ComponentName> allDisplayedWidgets = new HashSet<>();
-
-        // Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
-        for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
-                new TreeMap<>(recommendations).entrySet()) {
-            // If none of the recommendations for the category could fit in the mAvailableHeight, we
-            // don't want to add that category; and we look for the next one.
-            Set<ComponentName> displayedWidgetsForCategory = maybeDisplayInTable(entry.getValue(),
-                    deviceProfile,
-                    availableWidth, cellPadding);
-            if (!displayedWidgetsForCategory.isEmpty()) {
-                mCategoryTitles.add(
-                        context.getResources().getString(entry.getKey().categoryTitleRes));
-                displayedCategories++;
-                allDisplayedWidgets.addAll(displayedWidgetsForCategory);
+            final Map<WidgetRecommendationCategory, List<WidgetItem>> mappedRecommendations;
+            if (shouldShowSinglePageView(recommendations)) { // map to single category.
+                mappedRecommendations = Map.of(DEFAULT_WIDGET_RECOMMENDATION_CATEGORY,
+                        recommendations.values().stream().flatMap(
+                                Collection::stream).toList());
+            } else {
+                mappedRecommendations = recommendations;
             }
 
-            if (displayedCategories == MAX_CATEGORIES) {
-                break;
+            // For purpose of recommendations section, we don't want paging dots to be halved in two
+            // pane display, so, we always provide isTwoPanels = "false".
+            mPageIndicator.setPauseScroll(/*pause=*/true, /*isTwoPanels=*/ false);
+            clear();
+
+            int displayedCategories = 0;
+            Set<ComponentName> allDisplayedWidgets = new HashSet<>();
+
+            // Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
+            for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
+                    new TreeMap<>(mappedRecommendations).entrySet()) {
+                // If none of the recommendations for the category could fit in the
+                // mAvailableHeight, we don't want to add that category; and we look for the next
+                // one.
+                Set<ComponentName> displayedWidgetsForCategory = maybeDisplayInTable(
+                        entry.getValue(),
+                        deviceProfile,
+                        availableWidth, cellPadding);
+                if (!displayedWidgetsForCategory.isEmpty()) {
+                    mCategoryTitles.add(
+                            context.getResources().getString(entry.getKey().categoryTitleRes));
+                    displayedCategories++;
+                    allDisplayedWidgets.addAll(displayedWidgetsForCategory);
+                }
+
+                if (displayedCategories == MAX_CATEGORIES) {
+                    break;
+                }
             }
-        }
 
-        if (mDisplayedWidgets.isEmpty()) {
-            // Save the widgets shown for the first time user opened the picker; so that, they can
-            // be maintained across orientation changes.
-            mDisplayedWidgets = allDisplayedWidgets;
-        }
+            if (mDisplayedWidgets.isEmpty()) {
+                // Save the widgets shown for the first time user opened the picker; so that,
+                // they can
+                // be maintained across orientation changes.
+                mDisplayedWidgets = allDisplayedWidgets;
+            }
 
-        updateTitleAndIndicator(requestedPage);
-        // For purpose of recommendations section, we don't want paging dots to be halved in two
-        // pane display, so, we always provide isTwoPanels = "false".
-        mPageIndicator.setPauseScroll(/*pause=*/false, /*isTwoPanels=*/false);
-        return allDisplayedWidgets.size();
+            updateTitleAndIndicator(requestedPage);
+            // For purpose of recommendations section, we don't want paging dots to be halved in two
+            // pane display, so, we always provide isTwoPanels = "false".
+            mPageIndicator.setPauseScroll(/*pause=*/false, /*isTwoPanels=*/false);
+            return allDisplayedWidgets.size();
+        } else {
+            return mDisplayedWidgets.size();
+        }
+    }
+
+    /**
+     * Returns if we should re-render the views.
+     */
+    private boolean shouldUpdate(int availableWidth, float availableHeight) {
+        return this.mAvailableWidth != availableWidth || this.mAvailableHeight != availableHeight
+                || getContext().getResources().getConfiguration().uiMode != this.mLastUiMode;
     }
 
     private void clear() {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index b0abf23..44c0ebd 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -628,10 +628,12 @@
         if (mIsInSearchMode) {
             return;
         }
+        boolean forceUpdate = false;
         // We avoid applying new recommendations when some are already displayed.
         if (mRecommendedWidgetsMap.isEmpty()) {
             mRecommendedWidgetsMap =
                     mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
+            forceUpdate = true;
         }
         mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
                 mRecommendedWidgetsMap,
@@ -639,7 +641,8 @@
                 /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
                 /* availableWidth= */ mMaxSpanPerRow,
                 /* cellPadding= */ mWidgetCellHorizontalPadding,
-                /* requestedPage= */ mRecommendationsCurrentPage
+                /* requestedPage= */ mRecommendationsCurrentPage,
+                /* forceUpdate= */ forceUpdate
         );
 
         mWidgetRecommendationsContainer.setVisibility(
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 679b0f5..fc99fcc 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -112,21 +112,48 @@
         // Bind the widget items.
         for (int i = 0; i < widgetItemsTable.size(); i++) {
             List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
-            for (int j = 0; j < widgetItemsPerRow.size(); j++) {
-                WidgetTableRow row = (WidgetTableRow) table.getChildAt(i);
-                row.setVisibility(View.VISIBLE);
-                WidgetCell widget = (WidgetCell) row.getChildAt(j);
-                widget.clear();
-                widget.addPreviewReadyListener(row);
-                WidgetItem widgetItem = widgetItemsPerRow.get(j);
-                widget.setVisibility(View.VISIBLE);
+            WidgetTableRow row = (WidgetTableRow) table.getChildAt(i);
 
-                widget.applyFromCellItem(widgetItem);
-                widget.requestLayout();
+            if (areRowItemsUnchanged(row, widgetItemsPerRow)) {  // Just show widgets in row as is
+                row.setVisibility(View.VISIBLE);
+                for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+                    WidgetCell widget = (WidgetCell) row.getChildAt(j);
+                    widget.setVisibility(View.VISIBLE);
+                }
+            } else {
+                for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+                    row.setVisibility(View.VISIBLE);
+                    WidgetCell widget = (WidgetCell) row.getChildAt(j);
+                    widget.clear();
+                    WidgetItem widgetItem = widgetItemsPerRow.get(j);
+                    widget.addPreviewReadyListener(row);
+                    widget.setVisibility(View.VISIBLE);
+
+                    widget.applyFromCellItem(widgetItem);
+                    widget.requestLayout();
+                }
             }
         }
     }
 
+    private boolean areRowItemsUnchanged(WidgetTableRow row, List<WidgetItem> widgetItemsPerRow) {
+        // NOTE: on rotation or fold / unfold, we bind different view holders
+        // so, we don't any special handling for that case.
+        if (row.getChildCount() != widgetItemsPerRow.size()) { // Items not equal
+            return false;
+        }
+
+        for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+            WidgetCell widgetCell = (WidgetCell) row.getChildAt(j);
+            WidgetItem widgetItem = widgetItemsPerRow.get(j);
+            if (widgetCell.getWidgetItem() == null
+                    || !widgetCell.getWidgetItem().equals(widgetItem)) {
+                return false; // Items at given position in row aren't same.
+            }
+        }
+        return true;
+    }
+
     /**
      * Adds and hides table rows and columns from {@code table} to ensure there is sufficient room
      * to display {@code widgetItemsTable}.
@@ -151,26 +178,31 @@
                 tableRow.setGravity(Gravity.TOP);
                 table.addView(tableRow);
             }
-            // Pass resize delay to let the "move" and "change" animations run before resizing the
-            // row.
-            tableRow.setupRow(widgetItems.size(),
-                    /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY);
-            if (tableRow.getChildCount() > widgetItems.size()) {
-                for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
-                    tableRow.getChildAt(j).setVisibility(View.GONE);
-                }
-            } else {
-                for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
-                    WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
-                            R.layout.widget_cell, tableRow, false);
-                    // set up touch.
-                    widget.setOnClickListener(mIconClickListener);
-                    widget.addPreviewReadyListener(tableRow);
-                    View preview = widget.findViewById(R.id.widget_preview_container);
-                    preview.setOnClickListener(mIconClickListener);
-                    preview.setOnLongClickListener(mIconLongClickListener);
-                    widget.setAnimatePreview(false);
-                    tableRow.addView(widget);
+
+            // If the row items are unchanged, we don't need to re-setup the row or the items;
+            // we can just show the row as is.
+            if (!areRowItemsUnchanged(tableRow, widgetItems)) {
+                // Pass resize delay to let the "move" and "change" animations run before resizing
+                // the row.
+                tableRow.setupRow(widgetItems.size(),
+                        /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY);
+                if (tableRow.getChildCount() > widgetItems.size()) {
+                    for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
+                        tableRow.getChildAt(j).setVisibility(View.GONE);
+                    }
+                } else {
+                    for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
+                        WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
+                                R.layout.widget_cell, tableRow, false);
+                        // set up touch.
+                        widget.setOnClickListener(mIconClickListener);
+                        widget.addPreviewReadyListener(tableRow);
+                        View preview = widget.findViewById(R.id.widget_preview_container);
+                        preview.setOnClickListener(mIconClickListener);
+                        preview.setOnLongClickListener(mIconLongClickListener);
+                        widget.setAnimatePreview(false);
+                        tableRow.addView(widget);
+                    }
                 }
             }
         }
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
index d611ae8..91ba628 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
@@ -34,19 +34,20 @@
     private val context = InstrumentationRegistry.getInstrumentation().targetContext
     private val canvas = mock<Canvas>()
     private val systemUnderTest =
-        UserBadgeDrawable(context, R.drawable.ic_work_app_badge, R.color.badge_tint_work, false)
+        UserBadgeDrawable(
+            context,
+            R.drawable.ic_work_app_badge,
+            R.color.badge_tint_work,
+            false /* isThemed */,
+            null, /* shape */
+        )
 
     @Test
     fun draw_opaque() {
         val colorList = mutableListOf<Int>()
-        whenever(
-            canvas.drawCircle(
-                any(),
-                any(),
-                any(),
-                any()
-            )
-        ).then { colorList.add(it.getArgument<Paint>(3).color) }
+        whenever(canvas.drawCircle(any(), any(), any(), any())).then {
+            colorList.add(it.getArgument<Paint>(3).color)
+        }
 
         systemUnderTest.alpha = 255
         systemUnderTest.draw(canvas)
@@ -57,14 +58,9 @@
     @Test
     fun draw_transparent() {
         val colorList = mutableListOf<Int>()
-        whenever(
-            canvas.drawCircle(
-                any(),
-                any(),
-                any(),
-                any()
-            )
-        ).then { colorList.add(it.getArgument<Paint>(3).color) }
+        whenever(canvas.drawCircle(any(), any(), any(), any())).then {
+            colorList.add(it.getArgument<Paint>(3).color)
+        }
 
         systemUnderTest.alpha = 0
         systemUnderTest.draw(canvas)
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index 08b8f81..25f4cf1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -33,7 +33,6 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.same
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.verifyNoMoreInteractions
 import org.mockito.kotlin.whenever
@@ -142,10 +141,9 @@
     /** Sets up the item space data that will be returned from WorkspaceItemSpaceFinder. */
     private fun givenNewItemSpaces(vararg newItemSpaces: NewItemSpace) {
         val spaceStack = newItemSpaces.toMutableList()
-        whenever(
-                mWorkspaceItemSpaceFinder.findSpaceForItem(any(), any(), any(), any(), any(), any())
-            )
-            .then { spaceStack.removeFirst().toIntArray() }
+        whenever(mWorkspaceItemSpaceFinder.findSpaceForItem(any(), any(), any(), any())).then {
+            spaceStack.removeFirst().toIntArray()
+        }
     }
 
     /**
@@ -155,8 +153,6 @@
     private fun verifyItemSpaceFinderCall(nonEmptyScreenIds: List<Int>, numberOfExpectedCall: Int) {
         verify(mWorkspaceItemSpaceFinder, times(numberOfExpectedCall))
             .findSpaceForItem(
-                eq(mAppState),
-                same(mModelHelper.bgDataModel),
                 eq(IntArray.wrap(*nonEmptyScreenIds.toIntArray())),
                 eq(IntArray()),
                 eq(1),
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
index ad40818..b848d27 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -55,7 +55,6 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
 import android.database.MatrixCursor;
 import android.graphics.Bitmap;
@@ -76,6 +75,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext;
 import com.android.launcher3.util.PackageManagerHelper;
 
 import org.junit.After;
@@ -96,11 +96,10 @@
 
     private LauncherModelHelper mModelHelper;
     private LauncherAppState mApp;
-    private PackageManagerHelper mPmHelper;
 
     private MatrixCursor mCursor;
     private InvariantDeviceProfile mIDP;
-    private Context mContext;
+    private SandboxModelContext mContext;
 
     private LoaderCursor mLoaderCursor;
 
@@ -116,7 +115,6 @@
         mContext = mModelHelper.sandboxContext;
         mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
         mApp = LauncherAppState.getInstance(mContext);
-        mPmHelper = PackageManagerHelper.INSTANCE.get(mContext);
 
         mCursor = new MatrixCursor(new String[] {
                 ICON, TITLE, _ID, CONTAINER, ITEM_TYPE,
@@ -126,7 +124,8 @@
         });
 
         UserManagerState ums = new UserManagerState();
-        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums, mPmHelper, null);
+        mLoaderCursor = mContext.getAppComponent().getLoaderCursorFactory()
+                .createLoaderCursor(mCursor, ums, null);
         ums.allUsers.put(0, Process.myUserHandle());
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
index eec6eed..5fd93d1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
@@ -27,8 +27,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.AppFilter
 import com.android.launcher3.Flags.FLAG_ENABLE_PRIVATE_SPACE
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.PackageUpdatedTask.OP_ADD
 import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE
@@ -39,17 +40,20 @@
 import com.android.launcher3.model.PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.AllModulesForTest
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.LauncherModelHelper
-import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.TestUtil
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
@@ -61,10 +65,8 @@
     @get:Rule val setFlagsRule = SetFlagsRule()
 
     private val mUser = myUserHandle()
-    private val mDataModel: BgDataModel = BgDataModel()
     private val mLauncherModelHelper = LauncherModelHelper()
-    private val mContext: SandboxModelContext = spy(mLauncherModelHelper.sandboxContext)
-    private val mAppState: LauncherAppState = spy(LauncherAppState.getInstance(mContext))
+    private val mContext = mLauncherModelHelper.sandboxContext
 
     private val expectedPackage = "Test.Package"
     private val expectedComponent = ComponentName(expectedPackage, "TestClass")
@@ -72,22 +74,33 @@
     private val expectedWorkspaceItem = spy(WorkspaceItemInfo())
 
     private val mockIconCache: IconCache = mock()
-    private val mockTaskController: ModelTaskController = mock<ModelTaskController>()
     private val mockAppFilter: AppFilter = mock<AppFilter>()
+    private lateinit var mAllAppsList: AllAppsList
+
     private val mockApplicationInfo: ApplicationInfo = mock<ApplicationInfo>()
     private val mockActivityInfo: ActivityInfo = mock<ActivityInfo>()
 
-    private lateinit var mAllAppsList: AllAppsList
+    private lateinit var mDataModel: BgDataModel
+    private lateinit var mockTaskController: ModelTaskController
 
     @Before
     fun setup() {
         mAllAppsList = spy(AllAppsList(mockIconCache, mockAppFilter))
-        mLauncherModelHelper.sandboxContext.spyService(LauncherApps::class.java).apply {
+        mContext.initDaggerComponent(
+            DaggerPackageUpdatedTaskTest_TestComponent.builder()
+                .bindAllAppsList(mAllAppsList)
+                .bindAppFilter(mockAppFilter)
+                .bindIconCache(mockIconCache)
+        )
+
+        mContext.spyService(LauncherApps::class.java).apply {
             whenever(getActivityList(expectedPackage, mUser))
                 .thenReturn(listOf(expectedActivityInfo))
         }
-        whenever(mAppState.iconCache).thenReturn(mockIconCache)
-        whenever(mockTaskController.app).thenReturn(mAppState)
+
+        mockTaskController = spy((mContext.appComponent as TestComponent).getTaskController())
+        mDataModel = (mContext.appComponent as TestComponent).getDataModel()
+
         whenever(mockAppFilter.shouldShowApp(expectedComponent)).thenReturn(true)
         mockApplicationInfo.apply {
             uid = 1
@@ -122,7 +135,6 @@
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mockIconCache).updateIconsForPkg(expectedPackage, mUser)
         verify(mAllAppsList).addPackage(mContext, expectedPackage, mUser)
@@ -141,7 +153,6 @@
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mockIconCache).updateIconsForPkg(expectedPackage, mUser)
         verify(mAllAppsList).updatePackage(mContext, expectedPackage, mUser)
@@ -159,7 +170,6 @@
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mockIconCache).removeIconsForPkg(expectedPackage, mUser)
         verify(mAllAppsList).removePackage(expectedPackage, mUser)
@@ -176,7 +186,6 @@
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mAllAppsList).removePackage(expectedPackage, mUser)
         verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
@@ -190,10 +199,11 @@
         // When
         mDataModel.addItem(mContext, expectedWorkspaceItem, true)
         mAllAppsList.add(AppInfo(mContext, expectedActivityInfo, mUser), expectedActivityInfo)
+        mAllAppsList.getAndResetChangeFlag()
+        doAnswer {}.whenever(mockTaskController).bindApplicationsIfNeeded()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mAllAppsList).updateDisabledFlags(any(), any())
         verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWorkspaceItem))
@@ -206,10 +216,11 @@
         val taskUnderTest = PackageUpdatedTask(OP_UNSUSPEND, mUser, expectedPackage)
         // When
         mDataModel.addItem(mContext, expectedWorkspaceItem, true)
+        mAllAppsList.getAndResetChangeFlag()
+        doAnswer {}.whenever(mockTaskController).bindApplicationsIfNeeded()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mAllAppsList).updateDisabledFlags(any(), any())
         verify(mockTaskController).bindUpdatedWorkspaceItems(emptyList())
@@ -226,10 +237,29 @@
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             taskUnderTest.execute(mockTaskController, mDataModel, mAllAppsList)
         }
-        mLauncherModelHelper.loadModelSync()
         // Then
         verify(mAllAppsList).updateDisabledFlags(any(), any())
         verify(mockTaskController).bindUpdatedWorkspaceItems(emptyList())
         assertThat(mAllAppsList.data).isEmpty()
     }
+
+    @LauncherAppSingleton
+    @Component(modules = [AllModulesForTest::class])
+    interface TestComponent : LauncherAppComponent {
+
+        fun getDataModel(): BgDataModel
+
+        fun getTaskController(): ModelTaskController
+
+        @Component.Builder
+        interface Builder : LauncherAppComponent.Builder {
+            @BindsInstance fun bindAppFilter(appFilter: AppFilter): Builder
+
+            @BindsInstance fun bindIconCache(iconCache: IconCache): Builder
+
+            @BindsInstance fun bindAllAppsList(list: AllAppsList): Builder
+
+            override fun build(): TestComponent
+        }
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
index c6863f4..0ab736b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
@@ -30,7 +30,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.Flags
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.icons.CacheableShortcutInfo
@@ -73,7 +72,6 @@
     private val user: UserHandle = myUserHandle()
     private val mockTaskController: ModelTaskController = mock()
     private val mockAllApps: AllAppsList = mock()
-    private val mockAppState: LauncherAppState = mock()
     private val mockIconCache: IconCache = mock()
 
     private val expectedWai =
@@ -93,9 +91,8 @@
         modelHelper.loadModelSync()
         context = modelHelper.sandboxContext
         launcherApps = context.spyService(LauncherApps::class.java)
-        whenever(mockTaskController.app).thenReturn(mockAppState)
-        whenever(mockAppState.context).thenReturn(context)
-        whenever(mockAppState.iconCache).thenReturn(mockIconCache)
+        whenever(mockTaskController.context).thenReturn(context)
+        whenever(mockTaskController.iconCache).thenReturn(mockIconCache)
         whenever(mockIconCache.getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>()))
             .then { _ -> { expectedWai.bitmap = BitmapInfo.LOW_RES_INFO } }
         shortcuts = emptyList()
@@ -132,8 +129,7 @@
         // When
         shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
         // Then
-        verify(mockAppState.iconCache)
-            .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+        verify(mockIconCache).getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
         verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
     }
 
@@ -196,8 +192,7 @@
         // When
         shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
         // Then
-        verify(mockAppState.iconCache)
-            .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+        verify(mockIconCache).getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
         verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
     }
 
@@ -256,8 +251,7 @@
         // When
         shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
         // Then
-        verify(mockAppState.iconCache)
-            .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+        verify(mockIconCache).getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
         verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
     }
 
@@ -287,8 +281,7 @@
         // When
         shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
         // Then
-        verify(mockAppState.iconCache)
-            .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+        verify(mockIconCache).getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
         verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
index 777d81b..2e6ecc5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -25,9 +25,9 @@
 import android.platform.test.rule.LimitDevicesRule
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.AppFilter
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.data.PackageItemInfo
 import com.android.launcher3.pm.UserCache
@@ -62,7 +62,6 @@
     @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
     @Mock private lateinit var appWidgetManager: AppWidgetManager
-    @Mock private lateinit var app: LauncherAppState
     @Mock private lateinit var iconCacheMock: IconCache
 
     private lateinit var context: Context
@@ -93,9 +92,6 @@
 
         whenever(iconCacheMock.getTitleNoCache(any<LauncherAppWidgetProviderInfo>()))
             .thenReturn("title")
-        whenever(app.iconCache).thenReturn(iconCacheMock)
-        whenever(app.context).thenReturn(context)
-        whenever(app.invariantDeviceProfile).thenReturn(idp)
 
         val widgetToCategoryEntry: Map.Entry<ComponentName, IntSet> =
             WidgetSections.getWidgetsToCategory(context).entries.first()
@@ -128,7 +124,7 @@
         val userCache = spy(UserCache.INSTANCE.get(context))
         whenever(userCache.userProfiles).thenReturn(listOf(UserHandle.CURRENT))
 
-        underTest = WidgetsModel()
+        underTest = WidgetsModel(context, idp, iconCacheMock, AppFilter(context))
     }
 
     @Test
@@ -237,7 +233,7 @@
                     update = false
                     // Similarly, model could update its code independently while a client is
                     // iterating on the list.
-                    underTest.update(app, /* packageUser= */ null)
+                    underTest.update(/* packageUser= */ null)
                 }
             }
 
@@ -253,7 +249,7 @@
     private fun loadWidgets() {
         val latch = CountDownLatch(1)
         Executors.MODEL_EXECUTOR.execute {
-            underTest.update(app, /* packageUser= */ null)
+            underTest.update(/* packageUser= */ null)
             latch.countDown()
         }
         if (!latch.await(LOAD_WIDGETS_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index a55d64b..cba7b88 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -31,7 +31,7 @@
 import android.util.LongSparseArray
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.Flags
-import com.android.launcher3.LauncherAppState
+import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
@@ -41,6 +41,7 @@
 import com.android.launcher3.Utilities.EMPTY_PERSON_ARRAY
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
 import com.android.launcher3.icons.CacheableShortcutInfo
+import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.ItemInfo
@@ -66,12 +67,14 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Answers
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
@@ -88,12 +91,12 @@
     @Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
     @Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
     @Mock private lateinit var mockBgDataModel: BgDataModel
-    @Mock private lateinit var mockAppState: LauncherAppState
     @Mock private lateinit var mockPmHelper: PackageManagerHelper
-    @Mock private lateinit var mockCursor: LoaderCursor
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var mockCursor: LoaderCursor
     @Mock private lateinit var mockUserCache: UserCache
     @Mock private lateinit var mockUserManagerState: UserManagerState
     @Mock private lateinit var mockWidgetInflater: WidgetInflater
+    @Mock private lateinit var mockIconCache: IconCache
 
     lateinit var mModelHelper: LauncherModelHelper
     lateinit var mContext: SandboxModelContext
@@ -114,6 +117,7 @@
 
     @Before
     fun setup() {
+        MockitoAnnotations.initMocks(this)
         mModelHelper = LauncherModelHelper()
         mContext = mModelHelper.sandboxContext
         mLauncherApps =
@@ -122,9 +126,6 @@
                 doReturn(true).whenever(this).isActivityEnabled(mComponentName, mUserHandle)
             }
         mUserHandle = Process.myUserHandle()
-        mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
-        mockWorkspaceInfo = mock<WorkspaceItemInfo>()
-        mockBgDataModel = mock<BgDataModel>()
         mComponentName = ComponentName("package", "class")
         mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
         mIntent =
@@ -133,40 +134,26 @@
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
-        mockAppState =
-            mock<LauncherAppState>().apply {
-                whenever(context).thenReturn(mContext)
-                whenever(iconCache).thenReturn(mock())
-                whenever(iconCache.getShortcutIcon(any(), any(), any())).then {}
-            }
-        mockPmHelper =
-            mock<PackageManagerHelper>().apply {
-                whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
-                    .thenReturn(mIntent)
-            }
-        mockCursor =
-            mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
-                user = mUserHandle
-                itemType = ITEM_TYPE_APPLICATION
-                id = 1
-                restoreFlag = 1
-                serialNumber = 101
-                whenever(parseIntent()).thenReturn(mIntent)
-                whenever(markRestored()).doAnswer { restoreFlag = 0 }
-                whenever(updater().put(Favorites.INTENT, mIntent.toUri(0)).commit()).thenReturn(1)
-                whenever(getAppShortcutInfo(any(), any(), any(), any()))
-                    .thenReturn(mockWorkspaceInfo)
-                whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
-            }
-        mockUserCache =
-            mock<UserCache>().apply {
-                val userIconInfo =
-                    mock<UserIconInfo>().apply { whenever(isPrivate).thenReturn(false) }
-                whenever(getUserInfo(any())).thenReturn(userIconInfo)
-            }
+        whenever(mockIconCache.getShortcutIcon(any(), any(), any())).then {}
+        whenever(mockPmHelper.getAppLaunchIntent(mComponentName.packageName, mUserHandle))
+            .thenReturn(mIntent)
+        mockCursor.apply {
+            user = mUserHandle
+            itemType = ITEM_TYPE_APPLICATION
+            id = 1
+            restoreFlag = 1
+            serialNumber = 101
+            whenever(parseIntent()).thenReturn(mIntent)
+            whenever(markRestored()).doAnswer { restoreFlag = 0 }
+            whenever(updater().put(Favorites.INTENT, mIntent.toUri(0)).commit()).thenReturn(1)
+            whenever(getAppShortcutInfo(any(), any(), any(), any())).thenReturn(mockWorkspaceInfo)
+            whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
+        }
+        mockUserCache.apply {
+            val userIconInfo = mock<UserIconInfo>().apply { whenever(isPrivate).thenReturn(false) }
+            whenever(getUserInfo(any())).thenReturn(userIconInfo)
+        }
 
-        mockUserManagerState = mock<UserManagerState>()
-        mockWidgetInflater = mock<WidgetInflater>()
         mKeyToPinnedShortcutsMap = mutableMapOf()
         mInstallingPkgs = hashMapOf()
         mAllDeepShortcuts = mutableListOf()
@@ -187,7 +174,6 @@
         userManagerState: UserManagerState = mockUserManagerState,
         launcherApps: LauncherApps = mLauncherApps,
         shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
-        app: LauncherAppState = mockAppState,
         bgDataModel: BgDataModel = mockBgDataModel,
         widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> = mWidgetProvidersMap,
         widgetInflater: WidgetInflater = mockWidgetInflater,
@@ -205,7 +191,7 @@
             userCache = userCache,
             userManagerState = userManagerState,
             launcherApps = launcherApps,
-            app = app,
+            context = mContext,
             bgDataModel = bgDataModel,
             widgetProvidersMap = widgetProvidersMap,
             widgetInflater = widgetInflater,
@@ -217,6 +203,9 @@
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
             allDeepShortcuts = allDeepShortcuts,
+            iconCache = mockIconCache,
+            idp = InvariantDeviceProfile.INSTANCE.get(mContext),
+            isSafeMode = false,
         )
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
index dd03eee..a18f93b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
@@ -29,8 +29,6 @@
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemSpaceFinderTest : AbstractWorkspaceModelTest() {
 
-    private val mItemSpaceFinder = WorkspaceItemSpaceFinder()
-
     @Before
     override fun setup() {
         super.setup()
@@ -42,15 +40,12 @@
     }
 
     private fun findSpace(spanX: Int, spanY: Int): NewItemSpace =
-        mItemSpaceFinder
-            .findSpaceForItem(
-                mAppState,
+        WorkspaceItemSpaceFinder(
                 mModelHelper.bgDataModel,
-                mExistingScreens,
-                mNewScreens,
-                spanX,
-                spanY,
+                mAppState.invariantDeviceProfile,
+                mModelHelper.model,
             )
+            .findSpaceForItem(mExistingScreens, mNewScreens, spanX, spanY)
             .let { NewItemSpace.fromIntArray(it) }
 
     private fun assertRegionVacant(newItemSpace: NewItemSpace, spanX: Int, spanY: Int) {
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 95d5076..f490bd6 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.Test;
 
@@ -127,7 +126,6 @@
      * Adds three icons to the workspace and removes one of them by dragging to uninstall.
      */
     @Test
-    @ScreenRecordRule.ScreenRecord // b/399756302
     @PlatinumTest(focusArea = "launcher")
     public void uninstallWorkspaceIcon() throws IOException {
         Point[] gridPositions = TestUtil.getCornersAndCenterPositions(mLauncher);
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index 246219f..7f9b7a0 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -16,7 +16,6 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
 import com.android.launcher3.Flags
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherModel
 import com.android.launcher3.LauncherModel.LoaderTransaction
 import com.android.launcher3.LauncherPrefs
@@ -31,6 +30,7 @@
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.icons.cache.CachingLogic
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
+import com.android.launcher3.model.LoaderTask.LoaderTaskFactory
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -43,7 +43,6 @@
 import com.android.launcher3.util.LooperIdleLock
 import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.UserIconInfo
-import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -57,7 +56,6 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -93,26 +91,28 @@
 
     @Mock private lateinit var bgAllAppsList: AllAppsList
     @Mock private lateinit var modelDelegate: ModelDelegate
-    @Mock private lateinit var launcherBinder: BaseLauncherBinder
-    private lateinit var launcherModel: LauncherModel
-    @Mock private lateinit var transaction: LoaderTransaction
+    @Mock private lateinit var launcherModel: LauncherModel
     @Mock private lateinit var iconCache: IconCache
-    @Mock private lateinit var idleLock: LooperIdleLock
-    @Mock private lateinit var iconCacheUpdateHandler: IconCacheUpdateHandler
     @Mock private lateinit var userCache: UserCache
 
-    @Spy private var userManagerState: UserManagerState? = UserManagerState()
+    @Mock private lateinit var launcherBinder: BaseLauncherBinder
+    @Mock private lateinit var transaction: LoaderTransaction
+    @Mock private lateinit var idleLock: LooperIdleLock
+    @Mock private lateinit var iconCacheUpdateHandler: IconCacheUpdateHandler
+
+    @Spy private var userManagerState: UserManagerState = UserManagerState()
 
     @get:Rule val setFlagsRule = SetFlagsRule()
 
-    private val app: LauncherAppState
-        get() = context.appComponent.launcherAppState
+    private val testComponent: TestComponent
+        get() = context.appComponent as TestComponent
+
+    private val bgDataModel: BgDataModel
+        get() = testComponent.getDataModel()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        setFlagsRule.enableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER)
-        launcherModel = mock(LauncherModel::class.java)
         mockitoSession =
             ExtendedMockito.mockitoSession()
                 .strictness(Strictness.LENIENT)
@@ -125,14 +125,17 @@
         `when`(launcherModel.beginLoader(any())).thenReturn(transaction)
         `when`(launcherModel.modelDbController)
             .thenReturn(FactitiousDbController(context, INSERTION_STATEMENT_FILE))
+        `when`(launcherModel.modelDelegate).thenReturn(modelDelegate)
         `when`(launcherBinder.newIdleLock(any())).thenReturn(idleLock)
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
         `when`(iconCache.getUpdateHandler()).thenReturn(iconCacheUpdateHandler)
+
         context.initDaggerComponent(
             DaggerLoaderTaskTest_TestComponent.builder()
                 .bindUserCache(userCache)
                 .bindIconCache(iconCache)
                 .bindLauncherModel(launcherModel)
+                .bindAllAppsList(bgAllAppsList)
         )
         context.appComponent.idp.apply {
             numRows = 5
@@ -152,14 +155,16 @@
 
     @Test
     fun loadsDataProperly() =
-        with(BgDataModel()) {
+        with(bgDataModel) {
             val MAIN_HANDLE = Process.myUserHandle()
             val mockUserHandles = arrayListOf<UserHandle>(MAIN_HANDLE)
             `when`(userCache.userProfiles).thenReturn(mockUserHandles)
             `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 1))
-            LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder)
+            testComponent
+                .getLoaderTaskFactory()
+                .newLoaderTask(launcherBinder, userManagerState)
                 .runSyncOnBackgroundThread()
-            Truth.assertThat(
+            assertThat(
                     itemsIdMap
                         .filter {
                             it.container == CONTAINER_DESKTOP || it.container == CONTAINER_HOTSEAT
@@ -167,9 +172,8 @@
                         .size
                 )
                 .isAtLeast(32)
-            Truth.assertThat(itemsIdMap.filter { ModelUtils.WIDGET_FILTER.test(it) }.size)
-                .isAtLeast(7)
-            Truth.assertThat(
+            assertThat(itemsIdMap.filter { ModelUtils.WIDGET_FILTER.test(it) }.size).isAtLeast(7)
+            assertThat(
                     itemsIdMap
                         .filter {
                             it.itemType == ITEM_TYPE_FOLDER || it.itemType == ITEM_TYPE_APP_PAIR
@@ -177,12 +181,14 @@
                         .size
                 )
                 .isAtLeast(8)
-            Truth.assertThat(itemsIdMap.size()).isAtLeast(40)
+            assertThat(itemsIdMap.size()).isAtLeast(40)
         }
 
     @Test
     fun bindsLoadedDataCorrectly() {
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        testComponent
+            .getLoaderTaskFactory()
+            .newLoaderTask(launcherBinder, userManagerState)
             .runSyncOnBackgroundThread()
 
         verify(launcherBinder).bindWorkspace(true, false)
@@ -200,7 +206,7 @@
 
     @Test
     fun setsQuietModeFlagCorrectlyForWorkProfile() =
-        with(BgDataModel()) {
+        with(bgDataModel) {
             setFlagsRule.enableFlags(Flags.FLAG_ENABLE_PRIVATE_SPACE)
             val MAIN_HANDLE = Process.myUserHandle()
             val mockUserHandles = arrayListOf<UserHandle>(MAIN_HANDLE)
@@ -208,7 +214,9 @@
             `when`(userManagerState?.isUserQuiet(MAIN_HANDLE)).thenReturn(true)
             `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 1))
 
-            LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder, userManagerState)
+            testComponent
+                .getLoaderTaskFactory()
+                .newLoaderTask(launcherBinder, userManagerState)
                 .runSyncOnBackgroundThread()
 
             verify(bgAllAppsList)
@@ -221,7 +229,7 @@
 
     @Test
     fun setsQuietModeFlagCorrectlyForPrivateProfile() =
-        with(BgDataModel()) {
+        with(bgDataModel) {
             setFlagsRule.enableFlags(Flags.FLAG_ENABLE_PRIVATE_SPACE)
             val MAIN_HANDLE = Process.myUserHandle()
             val mockUserHandles = arrayListOf<UserHandle>(MAIN_HANDLE)
@@ -229,7 +237,9 @@
             `when`(userManagerState?.isUserQuiet(MAIN_HANDLE)).thenReturn(true)
             `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 3))
 
-            LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder, userManagerState)
+            testComponent
+                .getLoaderTaskFactory()
+                .newLoaderTask(launcherBinder, userManagerState)
                 .runSyncOnBackgroundThread()
 
             verify(bgAllAppsList)
@@ -268,7 +278,9 @@
         RestoreDbTask.setPending(spyContext)
 
         // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        testComponent
+            .getLoaderTaskFactory()
+            .newLoaderTask(launcherBinder, userManagerState)
             .runSyncOnBackgroundThread()
 
         // Then
@@ -336,7 +348,9 @@
         Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0)
 
         // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        testComponent
+            .getLoaderTaskFactory()
+            .newLoaderTask(launcherBinder, userManagerState)
             .runSyncOnBackgroundThread()
 
         // Then
@@ -375,7 +389,9 @@
         RestoreDbTask.setPending(spyContext)
 
         // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        testComponent
+            .getLoaderTaskFactory()
+            .newLoaderTask(launcherBinder, userManagerState)
             .runSyncOnBackgroundThread()
 
         // Then
@@ -414,7 +430,9 @@
         RestoreDbTask.setPending(spyContext)
 
         // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        testComponent
+            .getLoaderTaskFactory()
+            .newLoaderTask(launcherBinder, userManagerState)
             .runSyncOnBackgroundThread()
 
         // Then
@@ -444,7 +462,8 @@
             )
         val expectedAppInfo = AppInfo().apply { componentName = expectedComponent }
         // When
-        val loader = LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        val loader =
+            testComponent.getLoaderTaskFactory().newLoaderTask(launcherBinder, userManagerState)
         val actualIconRequest =
             loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
         // Then
@@ -474,7 +493,8 @@
             )
         val expectedAppInfo = AppInfo().apply { componentName = expectedComponent }
         // When
-        val loader = LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        val loader =
+            testComponent.getLoaderTaskFactory().newLoaderTask(launcherBinder, userManagerState)
         val actualIconRequest =
             loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
         // Then
@@ -505,7 +525,8 @@
         val expectedAppInfo =
             AppInfo().apply { componentName = ComponentName("differentPkg", "differentClass") }
         // When
-        val loader = LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        val loader =
+            testComponent.getLoaderTaskFactory().newLoaderTask(launcherBinder, userManagerState)
         val actualIconRequest =
             loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
         // Then
@@ -532,7 +553,8 @@
             )
         val expectedAppInfo = AppInfo()
         // When
-        val loader = LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        val loader =
+            testComponent.getLoaderTaskFactory().newLoaderTask(launcherBinder, userManagerState)
         val actualIconRequest =
             loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
         // Then
@@ -543,6 +565,11 @@
     @LauncherAppSingleton
     @Component(modules = [AllModulesForTest::class])
     interface TestComponent : LauncherAppComponent {
+
+        fun getLoaderTaskFactory(): LoaderTaskFactory
+
+        fun getDataModel(): BgDataModel
+
         @Component.Builder
         interface Builder : LauncherAppComponent.Builder {
             @BindsInstance fun bindUserCache(userCache: UserCache): Builder
@@ -551,6 +578,8 @@
 
             @BindsInstance fun bindIconCache(iconCache: IconCache): Builder
 
+            @BindsInstance fun bindAllAppsList(list: AllAppsList): Builder
+
             override fun build(): TestComponent
         }
     }
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
index 8db049c..fd44023 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
@@ -30,12 +30,13 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
-import com.android.launcher3.LauncherAppState
+import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
 import com.android.launcher3.icons.CacheableShortcutInfo
+import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
@@ -52,11 +53,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Answers
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
 import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
@@ -71,10 +73,10 @@
     @Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
     @Mock private lateinit var mockBgDataModel: BgDataModel
     @Mock private lateinit var mockContext: Context
-    @Mock private lateinit var mockAppState: LauncherAppState
+    @Mock private lateinit var mockIconCache: IconCache
     @Mock private lateinit var mockPmHelper: PackageManagerHelper
     @Mock private lateinit var mockLauncherApps: LauncherApps
-    @Mock private lateinit var mockCursor: LoaderCursor
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var mockCursor: LoaderCursor
     @Mock private lateinit var mockUserCache: UserCache
     @Mock private lateinit var mockUserManagerState: UserManagerState
     @Mock private lateinit var mockWidgetInflater: WidgetInflater
@@ -95,10 +97,9 @@
 
     @Before
     fun setup() {
+        MockitoAnnotations.initMocks(this)
+
         mUserHandle = UserHandle(0)
-        mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
-        mockWorkspaceInfo = mock<WorkspaceItemInfo>()
-        mockBgDataModel = mock<BgDataModel>()
         mComponentName = ComponentName("package", "class")
         mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
         intent =
@@ -107,52 +108,36 @@
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
-        mockContext =
-            mock<Context>().apply {
-                whenever(packageManager).thenReturn(mock())
-                whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
-                whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
-                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
-            }
-        mockAppState =
-            mock<LauncherAppState>().apply {
-                whenever(context).thenReturn(mockContext)
-                whenever(iconCache).thenReturn(mock())
-                whenever(iconCache.getShortcutIcon(any(), any(), any())).then {}
-            }
-        mockPmHelper =
-            mock<PackageManagerHelper>().apply {
-                whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
-                    .thenReturn(intent)
-            }
-        mockCursor =
-            Mockito.mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
-                user = mUserHandle
-                itemType = ITEM_TYPE_APPLICATION
-                id = 1
-                restoreFlag = 1
-                serialNumber = 101
-                whenever(parseIntent()).thenReturn(intent)
-                whenever(markRestored()).doAnswer { restoreFlag = 0 }
-                whenever(updater().put(Favorites.INTENT, intent.toUri(0)).commit()).thenReturn(1)
-                whenever(getAppShortcutInfo(any(), any(), any(), any()))
-                    .thenReturn(mockWorkspaceInfo)
-                whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
-            }
-        mockUserCache =
-            mock<UserCache>().apply {
-                val userIconInfo =
-                    mock<UserIconInfo>().apply { whenever(isPrivate).thenReturn(false) }
-                whenever(getUserInfo(any())).thenReturn(userIconInfo)
-            }
+        mockLauncherApps.apply {
+            whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+            whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
+        }
+        mockContext.apply {
+            whenever(packageManager).thenReturn(mock())
+            whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
+            whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
+            whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
+        }
 
-        mockUserManagerState = mock<UserManagerState>()
-        mockWidgetInflater = mock<WidgetInflater>()
+        whenever(mockIconCache.getShortcutIcon(any(), any(), any())).then {}
+        whenever(mockPmHelper.getAppLaunchIntent(mComponentName.packageName, mUserHandle))
+            .thenReturn(intent)
+        mockCursor.apply {
+            user = mUserHandle
+            itemType = ITEM_TYPE_APPLICATION
+            id = 1
+            restoreFlag = 1
+            serialNumber = 101
+            whenever(parseIntent()).thenReturn(intent)
+            whenever(markRestored()).doAnswer { restoreFlag = 0 }
+            whenever(updater().put(Favorites.INTENT, intent.toUri(0)).commit()).thenReturn(1)
+            whenever(getAppShortcutInfo(any(), any(), any(), any())).thenReturn(mockWorkspaceInfo)
+            whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
+        }
+
+        val mockUserInfo = mock<UserIconInfo>().apply { whenever(isPrivate).thenReturn(false) }
+        whenever(mockUserCache.getUserInfo(any())).thenReturn(mockUserInfo)
+
         mKeyToPinnedShortcutsMap = mutableMapOf()
         mInstallingPkgs = hashMapOf()
         mAllDeepShortcuts = mutableListOf()
@@ -283,7 +268,7 @@
         userManagerState: UserManagerState = mockUserManagerState,
         launcherApps: LauncherApps = mockLauncherApps,
         shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
-        app: LauncherAppState = mockAppState,
+        context: Context = mockContext,
         bgDataModel: BgDataModel = mockBgDataModel,
         widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> = mWidgetProvidersMap,
         widgetInflater: WidgetInflater = mockWidgetInflater,
@@ -297,11 +282,11 @@
     ) =
         WorkspaceItemProcessor(
             c = cursor,
+            context = context,
             memoryLogger = memoryLogger,
             userCache = userCache,
             userManagerState = userManagerState,
             launcherApps = launcherApps,
-            app = app,
             bgDataModel = bgDataModel,
             widgetProvidersMap = widgetProvidersMap,
             widgetInflater = widgetInflater,
@@ -313,5 +298,8 @@
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
             allDeepShortcuts = allDeepShortcuts,
+            idp = InvariantDeviceProfile.INSTANCE.get(context),
+            iconCache = mockIconCache,
+            isSafeMode = false,
         )
 }