Merge "[2/n] Hide desktop button in overview for incompatible tasks" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 06809d7..1726eca 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -564,3 +564,23 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "restore_archived_shortcuts"
+  namespace: "launcher"
+  description: "Makes sure pre-archived pinned shortcuts also get restored"
+  bug: "375414891"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
+  name: "restore_archived_app_icons_from_db"
+  namespace: "launcher"
+  description: "Restores pre-archived icons from db when available, mimicing promise icons"
+  bug: "391913214"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt b/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt
new file mode 100644
index 0000000..29586c4
--- /dev/null
+++ b/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dagger
+
+import dagger.Module
+
+/**
+ * Module containing bindings for the final derivative app, an implementation of this module should
+ * be included in the final app code.
+ */
+@Module abstract class AppModule {}
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 1f33e08..a530325 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -34,7 +34,6 @@
     <string name="launcher_restore_event_logger_class" translatable="false">com.android.quickstep.LauncherRestoreEventLoggerImpl</string>
     <string name="taskbar_edu_tooltip_controller_class" translatable="false">com.android.launcher3.taskbar.TaskbarEduTooltipController</string>
     <string name="nav_handle_long_press_handler_class" translatable="false"></string>
-    <string name="contextual_search_state_manager_class" translatable="false"></string>
 
     <!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
          determines how many thumbnails will be fetched in the background. -->
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 0474000..d2a7029 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -304,10 +304,6 @@
     <string name="taskbar_a11y_shown_with_bubbles_left_title">Taskbar &#38; bubbles left shown</string>
     <!-- Accessibility title for the Taskbar window appearing together with bubble bar on right. [CHAR_LIMIT=30] -->
     <string name="taskbar_a11y_shown_with_bubbles_right_title">Taskbar &#38; bubbles right shown</string>
-    <!-- Accessibility title for the Taskbar window being closed. [CHAR_LIMIT=30] -->
-    <string name="taskbar_a11y_hidden_title">Taskbar hidden</string>
-    <!-- Accessibility title for the Taskbar window being closed together with bubble bar. [CHAR_LIMIT=30] -->
-    <string name="taskbar_a11y_hidden_with_bubbles_title">Taskbar &#38; bubbles hidden</string>
     <!-- Accessibility title for the Taskbar window on phones. [CHAR_LIMIT=NONE] -->
     <string name="taskbar_phone_a11y_title">Navigation bar</string>
     <!-- Text in popup dialog for user to switch between always showing Taskbar or not. [CHAR LIMIT=30] -->
diff --git a/quickstep/src/com/android/launcher3/dagger/Modules.kt b/quickstep/src/com/android/launcher3/dagger/Modules.kt
index 04c1d5e..52be413 100644
--- a/quickstep/src/com/android/launcher3/dagger/Modules.kt
+++ b/quickstep/src/com/android/launcher3/dagger/Modules.kt
@@ -21,9 +21,11 @@
 import com.android.launcher3.util.ApiWrapper
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.quickstep.util.GestureExclusionManager
 import com.android.quickstep.util.SystemWindowManagerProxy
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 
 private object Modules {}
 
@@ -42,3 +44,11 @@
     @Binds
     abstract fun bindPluginManagerWrapper(impl: PluginManagerWrapperImpl): PluginManagerWrapper
 }
+
+@Module
+object StaticObjectModule {
+
+    @Provides
+    @JvmStatic
+    fun provideGestureExclusionManager(): GestureExclusionManager = GestureExclusionManager.INSTANCE
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
index 56945ba..70868c5 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo;
-import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction;
 import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation;
 
 import android.app.prediction.AppTarget;
@@ -27,9 +26,12 @@
 
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.PredictionHelper;
 import com.android.launcher3.model.data.ItemInfo;
 
 import java.util.ArrayList;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * Model helper for app predictions in workspace
@@ -43,13 +45,18 @@
      */
     public static Bundle convertDataModelToAppTargetBundle(Context context, BgDataModel dataModel) {
         Bundle bundle = new Bundle();
-        ArrayList<AppTargetEvent> events = new ArrayList<>();
-        ArrayList<ItemInfo> workspaceItems = dataModel.getAllWorkspaceItems();
-        for (ItemInfo item : workspaceItems) {
-            AppTarget target = getAppTargetFromItemInfo(context, item);
-            if (target != null && !isTrackedForHotseatPrediction(item)) continue;
-            events.add(wrapAppTargetWithItemLocation(target, AppTargetEvent.ACTION_PIN, item));
-        }
+        ArrayList<AppTargetEvent> events = dataModel.itemsIdMap
+                .stream()
+                .filter(PredictionHelper::isTrackedForHotseatPrediction)
+                .map(item -> {
+                    AppTarget target = getAppTargetFromItemInfo(context, item);
+                    return target != null
+                            ? wrapAppTargetWithItemLocation(target, AppTargetEvent.ACTION_PIN, item)
+                            : null;
+                })
+                .filter(Objects::nonNull)
+                .collect(Collectors.toCollection(ArrayList::new));
+
         ArrayList<AppTarget> currentTargets = new ArrayList<>();
         FixedContainerItems hotseatItems = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
         if (hotseatItems != null) {
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 25e1813..40e8fc2 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -462,7 +462,7 @@
     private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) {
         Bundle bundle = new Bundle();
         ArrayList<AppTargetEvent> widgetEvents =
-                dataModel.getAllWorkspaceItems().stream()
+                dataModel.itemsIdMap.stream()
                         .filter(PredictionHelper::isTrackedForWidgetPrediction)
                         .map(item -> {
                             AppTarget target = getAppTargetFromItemInfo(context, item);
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 9d9054e..40e1c10 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
+import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
 
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toMap;
@@ -69,9 +70,11 @@
             @NonNull AllAppsList apps) {
         Predicate<WidgetItem> predictedWidgetsFilter = enableTieredWidgetsByDefaultInPicker()
                 ? dataModel.widgetsModel.getPredictedWidgetsFilter() : null;
-        Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
-                widget -> new ComponentKey(widget.providerName, widget.user)).collect(
-                Collectors.toSet());
+        Set<ComponentKey> widgetsInWorkspace = dataModel.itemsIdMap
+                .stream()
+                .filter(WIDGET_FILTER)
+                .map(item -> new ComponentKey(item.getTargetComponent(), item.user))
+                .collect(Collectors.toSet());
 
         // Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
         // being in predictions.
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index b928de0..3736e6d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -33,6 +33,7 @@
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.SingleTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -275,7 +276,7 @@
 
         if (desktopTask != null) {
             mTasks = desktopTask.getTasks().stream()
-                    .map(GroupTask::new)
+                    .map(SingleTask::new)
                     .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
                     .collect(Collectors.toList());
             // All other tasks, apart from the grouped desktop task, are hidden
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 1144ac5..289e720 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -42,7 +42,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
@@ -129,7 +129,9 @@
 
     private final Rect mTempRect = new Rect();
 
-    private static final int FLAG_SWITCHER_SHOWING = 1 << 0;
+    /** Whether the IME Switcher button is visible. */
+    private static final int FLAG_IME_SWITCHER_BUTTON_VISIBLE = 1 << 0;
+    /** Whether the IME is visible. */
     private static final int FLAG_IME_VISIBLE = 1 << 1;
     private static final int FLAG_ROTATION_BUTTON_VISIBLE = 1 << 2;
     private static final int FLAG_A11Y_VISIBLE = 1 << 3;
@@ -301,7 +303,7 @@
                     isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer,
                     mControllers.navButtonController, R.id.ime_switcher);
             mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton,
-                    flags -> ((flags & FLAG_SWITCHER_SHOWING) != 0)
+                    flags -> ((flags & FLAG_IME_SWITCHER_BUTTON_VISIBLE) != 0)
                             && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)));
         }
 
@@ -507,8 +509,9 @@
 
     private void parseSystemUiFlags(@SystemUiStateFlags long sysUiStateFlags) {
         mSysuiStateFlags = sysUiStateFlags;
+        boolean isImeSwitcherButtonVisible =
+                (sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_BUTTON_SHOWING) != 0;
         boolean isImeVisible = (sysUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
-        boolean isImeSwitcherShowing = (sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0;
         boolean a11yVisible = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
         boolean isHomeDisabled = (sysUiStateFlags & SYSUI_STATE_HOME_DISABLED) != 0;
         boolean isRecentsDisabled = (sysUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0;
@@ -522,9 +525,8 @@
         boolean isKeyboardShortcutHelperShowing =
                 (sysUiStateFlags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0;
 
-        // TODO(b/202218289) we're getting IME as not visible on lockscreen from system
+        updateStateForFlag(FLAG_IME_SWITCHER_BUTTON_VISIBLE, isImeSwitcherButtonVisible);
         updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible);
-        updateStateForFlag(FLAG_SWITCHER_SHOWING, isImeSwitcherShowing);
         updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible);
         updateStateForFlag(FLAG_DISABLE_HOME, isHomeDisabled);
         updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled);
@@ -1226,7 +1228,8 @@
 
     private static String getStateString(int flags) {
         StringJoiner str = new StringJoiner("|");
-        appendFlag(str, flags, FLAG_SWITCHER_SHOWING, "FLAG_SWITCHER_SHOWING");
+        appendFlag(str, flags, FLAG_IME_SWITCHER_BUTTON_VISIBLE,
+                "FLAG_IME_SWITCHER_BUTTON_VISIBLE");
         appendFlag(str, flags, FLAG_IME_VISIBLE, "FLAG_IME_VISIBLE");
         appendFlag(str, flags, FLAG_ROTATION_BUTTON_VISIBLE, "FLAG_ROTATION_BUTTON_VISIBLE");
         appendFlag(str, flags, FLAG_A11Y_VISIBLE, "FLAG_A11Y_VISIBLE");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 978ffdb..e8a0c45 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -86,7 +86,6 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.BubbleTextView.RunningAppState;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
@@ -1330,7 +1329,7 @@
                 mControllers.uiController.onTaskbarIconLaunched(api);
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
-        } else if (tag instanceof TaskItemInfo info && !Flags.enableMultiInstanceMenuTaskbar()) {
+        } else if (tag instanceof TaskItemInfo info) {
             RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId())
                     ? createDesktopAppLaunchRemoteTransition(
                             AppLaunchType.UNMINIMIZE, Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
@@ -1341,7 +1340,8 @@
                 taskView = recents.getTaskViewByTaskId(info.getTaskId());
             }
 
-            if (areDesktopTasksVisible() && taskView != null) {
+            if (areDesktopTasksVisible() && taskView != null
+                    && mControllers.uiController.isInOverviewUi()) {
                 RunnableList runnableList = taskView.launchWithAnimation();
                 if (runnableList != null) {
                     runnableList.add(() ->
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 21c8255..b46b0dc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -89,6 +89,8 @@
 public class TaskbarManager {
     private static final String TAG = "TaskbarManager";
     private static final boolean DEBUG = false;
+    // TODO(b/382378283) remove all logs with this tag
+    public static final String NULL_TASKBAR_ROOT_LAYOUT_TAG = "b/382378283";
 
     /**
      * All the configurations which do not initiate taskbar recreation.
@@ -151,6 +153,20 @@
     private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
         @Override
         public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
+
+            if ((flags & CHANGE_DENSITY) != 0) {
+                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Display density changed");
+            }
+            if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
+                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Navigation mode changed");
+            }
+            if ((flags & CHANGE_DESKTOP_MODE) != 0) {
+                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Desktop mode changed");
+            }
+            if ((flags & CHANGE_TASKBAR_PINNING) != 0) {
+                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Taskbar pinning changed");
+            }
+
             if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE
                     | CHANGE_TASKBAR_PINNING)) != 0) {
                 recreateTaskbar();
@@ -345,13 +361,13 @@
 
     private void destroyTaskbarForDisplay(int displayId) {
         TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
-        debugWhyTaskbarNotDestroyed(
-                "destroyTaskbarForDisplay: " + taskbar + " displayId=" + displayId);
+        debugWhyTaskbarNotDestroyed("destroyTaskbarForDisplay: " + taskbar, displayId);
         if (taskbar != null) {
             taskbar.onDestroy();
             // remove all defaults that we store
             removeTaskbarFromMap(displayId);
         }
+        // make this display-specific
         DeviceProfile dp = mUserUnlocked ?
                 LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
         if (dp == null || !isTaskbarEnabled(dp)) {
@@ -506,6 +522,7 @@
     private void recreateTaskbarForDisplay(int displayId) {
         Trace.beginSection("recreateTaskbar");
         try {
+            // TODO: make this code display specific
             DeviceProfile dp = mUserUnlocked ?
                     LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
 
@@ -547,9 +564,14 @@
             if (enableTaskbarNoRecreate()) {
                 addTaskbarRootViewToWindow(displayId);
                 FrameLayout taskbarRootLayout = getTaskbarRootLayoutForDisplay(displayId);
-                taskbarRootLayout.removeAllViews();
-                taskbarRootLayout.addView(taskbar.getDragLayer());
-                taskbar.notifyUpdateLayoutParams();
+                if (taskbarRootLayout != null) {
+                    taskbarRootLayout.removeAllViews();
+                    taskbarRootLayout.addView(taskbar.getDragLayer());
+                    taskbar.notifyUpdateLayoutParams();
+                } else {
+                    Log.e(NULL_TASKBAR_ROOT_LAYOUT_TAG,
+                            "taskbarRootLayout is null for displayId=" + displayId);
+                }
             }
         } finally {
             Trace.endSection();
@@ -744,17 +766,25 @@
 
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarManager:");
-        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
-        if (taskbar == null) {
-            pw.println(prefix + "\tTaskbarActivityContext: null");
-        } else {
-            taskbar.dumpLogs(prefix + "\t", pw);
+        // iterate through taskbars and do the dump for each
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            TaskbarActivityContext taskbar = mTaskbars.get(i);
+            pw.println(prefix + "\tTaskbar at display " + displayId + ":");
+            if (taskbar == null) {
+                pw.println(prefix + "\t\tTaskbarActivityContext: null");
+            } else {
+                taskbar.dumpLogs(prefix + "\t\t", pw);
+            }
         }
+
     }
 
     private void addTaskbarRootViewToWindow(int displayId) {
         TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
         if (!enableTaskbarNoRecreate() || taskbar == null) {
+            Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
+                    "addTaskbarRootViewToWindow - taskbar null | displayId=" + displayId);
             return;
         }
 
@@ -762,6 +792,10 @@
             mWindowManager.addView(getTaskbarRootLayoutForDisplay(displayId),
                     taskbar.getWindowLayoutParams());
             mAddedRootLayouts.put(displayId, true);
+        } else {
+            Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
+                    "addTaskbarRootViewToWindow - root layout already added | displayId="
+                            + displayId);
         }
     }
 
@@ -854,6 +888,7 @@
             }
         };
         addTaskbarRootLayoutToMap(displayId, newTaskbarRootLayout);
+        Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "created new root layout - displayId=" + displayId);
     }
 
     private boolean isDefaultDisplay(int displayId) {
@@ -867,7 +902,14 @@
      * @return The taskbar root layout {@link FrameLayout} for a given display or {@code null}.
      */
     private FrameLayout getTaskbarRootLayoutForDisplay(int displayId) {
-        return mRootLayouts.get(displayId);
+        FrameLayout frameLayout = mRootLayouts.get(displayId);
+        if (frameLayout != null) {
+            return frameLayout;
+        } else {
+            Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
+                    "getTaskbarRootLayoutForDisplay == null | displayId=" + displayId);
+            return null;
+        }
     }
 
     /**
@@ -880,6 +922,8 @@
         if (!mRootLayouts.contains(displayId) && rootLayout != null) {
             mRootLayouts.put(displayId, rootLayout);
         }
+
+        Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "mRootLayouts.size()=" + mRootLayouts.size());
     }
 
     /**
@@ -892,6 +936,8 @@
             mAddedRootLayouts.delete(displayId);
             mRootLayouts.delete(displayId);
         }
+
+        Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "mRootLayouts.size()=" + mRootLayouts.size());
     }
 
     private int getDefaultDisplayId() {
@@ -900,11 +946,17 @@
 
     /** Temp logs for b/254119092. */
     public void debugWhyTaskbarNotDestroyed(String debugReason) {
+        debugWhyTaskbarNotDestroyed(debugReason, getDefaultDisplayId());
+    }
+
+    /** Temp logs for b/254119092. */
+    public void debugWhyTaskbarNotDestroyed(String debugReason, int displayId) {
         StringJoiner log = new StringJoiner("\n");
-        log.add(debugReason);
+        log.add(debugReason  + " displayId=" + displayId);
 
         boolean activityTaskbarPresent = mActivity != null
                 && mActivity.getDeviceProfile().isTaskbarPresent;
+        // TODO: make this display specific
         boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(mWindowContext)
                 .getDeviceProfile(mWindowContext).isTaskbarPresent;
         if (activityTaskbarPresent == contextTaskbarPresent) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 5eb92d8..6047999 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -30,6 +30,7 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.SingleTask
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import java.io.PrintWriter
 
@@ -218,7 +219,7 @@
         // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
         val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
         val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
-        return sortedTasks.map { GroupTask(it) }
+        return sortedTasks.map { SingleTask(it) }
     }
 
     private fun reloadRecentTasksIfNeeded() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 025ad87..68114f1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -35,7 +35,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
@@ -259,8 +259,10 @@
 
     private @Nullable AnimatorSet mAnimator;
     private boolean mIsSystemGestureInProgress;
+    /** Whether the IME is visible. */
     private boolean mIsImeShowing;
-    private boolean mIsImeSwitcherShowing;
+    /** Whether the IME Switcher button is visible. */
+    private boolean mIsImeSwitcherButtonShowing;
 
     private final Alarm mTimeoutAlarm = new Alarm();
     private boolean mEnableBlockingTimeoutDuringTests = false;
@@ -1147,7 +1149,8 @@
                 SystemUiFlagUtils.isLocked(systemUiStateFlags));
 
         mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING);
-        mIsImeSwitcherShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SWITCHER_SHOWING);
+        mIsImeSwitcherButtonShowing =
+                hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SWITCHER_BUTTON_SHOWING);
         if (updateStateForFlag(FLAG_STASHED_IME, shouldStashForIme())) {
             animDuration = TASKBAR_STASH_DURATION_FOR_IME;
             startDelay = getTaskbarStashStartDelayForIme();
@@ -1199,7 +1202,7 @@
             return false;
         }
 
-        return mIsImeShowing || mIsImeSwitcherShowing;
+        return mIsImeShowing || mIsImeSwitcherButtonShowing;
     }
 
     /**
@@ -1375,7 +1378,7 @@
         pw.println(prefix + "\tmState=" + getStateString(mState));
         pw.println(prefix + "\tmIsSystemGestureInProgress=" + mIsSystemGestureInProgress);
         pw.println(prefix + "\tmIsImeShowing=" + mIsImeShowing);
-        pw.println(prefix + "\tmIsImeSwitcherShowing=" + mIsImeSwitcherShowing);
+        pw.println(prefix + "\tmIsImeSwitcherButtonShowing=" + mIsImeSwitcherButtonShowing);
     }
 
     private static String getStateString(long flags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index e4e97e5..457ba3d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -33,7 +33,6 @@
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.os.Bundle;
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.view.DisplayCutout;
@@ -41,7 +40,6 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
 import androidx.annotation.LayoutRes;
@@ -311,16 +309,6 @@
         mShouldTryStartAlign = mActivityContext.shouldStartAlignTaskbar();
     }
 
-    @Override
-    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
-        if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
-            announceTaskbarShown();
-        } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
-            announceTaskbarHidden();
-        }
-        return super.performAccessibilityActionInternal(action, arguments);
-    }
-
     private void announceTaskbarShown() {
         BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
         if (bubbleBarLocation == null) {
@@ -334,21 +322,12 @@
         }
     }
 
-    private void announceTaskbarHidden() {
-        BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible();
-        if (bubbleBarLocation == null) {
-            announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title));
-        } else {
-            announceForAccessibility(
-                    mContext.getString(R.string.taskbar_a11y_hidden_with_bubbles_title));
-        }
-    }
-
     protected void announceAccessibilityChanges() {
-        this.performAccessibilityAction(
-                isVisibleToUser() ? AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
-                        : AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
-
+        // Only announce taskbar window shown. Window disappearing is generally not announce.
+        // This also aligns with talkback guidelines and unnecessary announcement to users.
+        if (isVisibleToUser()) {
+            announceTaskbarShown();
+        }
         ActivityContext.lookupContext(getContext()).getDragLayer()
                 .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 7d39bf8..aff879e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -20,7 +20,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
@@ -34,6 +34,7 @@
 import android.os.SystemProperties;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.widget.Toast;
 
 import com.android.launcher3.taskbar.TaskbarSharedState;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
@@ -94,7 +95,7 @@
             | SYSUI_STATE_IME_SHOWING
             | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
             | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
-            | SYSUI_STATE_IME_SWITCHER_SHOWING;
+            | SYSUI_STATE_IME_SWITCHER_BUTTON_SHOWING;
 
     private static final long MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
@@ -589,6 +590,18 @@
                 });
     }
 
+    @Override
+    public void onDragItemOverBubbleBarDragZone(BubbleBarLocation location) {
+        //TODO(b/388894910): add meaningful implementation
+        MAIN_EXECUTOR.execute(() ->
+                Toast.makeText(mContext, "onDragItemOver " + location, Toast.LENGTH_SHORT).show());
+    }
+
+    @Override
+    public void onItemDraggedOutsideBubbleBarDropZone() {
+
+    }
+
     /** Notifies WMShell to show the expanded view. */
     void showExpandedView() {
         mSystemUiProxy.showExpandedView();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 58ebc50..f672840 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -23,6 +23,7 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion;
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
@@ -149,7 +150,9 @@
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewRecentsTouchContext;
 import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControllerDeprecated;
 import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController;
 import com.android.launcher3.util.ActivityOptionsWrapper;
@@ -265,6 +268,25 @@
 
     private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChanged;
 
+    private final TaskViewRecentsTouchContext mTaskViewRecentsTouchContext =
+            new TaskViewRecentsTouchContext() {
+                @Override
+                public boolean isRecentsInteractive() {
+                    return isInState(OVERVIEW) || isInState(OVERVIEW_MODAL_TASK);
+                }
+
+                @Override
+                public boolean isRecentsModal() {
+                    return isInState(OVERVIEW_MODAL_TASK);
+                }
+
+                @Override
+                public void onUserControlledAnimationCreated(
+                        AnimatorPlaybackController animController) {
+                    getStateManager().setCurrentUserControlledAnimation(animController);
+                }
+            };
+
     public static QuickstepLauncher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -665,7 +687,11 @@
             list.add(new StatusBarTouchController(this));
         }
 
-        list.add(new LauncherTaskViewController(this));
+        if (enableExpressiveDismissTaskMotion()) {
+            list.add(new TaskViewTouchController<>(this, mTaskViewRecentsTouchContext));
+        } else {
+            list.add(new TaskViewTouchControllerDeprecated<>(this, mTaskViewRecentsTouchContext));
+        }
         return list.toArray(new TouchController[list.size()]);
     }
 
@@ -1442,29 +1468,6 @@
         mBubbleBarLocation = bubbleBarLocation;
     }
 
-    private static final class LauncherTaskViewController extends
-            TaskViewTouchController<QuickstepLauncher> {
-
-        LauncherTaskViewController(QuickstepLauncher activity) {
-            super(activity);
-        }
-
-        @Override
-        protected boolean isRecentsInteractive() {
-            return mContainer.isInState(OVERVIEW) || mContainer.isInState(OVERVIEW_MODAL_TASK);
-        }
-
-        @Override
-        protected boolean isRecentsModal() {
-            return mContainer.isInState(OVERVIEW_MODAL_TASK);
-        }
-
-        @Override
-        protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
-            mContainer.getStateManager().setCurrentUserControlledAnimation(animController);
-        }
-    }
-
     @Override
     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
         super.dump(prefix, fd, writer, args);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewRecentsTouchContext.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewRecentsTouchContext.java
new file mode 100644
index 0000000..e8d31c1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewRecentsTouchContext.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import com.android.launcher3.anim.AnimatorPlaybackController;
+
+/** Interface providing context about the RecentsView state to a {@link TaskViewTouchController}. */
+public interface TaskViewRecentsTouchContext {
+    /** Returns whether Recents is interactive for touch. */
+    boolean isRecentsInteractive();
+
+    /** Returns if Recents is showing a single task in a modal way. */
+    boolean isRecentsModal();
+
+    /** Runs when a user controlled animation is created. */
+    default void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt
new file mode 100644
index 0000000..c996f34
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.touchcontrollers
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Context
+import android.graphics.Rect
+import android.os.VibrationEffect
+import android.view.MotionEvent
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS
+import com.android.launcher3.LauncherAnimUtils.blockedFlingDurationFactor
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatorPlaybackController
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.touch.BaseSwipeDetector
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.FlingBlockCheck
+import com.android.launcher3.util.TouchController
+import com.android.launcher3.util.VibratorWrapper
+import com.android.quickstep.util.VibrationConstants
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskView
+import kotlin.math.abs
+
+/** Touch controller for handling task view card swipes */
+class TaskViewTouchController<CONTAINER>(
+    private val container: CONTAINER,
+    private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext,
+) : AnimatorListenerAdapter(), TouchController, SingleAxisSwipeDetector.Listener where
+CONTAINER : Context,
+CONTAINER : RecentsViewContainer {
+    private val recentsView: RecentsView<*, *> = container.getOverviewPanel()
+    private val detector: SingleAxisSwipeDetector =
+        SingleAxisSwipeDetector(
+            container as Context,
+            this,
+            recentsView.pagedOrientationHandler.upDownSwipeDirection,
+        )
+    private val tempRect = Rect()
+    private val isRtl = Utilities.isRtl(container.resources)
+    private val flingBlockCheck = FlingBlockCheck()
+
+    private var currentAnimation: AnimatorPlaybackController? = null
+    private var currentAnimationIsGoingUp = false
+    private var allowGoingUp = false
+    private var allowGoingDown = false
+    private var noIntercept = false
+    private var displacementShift = 0f
+    private var progressMultiplier = 0f
+    private var endDisplacement = 0f
+    private var draggingEnabled = true
+    private var overrideVelocity: Float? = null
+    private var taskBeingDragged: TaskView? = null
+    private var isDismissHapticRunning = false
+
+    private fun canInterceptTouch(ev: MotionEvent): Boolean {
+        val currentAnimation = currentAnimation
+        return when {
+            (ev.edgeFlags and Utilities.EDGE_NAV_BAR) != 0 -> {
+                // Don't intercept swipes on the nav bar, as user might be trying to go home
+                // during a task dismiss animation.
+                currentAnimation?.animationPlayer?.end()
+                false
+            }
+            currentAnimation != null -> {
+                currentAnimation.forceFinishIfCloseToEnd()
+                true
+            }
+            AbstractFloatingView.getTopOpenViewWithType(
+                container,
+                AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT,
+            ) != null -> false
+            else -> taskViewRecentsTouchContext.isRecentsInteractive
+        }
+    }
+
+    override fun onAnimationCancel(animation: Animator) {
+        if (animation === currentAnimation?.target) {
+            clearState()
+        }
+    }
+
+    override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
+        if (
+            (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) &&
+                currentAnimation == null
+        ) {
+            clearState()
+        }
+        if (ev.action == MotionEvent.ACTION_DOWN) {
+            // Disable swiping up and down if the task overlay is modal.
+            if (taskViewRecentsTouchContext.isRecentsModal) {
+                noIntercept = true
+                return false
+            }
+            noIntercept = !canInterceptTouch(ev)
+            if (noIntercept) {
+                return false
+            }
+            // Now figure out which direction scroll events the controller will start
+            // calling the callbacks.
+            var directionsToDetectScroll = 0
+            var ignoreSlopWhenSettling = false
+            if (currentAnimation != null) {
+                directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH
+                ignoreSlopWhenSettling = true
+            } else {
+                taskBeingDragged = null
+                recentsView.taskViews.forEach { taskView ->
+                    if (
+                        recentsView.isTaskViewVisible(taskView) &&
+                            container.dragLayer.isEventOverView(taskView, ev)
+                    ) {
+                        taskBeingDragged = taskView
+                        val upDirection = recentsView.pagedOrientationHandler.getUpDirection(isRtl)
+
+                        // The task can be dragged up to dismiss it
+                        allowGoingUp = true
+
+                        // The task can be dragged down to open it if:
+                        // - It's the current page
+                        // - We support gestures to enter overview
+                        // - It's the focused task if in grid view
+                        // - The task is snapped
+                        allowGoingDown =
+                            taskView === recentsView.currentPageTaskView &&
+                                DisplayController.getNavigationMode(container).hasGestures &&
+                                (!recentsView.showAsGrid() || taskView.isLargeTile) &&
+                                recentsView.isTaskInExpectedScrollPosition(taskView)
+
+                        directionsToDetectScroll =
+                            if (allowGoingDown) SingleAxisSwipeDetector.DIRECTION_BOTH
+                            else upDirection
+                        return@forEach
+                    }
+                }
+                if (taskBeingDragged == null) {
+                    noIntercept = true
+                    return false
+                }
+            }
+            detector.setDetectableScrollConditions(directionsToDetectScroll, ignoreSlopWhenSettling)
+        }
+        if (noIntercept) {
+            return false
+        }
+        onControllerTouchEvent(ev)
+        return detector.isDraggingOrSettling
+    }
+
+    override fun onControllerTouchEvent(ev: MotionEvent): Boolean = detector.onTouchEvent(ev)
+
+    private fun reInitAnimationController(goingUp: Boolean) {
+        if (currentAnimation != null && currentAnimationIsGoingUp == goingUp) {
+            // No need to init
+            return
+        }
+        if ((goingUp && !allowGoingUp) || (!goingUp && !allowGoingDown)) {
+            // Trying to re-init in an unsupported direction.
+            return
+        }
+        val taskBeingDragged = taskBeingDragged ?: return
+        currentAnimation?.setPlayFraction(0f)
+        currentAnimation?.target?.removeListener(this)
+        currentAnimation?.dispatchOnCancel()
+
+        val orientationHandler = recentsView.pagedOrientationHandler
+        currentAnimationIsGoingUp = goingUp
+        val dl = container.dragLayer
+        val secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl)
+        val maxDuration = 2L * secondaryLayerDimension
+        val verticalFactor = orientationHandler.getTaskDragDisplacementFactor(isRtl)
+        val secondaryTaskDimension = orientationHandler.getSecondaryDimension(taskBeingDragged)
+        // The interpolator controlling the most prominent visual movement. We use this to determine
+        // whether we passed SUCCESS_TRANSITION_PROGRESS.
+        val currentInterpolator: Interpolator
+        val pa: PendingAnimation
+        if (goingUp) {
+            currentInterpolator = Interpolators.LINEAR
+            pa = PendingAnimation(maxDuration)
+            recentsView.createTaskDismissAnimation(
+                pa,
+                taskBeingDragged,
+                true, /* animateTaskView */
+                true, /* removeTask */
+                maxDuration,
+                false, /* dismissingForSplitSelection*/
+            )
+
+            endDisplacement = -secondaryTaskDimension.toFloat()
+        } else {
+            currentInterpolator = Interpolators.ZOOM_IN
+            pa =
+                recentsView.createTaskLaunchAnimation(
+                    taskBeingDragged,
+                    maxDuration,
+                    currentInterpolator,
+                )
+
+            // Since the thumbnail is what is filling the screen, based the end displacement on it.
+            taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true)
+            endDisplacement = (secondaryLayerDimension - tempRect.bottom).toFloat()
+        }
+        endDisplacement *= verticalFactor.toFloat()
+        currentAnimation =
+            pa.createPlaybackController().apply {
+                // Setting this interpolator doesn't affect the visual motion, but is used to
+                // determine whether we successfully reached the target state in onDragEnd().
+                target.interpolator = currentInterpolator
+                taskViewRecentsTouchContext.onUserControlledAnimationCreated(this)
+                target.addListener(this@TaskViewTouchController)
+                dispatchOnStart()
+            }
+        progressMultiplier = 1 / endDisplacement
+    }
+
+    override fun onDragStart(start: Boolean, startDisplacement: Float) {
+        if (!draggingEnabled) return
+        val currentAnimation = currentAnimation
+
+        val orientationHandler = recentsView.pagedOrientationHandler
+        if (currentAnimation == null) {
+            reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, isRtl))
+            displacementShift = 0f
+        } else {
+            displacementShift = currentAnimation.progressFraction / progressMultiplier
+            currentAnimation.pause()
+        }
+        flingBlockCheck.unblockFling()
+        overrideVelocity = null
+    }
+
+    override fun onDrag(displacement: Float): Boolean {
+        if (!draggingEnabled) return true
+        val taskBeingDragged = taskBeingDragged ?: return true
+        val currentAnimation = currentAnimation ?: return true
+
+        val orientationHandler = recentsView.pagedOrientationHandler
+        val totalDisplacement = displacement + displacementShift
+        val isGoingUp =
+            if (totalDisplacement == 0f) currentAnimationIsGoingUp
+            else orientationHandler.isGoingUp(totalDisplacement, isRtl)
+        if (isGoingUp != currentAnimationIsGoingUp) {
+            reInitAnimationController(isGoingUp)
+            flingBlockCheck.blockFling()
+        } else {
+            flingBlockCheck.onEvent()
+        }
+
+        if (isGoingUp) {
+            if (currentAnimation.progressFraction < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
+                // Halve the value when dismissing, as we are animating the drag across the full
+                // length for only the first half of the progress
+                currentAnimation.setPlayFraction(
+                    Utilities.boundToRange(totalDisplacement * progressMultiplier / 2, 0f, 1f)
+                )
+            } else {
+                // Set mOverrideVelocity to control task dismiss velocity in onDragEnd
+                var velocityDimenId = R.dimen.default_task_dismiss_drag_velocity
+                if (recentsView.showAsGrid()) {
+                    velocityDimenId =
+                        if (taskBeingDragged.isLargeTile) {
+                            R.dimen.default_task_dismiss_drag_velocity_grid_focus_task
+                        } else {
+                            R.dimen.default_task_dismiss_drag_velocity_grid
+                        }
+                }
+                overrideVelocity = -taskBeingDragged.resources.getDimension(velocityDimenId)
+
+                // Once halfway through task dismissal interpolation, switch from reversible
+                // dragging-task animation to playing the remaining task translation animations,
+                // while this is in progress disable dragging.
+                draggingEnabled = false
+            }
+        } else {
+            currentAnimation.setPlayFraction(
+                Utilities.boundToRange(totalDisplacement * progressMultiplier, 0f, 1f)
+            )
+        }
+
+        return true
+    }
+
+    override fun onDragEnd(velocity: Float) {
+        val taskBeingDragged = taskBeingDragged ?: return
+        val currentAnimation = currentAnimation ?: return
+
+        // Limit velocity, as very large scalar values make animations play too quickly
+        val maxTaskDismissDragVelocity =
+            taskBeingDragged.resources.getDimension(R.dimen.max_task_dismiss_drag_velocity)
+        val endVelocity =
+            Utilities.boundToRange(
+                overrideVelocity ?: velocity,
+                -maxTaskDismissDragVelocity,
+                maxTaskDismissDragVelocity,
+            )
+        overrideVelocity = null
+
+        var fling = draggingEnabled && detector.isFling(endVelocity)
+        val goingToEnd: Boolean
+        val blockedFling = fling && flingBlockCheck.isBlocked
+        if (blockedFling) {
+            fling = false
+        }
+        val orientationHandler = recentsView.pagedOrientationHandler
+        val goingUp = orientationHandler.isGoingUp(endVelocity, isRtl)
+        val progress = currentAnimation.progressFraction
+        val interpolatedProgress = currentAnimation.interpolatedProgress
+        goingToEnd =
+            if (fling) {
+                goingUp == currentAnimationIsGoingUp
+            } else {
+                interpolatedProgress > SUCCESS_TRANSITION_PROGRESS
+            }
+        var animationDuration =
+            BaseSwipeDetector.calculateDuration(
+                endVelocity,
+                if (goingToEnd) (1 - progress) else progress,
+            )
+        if (blockedFling && !goingToEnd) {
+            animationDuration *= blockedFlingDurationFactor(endVelocity).toLong()
+        }
+        // Due to very high or low velocity dismissals, animation durations can be inconsistently
+        // long or short. Bound the duration for animation of task translations for a more
+        // standardized feel.
+        animationDuration =
+            Utilities.boundToRange(
+                animationDuration,
+                MIN_TASK_DISMISS_ANIMATION_DURATION,
+                MAX_TASK_DISMISS_ANIMATION_DURATION,
+            )
+
+        currentAnimation.setEndAction { this.clearState() }
+        currentAnimation.startWithVelocity(
+            container,
+            goingToEnd,
+            abs(endVelocity.toDouble()).toFloat(),
+            endDisplacement,
+            animationDuration,
+        )
+        if (goingUp && goingToEnd && !isDismissHapticRunning) {
+            VibratorWrapper.INSTANCE.get(container)
+                .vibrate(
+                    TASK_DISMISS_VIBRATION_PRIMITIVE,
+                    TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE,
+                    TASK_DISMISS_VIBRATION_FALLBACK,
+                )
+            isDismissHapticRunning = true
+        }
+
+        draggingEnabled = true
+    }
+
+    private fun clearState() {
+        detector.finishedScrolling()
+        detector.setDetectableScrollConditions(0, false)
+        draggingEnabled = true
+        taskBeingDragged = null
+        currentAnimation = null
+        isDismissHapticRunning = false
+    }
+
+    companion object {
+        private const val ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f
+        private const val MIN_TASK_DISMISS_ANIMATION_DURATION: Long = 300
+        private const val MAX_TASK_DISMISS_ANIMATION_DURATION: Long = 600
+
+        private const val TASK_DISMISS_VIBRATION_PRIMITIVE: Int =
+            VibrationEffect.Composition.PRIMITIVE_TICK
+        private const val TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE: Float = 1f
+        private val TASK_DISMISS_VIBRATION_FALLBACK: VibrationEffect =
+            VibrationConstants.EFFECT_TEXTURE_TICK
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java
similarity index 94%
rename from quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java
index d622987..b1a36c7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java
@@ -49,10 +49,13 @@
 
 /**
  * Touch controller for handling task view card swipes
+ *
+ * @deprecated This class will be replaced by the new {@link TaskViewTouchController}.
  */
-public abstract class TaskViewTouchController<CONTAINER extends Context & RecentsViewContainer>
-        extends AnimatorListenerAdapter implements TouchController,
-        SingleAxisSwipeDetector.Listener {
+@Deprecated
+public class TaskViewTouchControllerDeprecated<
+        CONTAINER extends Context & RecentsViewContainer> extends AnimatorListenerAdapter
+        implements TouchController, SingleAxisSwipeDetector.Listener {
 
     private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
     private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
@@ -65,6 +68,7 @@
             VibrationConstants.EFFECT_TEXTURE_TICK;
 
     protected final CONTAINER mContainer;
+    private final TaskViewRecentsTouchContext mTaskViewRecentsTouchContext;
     private final SingleAxisSwipeDetector mDetector;
     private final RecentsView<?, ?> mRecentsView;
     private final Rect mTempRect = new Rect();
@@ -88,8 +92,10 @@
 
     private boolean mIsDismissHapticRunning = false;
 
-    public TaskViewTouchController(CONTAINER container) {
+    public TaskViewTouchControllerDeprecated(CONTAINER container,
+            TaskViewRecentsTouchContext taskViewRecentsTouchContext) {
         mContainer = container;
+        mTaskViewRecentsTouchContext = taskViewRecentsTouchContext;
         mRecentsView = container.getOverviewPanel();
         mIsRtl = Utilities.isRtl(container.getResources());
         SingleAxisSwipeDetector.Direction dir =
@@ -117,15 +123,7 @@
                 mContainer, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
             return false;
         }
-        return isRecentsInteractive();
-    }
-
-    protected abstract boolean isRecentsInteractive();
-
-    /** Is recents view showing a single task in a modal way. */
-    protected abstract boolean isRecentsModal();
-
-    protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+        return mTaskViewRecentsTouchContext.isRecentsInteractive();
     }
 
     @Override
@@ -161,7 +159,7 @@
                     if (mRecentsView.isTaskViewVisible(taskView) && mContainer.getDragLayer()
                             .isEventOverView(taskView, ev)) {
                         // Disable swiping up and down if the task overlay is modal.
-                        if (isRecentsModal()) {
+                        if (mTaskViewRecentsTouchContext.isRecentsModal()) {
                             mTaskBeingDragged = null;
                             break;
                         }
@@ -259,7 +257,7 @@
         // Setting this interpolator doesn't affect the visual motion, but is used to determine
         // whether we successfully reached the target state in onDragEnd().
         mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
-        onUserControlledAnimationCreated(mCurrentAnimation);
+        mTaskViewRecentsTouchContext.onUserControlledAnimationCreated(mCurrentAnimation);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
         mProgressMultiplier = 1 / mEndDisplacement;
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 66f307c..6ad9a2c 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -28,7 +28,9 @@
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj
+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
@@ -344,9 +346,12 @@
             return false
         }
 
-        val activity = containerInterface.getCreatedContainer()
-        if (activity != null) {
-            InteractionJankMonitorWrapper.begin(activity.rootView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
+        val recentsInWindowFlagSet =
+            enableFallbackOverviewInWindow() || enableLauncherOverviewInWindow()
+        if (!recentsInWindowFlagSet) {
+            containerInterface.getCreatedContainer()?.rootView?.let { view ->
+                InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
+            }
         }
 
         val gestureState =
@@ -373,6 +378,12 @@
                     transitionInfo: TransitionInfo,
                 ) {
                     Log.d(TAG, "recents animation started: $command")
+                    if (recentsInWindowFlagSet) {
+                        containerInterface.getCreatedContainer()?.rootView?.let { view ->
+                            InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
+                        }
+                    }
+
                     updateRecentsViewFocus(command)
                     logShowOverviewFrom(command.type)
                     containerInterface.runOnInitBackgroundStateUI {
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 379d71b4..01ced75 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -39,6 +39,8 @@
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
+import com.android.quickstep.util.SplitTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.recents.IRecentTasksListener;
@@ -50,6 +52,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -360,22 +363,19 @@
                 final Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
                 final Task task1 = Task.from(task1Key, taskInfo1,
                         tmpLockedUsers.get(task1Key.userId) /* isLocked */);
-                final Task task2;
-                final SplitConfigurationOptions.SplitBounds launcherSplitBounds;
 
                 if (rawTask.isBaseType(TYPE_SPLIT)) {
                     final TaskInfo taskInfo2 = rawTask.getBaseGroupedTask().getTaskInfo2();
                     final Task.TaskKey task2Key = new Task.TaskKey(taskInfo2);
-                    task2 = Task.from(task2Key, taskInfo2,
+                    final Task task2 = Task.from(task2Key, taskInfo2,
                             tmpLockedUsers.get(task2Key.userId) /* isLocked */);
-                    launcherSplitBounds =
+                    final SplitConfigurationOptions.SplitBounds launcherSplitBounds =
                             convertShellSplitBoundsToLauncher(
                                     rawTask.getBaseGroupedTask().getSplitBounds());
+                    allTasks.add(new SplitTask(task1, task2, launcherSplitBounds));
                 } else {
-                    task2 = null;
-                    launcherSplitBounds = null;
+                    allTasks.add(new SingleTask(task1));
                 }
-                allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
             } else {
                 TaskInfo taskInfo1 = rawTask.getTaskInfo1();
                 TaskInfo taskInfo2 = rawTask.getTaskInfo2();
@@ -407,9 +407,14 @@
                 if (taskInfo1.isVisible) {
                     numVisibleTasks++;
                 }
-                final SplitConfigurationOptions.SplitBounds launcherSplitBounds =
-                        convertShellSplitBoundsToLauncher(rawTask.getSplitBounds());
-                allTasks.add(new GroupTask(task1, task2, launcherSplitBounds));
+                if (task2 != null) {
+                    Objects.requireNonNull(rawTask.getSplitBounds());
+                    final SplitConfigurationOptions.SplitBounds launcherSplitBounds =
+                            convertShellSplitBoundsToLauncher(rawTask.getSplitBounds());
+                    allTasks.add(new SplitTask(task1, task2, launcherSplitBounds));
+                } else {
+                    allTasks.add(new SingleTask(task1));
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index d4305a5..0c89a80 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -64,12 +64,15 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.NavigationMode;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -85,13 +88,14 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
+
+import javax.inject.Inject;
 
 /**
  * Manages the state of the system during a swipe up gesture.
  */
-public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener,
-        SafeCloseable {
+@LauncherAppSingleton
+public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener {
 
     static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
 
@@ -99,8 +103,8 @@
     private static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 3f;
     private static final float QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL = 1.414f;
 
-    public static MainThreadInitializedObject<RecentsAnimationDeviceState> INSTANCE =
-            new MainThreadInitializedObject<>(RecentsAnimationDeviceState::new);
+    public static DaggerSingletonObject<RecentsAnimationDeviceState> INSTANCE =
+            new DaggerSingletonObject<>(LauncherAppComponent::getRecentsAnimationDeviceState);
 
     private final Context mContext;
     private final DisplayController mDisplayController;
@@ -110,12 +114,11 @@
 
     private final RotationTouchHelper mRotationTouchHelper;
     private final TaskStackChangeListener mPipListener;
+    private final DaggerSingletonTracker mLifeCycle;
     // Cache for better performance since it doesn't change at runtime.
     private final boolean mCanImeRenderGesturalNavButtons =
             InputMethodService.canImeRenderGesturalNavButtons();
 
-    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
-
     private @SystemUiStateFlags long mSystemUiStateFlags = QuickStepContract.SYSUI_STATE_AWAKE;
     private NavigationMode mMode = THREE_BUTTONS;
     private NavBarPosition mNavBarPosition;
@@ -134,35 +137,39 @@
     private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION;
     private boolean mExclusionListenerRegistered;
 
-    private RecentsAnimationDeviceState(Context context) {
-        this(context, GestureExclusionManager.INSTANCE);
-    }
-
     @VisibleForTesting
-    RecentsAnimationDeviceState(Context context, GestureExclusionManager exclusionManager) {
+    @Inject
+    RecentsAnimationDeviceState(
+            @ApplicationContext Context context,
+            GestureExclusionManager exclusionManager,
+            DisplayController displayController,
+            ContextualSearchStateManager contextualSearchStateManager,
+            RotationTouchHelper rotationTouchHelper,
+            SettingsCache settingsCache,
+            DaggerSingletonTracker lifeCycle) {
         mContext = context;
-        mDisplayController = DisplayController.INSTANCE.get(context);
+        mDisplayController = displayController;
         mExclusionManager = exclusionManager;
-        mContextualSearchStateManager = ContextualSearchStateManager.INSTANCE.get(context);
+        mContextualSearchStateManager = contextualSearchStateManager;
+        mRotationTouchHelper = rotationTouchHelper;
+        mLifeCycle = lifeCycle;
         mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
-        mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
 
         // Register for exclusion updates
-        runOnDestroy(this::unregisterExclusionListener);
+        mLifeCycle.addCloseable(this::unregisterExclusionListener);
 
         // Register for display changes changes
         mDisplayController.addChangeListener(this);
         onDisplayInfoChanged(context, mDisplayController.getInfo(), CHANGE_ALL);
-        runOnDestroy(() -> mDisplayController.removeChangeListener(this));
+        mLifeCycle.addCloseable(() -> mDisplayController.removeChangeListener(this));
 
-        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
         if (mIsOneHandedModeSupported) {
             Uri oneHandedUri = Settings.Secure.getUriFor(ONE_HANDED_ENABLED);
             SettingsCache.OnChangeListener onChangeListener =
                     enabled -> mIsOneHandedModeEnabled = enabled;
             settingsCache.register(oneHandedUri, onChangeListener);
             mIsOneHandedModeEnabled = settingsCache.getValue(oneHandedUri);
-            runOnDestroy(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
+            mLifeCycle.addCloseable(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
         } else {
             mIsOneHandedModeEnabled = false;
         }
@@ -173,14 +180,16 @@
                 enabled -> mIsSwipeToNotificationEnabled = enabled;
         settingsCache.register(swipeBottomNotificationUri, onChangeListener);
         mIsSwipeToNotificationEnabled = settingsCache.getValue(swipeBottomNotificationUri);
-        runOnDestroy(() -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener));
+        mLifeCycle.addCloseable(
+                () -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener));
 
         Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
         mIsUserSetupComplete = settingsCache.getValue(setupCompleteUri, 0);
         if (!mIsUserSetupComplete) {
             SettingsCache.OnChangeListener userSetupChangeListener = e -> mIsUserSetupComplete = e;
             settingsCache.register(setupCompleteUri, userSetupChangeListener);
-            runOnDestroy(() -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener));
+            mLifeCycle.addCloseable(
+                    () -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener));
         }
 
         try {
@@ -201,21 +210,10 @@
             }
         };
         TaskStackChangeListeners.getInstance().registerTaskStackListener(mPipListener);
-        runOnDestroy(() ->
+        mLifeCycle.addCloseable(() ->
                 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mPipListener));
     }
 
-    private void runOnDestroy(Runnable action) {
-        mOnDestroyActions.add(action);
-    }
-
-    @Override
-    public void close() {
-        for (Runnable r : mOnDestroyActions) {
-            r.run();
-        }
-    }
-
     /**
      * Adds a listener for the nav mode change, guaranteed to be called after the device state's
      * mode has changed.
@@ -228,7 +226,7 @@
         };
         mDisplayController.addChangeListener(listener);
         callback.run();
-        runOnDestroy(() -> mDisplayController.removeChangeListener(listener));
+        mLifeCycle.addCloseable(() -> mDisplayController.removeChangeListener(listener));
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 8edbacb..ef63b9b 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -142,7 +142,9 @@
         if (mSplitBounds == null) {
             SplitBounds shellSplitBounds = targets.extras.getParcelable(KEY_EXTRA_SPLIT_BOUNDS,
                     SplitBounds.class);
-            mSplitBounds = convertShellSplitBoundsToLauncher(shellSplitBounds);
+            if (shellSplitBounds != null) {
+                mSplitBounds = convertShellSplitBoundsToLauncher(shellSplitBounds);
+            }
         }
 
         boolean containsSplitTargets = mSplitBounds != null;
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index f54b655..a614327 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -33,13 +33,16 @@
 import android.view.MotionEvent;
 import android.view.OrientationEventListener;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.NavigationMode;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -48,16 +51,20 @@
 
 import java.io.PrintWriter;
 
+import javax.inject.Inject;
+
 /**
  * Helper class for transforming touch events
  */
-public class RotationTouchHelper implements DisplayInfoChangeListener, SafeCloseable {
+@LauncherAppSingleton
+public class RotationTouchHelper implements DisplayInfoChangeListener {
 
-    public static final MainThreadInitializedObject<RotationTouchHelper> INSTANCE =
-            new MainThreadInitializedObject<>(RotationTouchHelper::new);
+    public static final DaggerSingletonObject<RotationTouchHelper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherAppComponent::getRotationTouchHelper);
 
     private final OrientationTouchTransformer mOrientationTouchTransformer;
     private final DisplayController mDisplayController;
+    private final SystemUiProxy mSystemUiProxy;
     private final int mDisplayId;
     private int mDisplayRotation;
 
@@ -127,12 +134,17 @@
     private boolean mTaskListFrozen;
     private final Context mContext;
 
-    private RotationTouchHelper(Context context) {
+    @Inject
+    RotationTouchHelper(@ApplicationContext Context context,
+            DisplayController displayController,
+            SystemUiProxy systemUiProxy,
+            DaggerSingletonTracker lifeCycle) {
         mContext = context;
-        mDisplayController = DisplayController.INSTANCE.get(mContext);
-        Resources resources = mContext.getResources();
+        mDisplayController = displayController;
+        mSystemUiProxy = systemUiProxy;
         mDisplayId = DEFAULT_DISPLAY;
 
+        Resources resources = mContext.getResources();
         mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
                 () -> QuickStepContract.getWindowCornerRadius(mContext));
 
@@ -160,14 +172,13 @@
                 }
             }
         };
-    }
 
-    @Override
-    public void close() {
-        mDisplayController.removeChangeListener(this);
-        mOrientationListener.disable();
-        TaskStackChangeListeners.getInstance()
-                .unregisterTaskStackListener(mFrozenTaskListener);
+        lifeCycle.addCloseable(() -> {
+            mDisplayController.removeChangeListener(this);
+            mOrientationListener.disable();
+            TaskStackChangeListeners.getInstance()
+                    .unregisterTaskStackListener(mFrozenTaskListener);
+        });
     }
 
     public boolean isTaskListFrozen() {
@@ -340,8 +351,7 @@
     }
 
     private void notifySysuiOfCurrentRotation(int rotation) {
-        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
-                .notifyPrioritizedRotation(rotation));
+        UI_HELPER_EXECUTOR.execute(() -> mSystemUiProxy.notifyPrioritizedRotation(rotation));
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index 94ef4dd..87953c7 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -1073,6 +1073,19 @@
     //
     // Desktop Mode
     //
+    /** Calls shell to create a new desk (if possible) on the display whose ID is `displayId`. */
+    fun createDesktop(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?) =
+        executeWithErrorLog({ "Failed call activateDesk" }) {
+            desktopMode?.activateDesk(deskId, transition)
+        }
+
     /** Call shell to show all apps active on the desktop */
     fun showDesktopApps(displayId: Int, transition: RemoteTransition?) =
         executeWithErrorLog({ "Failed call showDesktopApps" }) {
@@ -1089,14 +1102,6 @@
             desktopMode?.showDesktopApp(taskId, transition, toFrontReason)
         }
 
-    /** Call shell to get number of visible freeform tasks */
-    fun getVisibleDesktopTaskCount(displayId: Int): Int {
-        executeWithErrorLog({ "Failed call getVisibleDesktopTaskCount" }) {
-            return desktopMode?.getVisibleTaskCount(displayId) ?: 0
-        }
-        return 0
-    }
-
     /** Set a listener on shell to get updates about desktop task state */
     fun setDesktopTaskListener(listener: IDesktopTaskListener?) {
         desktopTaskListener = listener
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index 1d40d76..fe25f32 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -21,10 +21,13 @@
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.util.AsyncClockEventDelegate;
+import com.android.quickstep.util.ContextualSearchStateManager;
 
 /**
  * Launcher Quickstep base component for Dagger injection.
@@ -49,4 +52,10 @@
     DesktopVisibilityController getDesktopVisibilityController();
 
     TopTaskTracker getTopTaskTracker();
+
+    RotationTouchHelper getRotationTouchHelper();
+
+    ContextualSearchStateManager getContextualSearchStateManager();
+
+    RecentsAnimationDeviceState getRecentsAnimationDeviceState();
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d9209bf..fff7e9b 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -33,7 +33,6 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
@@ -47,6 +46,7 @@
 import com.android.quickstep.GestureState;
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.views.OverviewActionsView;
@@ -210,7 +210,7 @@
             if (!found) {
                 ArrayList<GroupTask> newList = new ArrayList<>(taskGroups.size() + 1);
                 newList.addAll(taskGroups);
-                newList.add(new GroupTask(mHomeTask, null, null));
+                newList.add(new SingleTask(mHomeTask));
                 taskGroups = newList;
             }
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
index a2884b6..7e5afc3 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -15,9 +15,15 @@
  */
 package com.android.quickstep.fallback;
 
+import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion;
+
 import android.content.Context;
 import android.util.AttributeSet;
 
+import com.android.launcher3.statemanager.StatefulContainer;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewRecentsTouchContext;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControllerDeprecated;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -25,16 +31,33 @@
 /**
  * Drag layer for fallback recents activity
  */
-public class RecentsDragLayer<T extends Context & RecentsViewContainer> extends BaseDragLayer<T> {
+public class RecentsDragLayer<T extends Context & RecentsViewContainer
+        & StatefulContainer<RecentsState>> extends BaseDragLayer<T> {
+
+    private final TaskViewRecentsTouchContext mTaskViewRecentsTouchContext =
+            new TaskViewRecentsTouchContext() {
+                @Override
+                public boolean isRecentsInteractive() {
+                    return mContainer.getRootView().hasWindowFocus()
+                            || mContainer.getStateManager().getState().hasLiveTile();
+                }
+
+                @Override
+                public boolean isRecentsModal() {
+                    return false;
+                }
+            };
+
     public RecentsDragLayer(Context context, AttributeSet attrs) {
         super(context, attrs, 1 /* alphaChannelCount */);
     }
 
     @Override
     public void recreateControllers() {
-        mControllers = new TouchController[] {
-                new RecentsTaskController(mContainer),
-                new FallbackNavBarTouchController(mContainer),
-        };
+        mControllers = new TouchController[]{
+                enableExpressiveDismissTaskMotion() ? new TaskViewTouchController<>(mContainer,
+                        mTaskViewRecentsTouchContext) : new TaskViewTouchControllerDeprecated<>(
+                        mContainer, mTaskViewRecentsTouchContext),
+                new FallbackNavBarTouchController(mContainer)};
     }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
deleted file mode 100644
index 07da379..0000000
--- a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2018 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;
-
-import android.content.Context;
-
-import com.android.launcher3.statemanager.StatefulContainer;
-import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
-import com.android.quickstep.views.RecentsViewContainer;
-
-public class RecentsTaskController<T extends Context & RecentsViewContainer &
-        StatefulContainer<RecentsState>> extends TaskViewTouchController<T> {
-    public RecentsTaskController(T container) {
-        super(container);
-    }
-
-    @Override
-    protected boolean isRecentsInteractive() {
-        return mContainer.getRootView().hasWindowFocus()
-                || mContainer.getStateManager().getState().hasLiveTile();
-    }
-
-    @Override
-    protected boolean isRecentsModal() {
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 9bfe71f..b2a30ca 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -31,15 +31,15 @@
 import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_0;
-import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_90;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_180;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_270;
+import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_90;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW;
-import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__PORTRAIT;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__LANDSCAPE;
+import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__PORTRAIT;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__SEASCAPE;
 
 import android.content.Context;
@@ -69,6 +69,7 @@
 import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
@@ -386,7 +387,8 @@
                 // and then write to StatsLog.
                 app.getModel().enqueueModelUpdateTask((taskController, dataModel, apps) ->
                         write(event, applyOverwrites(mItemInfo.buildProto(
-                                dataModel.collections.get(mItemInfo.container), mContext))));
+                                (CollectionInfo) dataModel.itemsIdMap.get(mItemInfo.container),
+                                mContext))));
             })) {
                 // Write log on the model thread so that logs do not go out of order
                 // (for eg: drop comes after drag)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 639d3a7..02baa39 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -146,8 +146,8 @@
             }
     }
 
-    override fun onDetachedFromWindow() {
-        super.onDetachedFromWindow()
+    // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
+    fun destroyScopes() {
         val scopeToCancel = viewAttachedScope
         recentsCoroutineScope.launch(dispatcherProvider.background) {
             scopeToCancel.cancel("TaskThumbnailView detaching from window")
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
index f75d3b3..ed96399 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -21,7 +21,6 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN;
 
 import android.app.PendingIntent;
@@ -44,11 +43,13 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.EventLogArray;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.quickstep.DeviceConfigWrapper;
@@ -58,12 +59,14 @@
 import java.io.PrintWriter;
 import java.util.Optional;
 
-/** Long-lived class to manage Contextual Search states like the user setting and availability. */
-public class ContextualSearchStateManager implements ResourceBasedOverride, SafeCloseable {
+import javax.inject.Inject;
 
-    public static final MainThreadInitializedObject<ContextualSearchStateManager> INSTANCE =
-            forOverride(ContextualSearchStateManager.class,
-                    R.string.contextual_search_state_manager_class);
+/** Long-lived class to manage Contextual Search states like the user setting and availability. */
+@LauncherAppSingleton
+public class ContextualSearchStateManager  {
+
+    public static final DaggerSingletonObject<ContextualSearchStateManager> INSTANCE =
+            new DaggerSingletonObject<>(LauncherAppComponent::getContextualSearchStateManager);
 
     private static final String TAG = "ContextualSearchStMgr";
     private static final int MAX_DEBUG_EVENT_SIZE = 20;
@@ -73,23 +76,29 @@
     private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi;
     private final SimpleBroadcastReceiver mContextualSearchPackageReceiver =
             new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties());
-    private final SettingsCache.OnChangeListener mContextualSearchSettingChangedListener =
-            this::onContextualSearchSettingChanged;
     protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE);
 
     // Cached value whether the ContextualSearch intent filter matched any enabled components.
     private boolean mIsContextualSearchIntentAvailable;
     private boolean mIsContextualSearchSettingEnabled;
 
-    protected Context mContext;
-    protected String mContextualSearchPackage;
+    protected final Context mContext;
+    protected final String mContextualSearchPackage;
+    protected final SystemUiProxy mSystemUiProxy;
+    protected final TopTaskTracker mTopTaskTracker;
 
-    public ContextualSearchStateManager() {}
-
-    public ContextualSearchStateManager(Context context) {
+    @Inject
+    public ContextualSearchStateManager(
+            @ApplicationContext Context context,
+            SettingsCache settingsCache,
+            SystemUiProxy systemUiProxy,
+            TopTaskTracker topTaskTracker,
+            DaggerSingletonTracker lifeCycle) {
         mContext = context;
         mContextualSearchPackage = mContext.getResources().getString(
                 com.android.internal.R.string.config_defaultContextualSearchPackageName);
+        mSystemUiProxy = systemUiProxy;
+        mTopTaskTracker = topTaskTracker;
 
         if (areAllContextualSearchFlagsDisabled()
                 || !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
@@ -106,11 +115,20 @@
                 context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
                 Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED);
 
-        SettingsCache.INSTANCE.get(context).register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
-                mContextualSearchSettingChangedListener);
-        onContextualSearchSettingChanged(
-                SettingsCache.INSTANCE.get(context).getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI));
-        SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener);
+        SettingsCache.OnChangeListener settingChangedListener =
+                isEnabled -> mIsContextualSearchSettingEnabled = isEnabled;
+        settingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, settingChangedListener);
+        mIsContextualSearchSettingEnabled =
+                settingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI);
+
+        systemUiProxy.addOnStateChangeListener(mSysUiStateChangeListener);
+
+        lifeCycle.addCloseable(() -> {
+            mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
+            unregisterSearchScreenSystemAction();
+            settingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, settingChangedListener);
+            systemUiProxy.removeOnStateChangeListener(mSysUiStateChangeListener);
+        });
     }
 
     /** Return {@code true} if the Settings toggle is enabled. */
@@ -118,10 +136,6 @@
         return mIsContextualSearchSettingEnabled;
     }
 
-    private void onContextualSearchSettingChanged(boolean isEnabled) {
-        mIsContextualSearchSettingEnabled = isEnabled;
-    }
-
     /** Whether search supports showing on the lockscreen. */
     protected boolean supportsShowWhenLocked() {
         return false;
@@ -208,7 +222,7 @@
 
     protected final void updateOverridesToSysUi() {
         // LPH commit haptic is always enabled
-        SystemUiProxy.INSTANCE.get(mContext).setOverrideHomeButtonLongPress(
+        mSystemUiProxy.setOverrideHomeButtonLongPress(
                 getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true);
         Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";"
                 + getLPHCustomSlopMultiplier().orElse(0f));
@@ -227,10 +241,8 @@
                                     new ContextualSearchInvoker(mContext).show(
                                             ENTRYPOINT_SYSTEM_ACTION);
                             if (contextualSearchInvoked) {
-                                String runningPackage =
-                                        TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask(
-                                                /* filterOnlyVisibleRecents */
-                                                true).getPackageName();
+                                String runningPackage = mTopTaskTracker.getCachedTopTask(
+                                        /* filterOnlyVisibleRecents */ true).getPackageName();
                                 StatsLogManager.newInstance(mContext).logger()
                                         .withPackageName(runningPackage)
                                         .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION);
@@ -259,15 +271,6 @@
         }
     }
 
-    @Override
-    public void close() {
-        mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
-        unregisterSearchScreenSystemAction();
-        SettingsCache.INSTANCE.get(mContext).unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
-                mContextualSearchSettingChangedListener);
-        SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener);
-    }
-
     protected final void addEventLog(String event) {
         synchronized (mEventLogArray) {
             mEventLogArray.addLog(event);
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.kt b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
index 1cee2d2..5463cf7 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.kt
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
@@ -17,7 +17,6 @@
 
 import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
-import java.util.Objects
 
 /**
  * A [Task] container that can contain N number of tasks that are part of the desktop in recent
@@ -39,9 +38,6 @@
     override fun equals(o: Any?): Boolean {
         if (this === o) return true
         if (o !is DesktopTask) return false
-        if (!super.equals(o)) return false
-        return tasks == o.tasks
+        return super.equals(o)
     }
-
-    override fun hashCode() = Objects.hash(super.hashCode(), tasks)
 }
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.kt b/quickstep/src/com/android/quickstep/util/GroupTask.kt
index 0bee5f6..d5bbcd3 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.kt
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.kt
@@ -22,10 +22,10 @@
 import java.util.Objects
 
 /**
- * A [Task] container that can contain one or two tasks, depending on if the two tasks are
- * represented as an app-pair in the recents task list.
+ * An abstract class for creating [Task] containers that can be [SingleTask]s, [SplitTask]s, or
+ * [DesktopTask]s in the recent tasks list.
  */
-open class GroupTask
+abstract class GroupTask
 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
 constructor(
     @Deprecated("Prefer using `getTasks()` instead") @JvmField val task1: Task,
@@ -33,13 +33,16 @@
     @JvmField val mSplitBounds: SplitConfigurationOptions.SplitBounds?,
     @JvmField val taskViewType: TaskViewType,
 ) {
-    constructor(task: Task) : this(task, null, null)
-
-    constructor(
-        t1: Task,
-        t2: Task?,
+    protected constructor(
+        task1: Task,
+        task2: Task?,
         splitBounds: SplitConfigurationOptions.SplitBounds?,
-    ) : this(t1, t2, splitBounds, if (t2 != null) TaskViewType.GROUPED else TaskViewType.SINGLE)
+    ) : this(
+        task1,
+        task2,
+        splitBounds,
+        if (task2 != null) TaskViewType.GROUPED else TaskViewType.SINGLE,
+    )
 
     open fun containsTask(taskId: Int) =
         task1.key.id == taskId || (task2 != null && task2.key.id == taskId)
@@ -59,18 +62,50 @@
         get() = listOfNotNull(task1, task2)
 
     /** Creates a copy of this instance */
-    open fun copy() = GroupTask(Task(task1), if (task2 != null) Task(task2) else null, mSplitBounds)
+    abstract fun copy(): GroupTask
 
     override fun toString() = "type=$taskViewType task1=$task1 task2=$task2"
 
     override fun equals(o: Any?): Boolean {
         if (this === o) return true
         if (o !is GroupTask) return false
-        return taskViewType == o.taskViewType &&
-            task1 == o.task1 &&
-            task2 == o.task2 &&
-            mSplitBounds == o.mSplitBounds
+        return taskViewType == o.taskViewType && tasks == o.tasks
     }
 
-    override fun hashCode() = Objects.hash(task1, task2, mSplitBounds, taskViewType)
+    override fun hashCode() = Objects.hash(tasks, taskViewType)
+}
+
+/** A [Task] container that must contain exactly one task in the recent tasks list. */
+class SingleTask(task: Task) :
+    GroupTask(task, task2 = null, mSplitBounds = null, TaskViewType.SINGLE) {
+    override fun copy() = SingleTask(task1)
+
+    override fun toString() = "type=$taskViewType task=$task1"
+
+    override fun equals(o: Any?): Boolean {
+        if (this === o) return true
+        if (o !is SingleTask) return false
+        return super.equals(o)
+    }
+}
+
+/**
+ * A [Task] container that must contain exactly two tasks and split bounds to represent an app-pair
+ * in the recent tasks list.
+ */
+class SplitTask(task1: Task, task2: Task, splitBounds: SplitConfigurationOptions.SplitBounds) :
+    GroupTask(task1, task2, splitBounds, TaskViewType.GROUPED) {
+
+    override fun copy() = SplitTask(task1, task2!!, mSplitBounds!!)
+
+    override fun toString() = "type=$taskViewType task1=$task1 task2=$task2"
+
+    override fun equals(o: Any?): Boolean {
+        if (this === o) return true
+        if (o !is SplitTask) return false
+        if (mSplitBounds!! != o.mSplitBounds!!) return false
+        return super.equals(o)
+    }
+
+    override fun hashCode() = Objects.hash(super.hashCode(), mSplitBounds)
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index d982e81..4005c5a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -33,38 +33,34 @@
         // TODO(b/254378592): Remove these methods when the two classes are reunited
         /** Converts the shell version of SplitBounds to the launcher version */
         @JvmStatic
-        fun convertShellSplitBoundsToLauncher(
-            shellSplitBounds: SplitBounds?
-        ): SplitConfigurationOptions.SplitBounds? {
-            return if (shellSplitBounds == null) {
-                null
-            } else {
-                SplitConfigurationOptions.SplitBounds(
-                    shellSplitBounds.leftTopBounds,
-                    shellSplitBounds.rightBottomBounds,
-                    shellSplitBounds.leftTopTaskId,
-                    shellSplitBounds.rightBottomTaskId,
-                    shellSplitBounds.snapPosition
-                )
-            }
-        }
+        fun convertShellSplitBoundsToLauncher(shellSplitBounds: SplitBounds) =
+            SplitConfigurationOptions.SplitBounds(
+                shellSplitBounds.leftTopBounds,
+                shellSplitBounds.rightBottomBounds,
+                shellSplitBounds.leftTopTaskId,
+                shellSplitBounds.rightBottomTaskId,
+                shellSplitBounds.snapPosition,
+            )
 
         /**
          * Given a TransitionInfo, generates the tree structure for those changes and extracts out
-         * the top most root and it's two immediate children.
-         * Changes can be provided in any order.
+         * the top most root and it's two immediate children. Changes can be provided in any order.
          *
-         * @return a [Pair] where first -> top most split root,
-         *         second -> [List] of 2, leftTop/bottomRight stage roots
+         * @return a [Pair] where first -> top most split root, second -> [List] of 2,
+         *   leftTop/bottomRight stage roots
          */
-        fun extractTopParentAndChildren(transitionInfo: TransitionInfo):
-                Pair<Change, List<Change>>? {
+        fun extractTopParentAndChildren(
+            transitionInfo: TransitionInfo
+        ): Pair<Change, List<Change>>? {
             val parentToChildren = mutableMapOf<Change, MutableList<Change>>()
             val hasParent = mutableSetOf<Change>()
             // filter out anything that isn't opening and the divider
-            val taskChanges: List<Change> = transitionInfo.changes
-                    .filter { change -> (change.mode == TRANSIT_OPEN ||
-                            change.mode == TRANSIT_TO_FRONT) && change.flags < FLAG_FIRST_CUSTOM}
+            val taskChanges: List<Change> =
+                transitionInfo.changes
+                    .filter { change ->
+                        (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) &&
+                            change.flags < FLAG_FIRST_CUSTOM
+                    }
                     .toList()
 
             // 1. Build Parent-Child Relationships
@@ -73,8 +69,8 @@
                 //  startAnimation() and we can know the precise taskIds of launching tasks.
                 change.parent?.let { parent ->
                     parentToChildren
-                            .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() }
-                            .add(change)
+                        .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() }
+                        .add(change)
                     hasParent.add(change)
                 }
             }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 684e84a..c0b026b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2693,16 +2693,17 @@
     }
 
     private void onReset() {
-        if (enableRefactorTaskThumbnail()) {
-            mRecentsViewModel.onReset();
-            removeAllViews();
-        }
         unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
         setCurrentPage(0);
         LayoutUtils.setViewEnabled(mActionsView, true);
         if (mOrientationState.setGestureActive(false)) {
             updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
         }
+        if (enableRefactorTaskThumbnail()) {
+            mRecentsViewModel.onReset();
+            // TODO(b/391842220) Remove TaskViews rather than calling specific logic to cancel scope
+            getTaskViews().forEach(TaskView::destroyScopes);
+        }
     }
 
     public int getRunningTaskViewId() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 5de8d1c..7ac0946 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -141,6 +141,11 @@
         }
     }
 
+    // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
+    fun destroyScopes() {
+        thumbnailView.destroyScopes()
+    }
+
     fun bindThumbnailView() {
         taskThumbnailViewModel.bind(task.key.id)
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 99df84c..e7a395f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -617,6 +617,11 @@
         taskContainers.forEach { it.destroy() }
     }
 
+    fun destroyScopes() {
+        // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
+        taskContainers.forEach { it.destroyScopes() }
+    }
+
     // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
     override fun hasOverlappingRendering() = false
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 6eccc36..c792783 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.res.Resources
+import android.graphics.Rect
 import android.os.Process
 import android.os.UserHandle
 import android.platform.test.annotations.EnableFlags
@@ -36,12 +37,16 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.taskbar.TaskbarRecentAppsController.TaskState
 import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
 import com.android.quickstep.TaskIconCache
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.SingleTask
+import com.android.quickstep.util.SplitTask
 import com.android.systemui.shared.recents.model.Task
+import com.android.wm.shell.shared.split.SplitScreenConstants
 import com.google.common.truth.Truth.assertThat
 import java.util.function.Consumer
 import org.junit.Before
@@ -913,15 +918,21 @@
         return packageNames.map { packageName ->
             if (packageName.startsWith("split")) {
                 val splitPackages = packageName.split("_")
-                GroupTask(
+                SplitTask(
                     createTask(100, splitPackages[0]),
                     createTask(101, splitPackages[1]),
-                    /* splitBounds = */ null,
+                    SplitConfigurationOptions.SplitBounds(
+                        /* leftTopBounds = */ Rect(),
+                        /* rightBottomBounds = */ Rect(),
+                        /* leftTopTaskId = */ -1,
+                        /* rightBottomTaskId = */ -1,
+                        /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50,
+                    ),
                 )
             } else {
                 // Use the number at the end of the test packageName as the id.
                 val id = 1000 + packageName[packageName.length - 1].code
-                GroupTask(createTask(id, packageName))
+                SingleTask(createTask(id, packageName))
             }
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
index e7f3523..df70b10 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
@@ -28,6 +28,7 @@
 import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW
 import com.android.launcher3.taskbar.TaskbarIconType.RECENT
 import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.SingleTask
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.Task.TaskKey
 import com.google.common.truth.FailureMetadata
@@ -56,7 +57,7 @@
     /** Creates a list of fake recent tasks. */
     fun createRecents(size: Int): List<GroupTask> {
         return List(size) {
-            GroupTask(
+            SingleTask(
                 Task().apply {
                     key =
                         TaskKey(
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index 542eb64..0c74610 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -18,6 +18,8 @@
 
 import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
+import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertTrue;
@@ -68,6 +70,7 @@
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.InputConsumerController;
+import com.android.wm.shell.shared.split.SplitBounds;
 
 import com.google.android.msdl.data.model.MSDLToken;
 
@@ -140,6 +143,12 @@
     public void setUpAnimationTargets() {
         Bundle extras = new Bundle();
         extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true);
+        extras.putParcelable(KEY_EXTRA_SPLIT_BOUNDS, new SplitBounds(
+                /* leftTopBounds = */ new Rect(),
+                /* rightBottomBounds = */ new Rect(),
+                /* leftTopTaskId = */ -1,
+                /* rightBottomTaskId = */ -1,
+                /* snapPosition = */ SNAP_TO_2_50_50));
         mRecentsAnimationTargets = new RecentsAnimationTargets(
                 new RemoteAnimationTarget[] {mRemoteAnimationTarget},
                 new RemoteAnimationTarget[] {mRemoteAnimationTarget},
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index f05b422..af741f6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -86,17 +86,11 @@
         whenever(displayManager.displays).thenReturn(arrayOf(display))
 
         sandboxContext.initDaggerComponent(
-            DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
+            DaggerTestComponent.builder()
+                .bindSystemUiProxy(systemUiProxy)
+                .bindRotationHelper(mock(RotationTouchHelper::class.java))
+                .bindRecentsState(mock(RecentsAnimationDeviceState::class.java))
         )
-        sandboxContext.putObject(
-            RotationTouchHelper.INSTANCE,
-            mock(RotationTouchHelper::class.java),
-        )
-        sandboxContext.putObject(
-            RecentsAnimationDeviceState.INSTANCE,
-            mock(RecentsAnimationDeviceState::class.java),
-        )
-
         gestureState = spy(GestureState(OverviewComponentObserver.INSTANCE.get(sandboxContext), 0))
 
         underTest =
@@ -117,20 +111,14 @@
         gestureState.setTrackpadGestureType(GestureState.TrackpadGestureType.THREE_FINGER)
         underTest.onGestureEnded(flingSpeed, PointF())
         verify(systemUiProxy)
-            .updateContextualEduStats(
-                /* isTrackpadGesture= */ eq(true),
-                eq(GestureType.HOME),
-            )
+            .updateContextualEduStats(/* isTrackpadGesture= */ eq(true), eq(GestureType.HOME))
     }
 
     @Test
     fun goHomeFromAppByTouch_updateEduStats() {
         underTest.onGestureEnded(flingSpeed, PointF())
         verify(systemUiProxy)
-            .updateContextualEduStats(
-                /* isTrackpadGesture= */ eq(false),
-                eq(GestureType.HOME),
-            )
+            .updateContextualEduStats(/* isTrackpadGesture= */ eq(false), eq(GestureType.HOME))
     }
 }
 
@@ -141,6 +129,10 @@
     interface Builder : LauncherAppComponent.Builder {
         @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
 
+        @BindsInstance fun bindRotationHelper(helper: RotationTouchHelper): Builder
+
+        @BindsInstance fun bindRecentsState(state: RecentsAnimationDeviceState): Builder
+
         override fun build(): TestComponent
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index 4a7c537..cad3b99 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -35,6 +35,7 @@
 import android.app.TaskInfo;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Rect;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -45,6 +46,8 @@
 import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.shared.GroupedTaskInfo;
+import com.android.wm.shell.shared.split.SplitBounds;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -99,7 +102,12 @@
     @Test
     public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception  {
         GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(
-                new RecentTaskInfo(), new RecentTaskInfo(), null);
+                new RecentTaskInfo(), new RecentTaskInfo(), new SplitBounds(
+                        /* leftTopBounds = */ new Rect(),
+                        /* rightBottomBounds = */ new Rect(),
+                        /* leftTopTaskId = */ -1,
+                        /* rightBottomTaskId = */ -1,
+                        /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50));
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
@@ -129,7 +137,13 @@
         task1.taskDescription = new ActivityManager.TaskDescription(taskDescription);
         RecentTaskInfo task2 = new RecentTaskInfo();
         task2.taskDescription = new ActivityManager.TaskDescription();
-        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(task1, task2, null);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(task1, task2,
+                new SplitBounds(
+                        /* leftTopBounds = */ new Rect(),
+                        /* rightBottomBounds = */ new Rect(),
+                        /* leftTopTaskId = */ -1,
+                        /* rightBottomTaskId = */ -1,
+                        /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50));
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index 0245908..b652ee8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -1,9 +1,8 @@
 package com.android.quickstep
 
-import android.content.Context
 import androidx.test.annotation.UiThreadTest
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SmallTest
+import com.android.launcher3.dagger.LauncherComponentProvider
 import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
 import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
@@ -12,6 +11,7 @@
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.NavigationMode
+import com.android.launcher3.util.SandboxApplication
 import com.android.quickstep.util.GestureExclusionManager
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING
@@ -27,6 +27,7 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -43,21 +44,32 @@
 @RunWith(LauncherMultivalentJUnit::class)
 class RecentsAnimationDeviceStateTest {
 
+    @get:Rule val context = SandboxApplication()
+
     @Mock private lateinit var exclusionManager: GestureExclusionManager
     @Mock private lateinit var info: Info
 
-    private val context = ApplicationProvider.getApplicationContext() as Context
     private lateinit var underTest: RecentsAnimationDeviceState
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = RecentsAnimationDeviceState(context, exclusionManager)
+
+        val component = LauncherComponentProvider.get(context)
+        underTest =
+            RecentsAnimationDeviceState(
+                context,
+                exclusionManager,
+                component.displayController,
+                component.contextualSearchStateManager,
+                component.rotationTouchHelper,
+                component.settingsCache,
+                component.daggerSingletonTracker,
+            )
     }
 
     @After
     fun tearDown() {
-        underTest.close()
         UI_HELPER_EXECUTOR.submit {}.get()
         MAIN_EXECUTOR.submit {}.get()
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
index a5c60ce..99a1c59 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
@@ -32,6 +32,7 @@
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Rect;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.annotation.UiThreadTest;
@@ -42,9 +43,12 @@
 import com.android.launcher3.R;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SplitTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -173,7 +177,13 @@
         Task.TaskKey taskKey2 = new Task.TaskKey(taskInfo2);
         Task task2 = Task.from(taskKey2, taskInfo2, false);
 
-        allTasks.add(new GroupTask(task1, task2, null));
+        allTasks.add(
+                new SplitTask(task1, task2, new SplitConfigurationOptions.SplitBounds(
+                        /* leftTopBounds = */ new Rect(),
+                        /* rightBottomBounds = */ new Rect(),
+                        /* leftTopTaskId = */ -1,
+                        /* rightBottomTaskId = */ -1,
+                        /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50)));
         return allTasks;
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index b6cf5bd..823f808 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -19,13 +19,17 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.graphics.Bitmap
+import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TestDispatcherProvider
 import com.android.quickstep.util.DesktopTask
-import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.SingleTask
+import com.android.quickstep.util.SplitTask
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
@@ -48,8 +52,18 @@
     private val tasks = (0..5).map(::createTaskWithId)
     private val defaultTaskList =
         listOf(
-            GroupTask(tasks[0]),
-            GroupTask(tasks[1], tasks[2], null),
+            SingleTask(tasks[0]),
+            SplitTask(
+                tasks[1],
+                tasks[2],
+                SplitConfigurationOptions.SplitBounds(
+                    /* leftTopBounds = */ Rect(),
+                    /* rightBottomBounds = */ Rect(),
+                    /* leftTopTaskId = */ -1,
+                    /* rightBottomTaskId = */ -1,
+                    /* snapPosition = */ SNAP_TO_2_50_50,
+                ),
+            ),
             DesktopTask(tasks.subList(3, 6)),
         )
     private val recentsModel = FakeRecentTasksDataSource()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
index 108cfb5..fa043b9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -21,7 +21,6 @@
 import android.graphics.Rect
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.SplitConfigurationOptions
-import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
 import com.android.wm.shell.shared.split.SplitScreenConstants
 import com.google.common.truth.Truth.assertThat
@@ -33,28 +32,28 @@
 
     @Test
     fun testGroupTask_sameInstance_isEqual() {
-        val task = GroupTask(createTask(1))
+        val task = SingleTask(createTask(1))
         assertThat(task).isEqualTo(task)
     }
 
     @Test
     fun testGroupTask_identicalConstructor_isEqual() {
-        val task1 = GroupTask(createTask(1))
-        val task2 = GroupTask(createTask(1))
+        val task1 = SingleTask(createTask(1))
+        val task2 = SingleTask(createTask(1))
         assertThat(task1).isEqualTo(task2)
     }
 
     @Test
     fun testGroupTask_copy_isEqual() {
-        val task1 = GroupTask(createTask(1))
+        val task1 = SingleTask(createTask(1))
         val task2 = task1.copy()
         assertThat(task1).isEqualTo(task2)
     }
 
     @Test
     fun testGroupTask_differentId_isNotEqual() {
-        val task1 = GroupTask(createTask(1))
-        val task2 = GroupTask(createTask(2))
+        val task1 = SingleTask(createTask(1))
+        val task2 = SingleTask(createTask(2))
         assertThat(task1).isNotEqualTo(task2)
     }
 
@@ -66,10 +65,10 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_2_50_50
+                SplitScreenConstants.SNAP_TO_2_50_50,
             )
-        val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
-        val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
+        val task1 = SplitTask(createTask(1), createTask(2), splitBounds)
+        val task2 = SplitTask(createTask(1), createTask(2), splitBounds)
         assertThat(task1).isEqualTo(task2)
     }
 
@@ -81,7 +80,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_2_50_50
+                SplitScreenConstants.SNAP_TO_2_50_50,
             )
         val splitBounds2 =
             SplitConfigurationOptions.SplitBounds(
@@ -89,17 +88,17 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_2_33_66
+                SplitScreenConstants.SNAP_TO_2_33_66,
             )
-        val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskViewType.GROUPED)
-        val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskViewType.GROUPED)
+        val task1 = SplitTask(createTask(1), createTask(2), splitBounds1)
+        val task2 = SplitTask(createTask(1), createTask(2), splitBounds2)
         assertThat(task1).isNotEqualTo(task2)
     }
 
     @Test
     fun testGroupTask_differentType_isNotEqual() {
-        val task1 = GroupTask(createTask(1), null, null, TaskViewType.SINGLE)
-        val task2 = GroupTask(createTask(1), null, null, TaskViewType.DESKTOP)
+        val task1 = SingleTask(createTask(1))
+        val task2 = DesktopTask(listOf(createTask(1)))
         assertThat(task1).isNotEqualTo(task2)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 708273e..0491c07 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -660,10 +660,16 @@
         intent.component = task2ComponentName
         taskInfo.baseIntent = intent
         task2.key = Task.TaskKey(taskInfo)
-        return GroupTask(
+        return SplitTask(
             task1,
             task2,
-            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50),
+            SplitConfigurationOptions.SplitBounds(
+                /* leftTopBounds = */ Rect(),
+                /* rightBottomBounds = */ Rect(),
+                /* leftTopTaskId = */ -1,
+                /* rightBottomTaskId = */ -1,
+                /* snapPosition = */ SNAP_TO_2_50_50,
+            ),
         )
     }
 
@@ -692,10 +698,16 @@
         intent.component = task2ComponentName
         taskInfo.baseIntent = intent
         task2.key = Task.TaskKey(taskInfo)
-        return GroupTask(
+        return SplitTask(
             task1,
             task2,
-            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50),
+            SplitConfigurationOptions.SplitBounds(
+                /* leftTopBounds = */ Rect(),
+                /* rightBottomBounds = */ Rect(),
+                /* leftTopTaskId = */ -1,
+                /* rightBottomTaskId = */ -1,
+                /* snapPosition = */ SNAP_TO_2_50_50,
+            ),
         )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
index 3c5e1e8..e2ca91a 100644
--- a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep;
 
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
 import static com.android.quickstep.InputConsumerUtils.newBaseConsumer;
 import static com.android.quickstep.InputConsumerUtils.newConsumer;
 
@@ -40,6 +38,9 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppModule;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarManager;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
@@ -54,7 +55,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.LockedUserState;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
@@ -75,6 +76,9 @@
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
+import dagger.BindsInstance;
+import dagger.Component;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -93,8 +97,8 @@
 @RunWith(AndroidJUnit4.class)
 public class InputConsumerUtilsTest {
 
-    @NonNull private final MainThreadInitializedObject.SandboxContext mContext =
-            new MainThreadInitializedObject.SandboxContext(getApplicationContext());
+    @Rule public final SandboxApplication mContext = new SandboxApplication();
+
     @NonNull private final InputMonitorCompat mInputMonitorCompat = new InputMonitorCompat("", 0);
 
     private TaskAnimationManager mTaskAnimationManager;
@@ -125,10 +129,12 @@
     }
 
     @Before
-    public void setupMainThreadInitializedObjects() {
-        mContext.putObject(LockedUserState.INSTANCE, mLockedUserState);
-        mContext.putObject(RotationTouchHelper.INSTANCE, mock(RotationTouchHelper.class));
-        mContext.putObject(RecentsAnimationDeviceState.INSTANCE, mDeviceState);
+    public void setupDaggerGraphOverrides() {
+        mContext.initDaggerComponent(DaggerInputConsumerUtilsTest_TestComponent
+                .builder()
+                .bindLockedState(mLockedUserState)
+                .bindRotationHelper(mock(RotationTouchHelper.class))
+                .bindRecentsState(mDeviceState));
     }
 
     @Before
@@ -595,4 +601,18 @@
 
         return bubbleControllers;
     }
+
+    @LauncherAppSingleton
+    @Component(modules = {LauncherAppModule.class})
+    interface TestComponent extends LauncherAppComponent {
+        @Component.Builder
+        interface Builder extends LauncherAppComponent.Builder {
+            @BindsInstance Builder bindLockedState(LockedUserState state);
+            @BindsInstance Builder bindRotationHelper(RotationTouchHelper helper);
+            @BindsInstance Builder bindRecentsState(RecentsAnimationDeviceState state);
+
+            @Override
+            TestComponent build();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 3d71ff1..c38444c 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -529,9 +529,11 @@
 
         for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
             DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
-            cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
             canvas.save();
-            canvas.translate(mTempLocation[0], mTempLocation[1]);
+            if (cellDrawing.mDelegateCellX >= 0 && cellDrawing.mDelegateCellY >= 0) {
+                cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
+                canvas.translate(mTempLocation[0], mTempLocation[1]);
+            }
             cellDrawing.drawUnderItem(canvas);
             canvas.restore();
         }
@@ -660,9 +662,11 @@
 
         for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
             DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
-            cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
             canvas.save();
-            canvas.translate(mTempLocation[0], mTempLocation[1]);
+            if (bg.mDelegateCellX >= 0 && bg.mDelegateCellY >= 0) {
+                cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
+                canvas.translate(mTempLocation[0], mTempLocation[1]);
+            }
             bg.drawOverItem(canvas);
             canvas.restore();
         }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 7563493..d93c07f 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -659,7 +659,8 @@
                 // Only fetch badge if the icon is on workspace
                 if (info.id != ItemInfo.NO_ID && badge == null) {
                     badge = appState.getIconCache().getShortcutInfoBadge(si)
-                            .newIcon(context, FLAG_THEMED);
+                            .newIcon(context, ThemeManager.INSTANCE.get(context)
+                                    .isMonoThemeEnabled() ? FLAG_THEMED : 0);
                 }
             }
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
diff --git a/src/com/android/launcher3/dagger/LauncherAppModule.java b/src/com/android/launcher3/dagger/LauncherAppModule.java
index ef136d0..c58a414 100644
--- a/src/com/android/launcher3/dagger/LauncherAppModule.java
+++ b/src/com/android/launcher3/dagger/LauncherAppModule.java
@@ -21,7 +21,9 @@
 @Module(includes = {
         WindowManagerProxyModule.class,
         ApiWrapperModule.class,
-        PluginManagerWrapperModule.class
+        PluginManagerWrapperModule.class,
+        StaticObjectModule.class,
+        AppModule.class
 })
 public class LauncherAppModule {
 }
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index c3e3992..0b7b20f 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PluginManagerWrapper;
@@ -68,6 +69,7 @@
     ThemeManager getThemeManager();
     DisplayController getDisplayController();
     WallpaperColorHints getWallpaperColorHints();
+    LockedUserState getLockedUserState();
 
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index be5f8f7..8a1f96d 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.folder;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
+
 import android.annotation.SuppressLint;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
@@ -37,6 +39,7 @@
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.CollectionInfo;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.Preconditions;
@@ -197,9 +200,18 @@
         @Override
         public void execute(@NonNull ModelTaskController taskController,
                 @NonNull BgDataModel dataModel, @NonNull AllAppsList apps) {
-            mCollectionInfos = dataModel.collections.clone();
+            mCollectionInfos = getCollectionForSuggestions(dataModel);
             mAppInfos = Arrays.asList(apps.copyData());
         }
     }
 
+    public static IntSparseArrayMap<CollectionInfo> getCollectionForSuggestions(
+            BgDataModel dataModel) {
+        IntSparseArrayMap<CollectionInfo> result = new IntSparseArrayMap<>();
+        dataModel.itemsIdMap.stream()
+                .filter(item -> item.itemType == ITEM_TYPE_FOLDER)
+                .forEach(item -> result.put(item.id, (FolderInfo) item));
+        return result;
+    }
+
 }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index f0e4fc4..a0b73ae 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -24,10 +24,10 @@
 import static com.android.launcher3.BubbleTextView.DISPLAY_WORKSPACE;
 import static com.android.launcher3.DeviceProfile.DEFAULT_SCALE;
 import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_PREVIEW_RENDERER;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
-import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
-import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
+import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter;
 
 import android.app.Fragment;
 import android.app.WallpaperColors;
@@ -105,7 +105,9 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 /**
  * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -456,54 +458,48 @@
 
     private void populate(BgDataModel dataModel,
             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
-        // Separate the items that are on the current screen, and the other remaining items.
-        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
-        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+        IntSet missingHotseatRank = new IntSet();
+        IntStream.range(0, mDp.numShownHotseatIcons).forEach(missingHotseatRank::add);
 
-        IntSet currentScreenIds = IntSet.wrap(mWorkspaceScreens.keySet());
-        filterCurrentWorkspaceItems(currentScreenIds, dataModel.workspaceItems,
-                currentWorkspaceItems, otherWorkspaceItems);
-        filterCurrentWorkspaceItems(currentScreenIds, dataModel.appWidgets, currentAppWidgets,
-                otherAppWidgets);
-        for (ItemInfo itemInfo : currentWorkspaceItems) {
-            switch (itemInfo.itemType) {
-                case Favorites.ITEM_TYPE_APPLICATION:
-                case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                    inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
-                    break;
-                case Favorites.ITEM_TYPE_FOLDER:
-                case Favorites.ITEM_TYPE_APP_PAIR:
-                    inflateAndAddCollectionIcon((CollectionInfo) itemInfo);
-                    break;
-                default:
-                    break;
-            }
-        }
-        Map<ComponentKey, AppWidgetProviderInfo> widgetsMap = widgetProviderInfoMap;
-        for (ItemInfo itemInfo : currentAppWidgets) {
-            switch (itemInfo.itemType) {
-                case Favorites.ITEM_TYPE_APPWIDGET:
-                case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                    if (widgetsMap == null) {
-                        widgetsMap = dataModel.widgetsModel.getWidgetsByComponentKey()
-                                .entrySet()
-                                .stream()
-                                .filter(entry -> entry.getValue().widgetInfo != null)
-                                .collect(Collectors.toMap(
-                                        Map.Entry::getKey,
-                                        entry -> entry.getValue().widgetInfo
-                                ));
+        Map<ComponentKey, AppWidgetProviderInfo>[] widgetsMap = new Map[] { widgetProviderInfoMap};
+
+        // Separate the items that are on the current screen, and the other remaining items.
+        dataModel.itemsIdMap.stream()
+                .filter(currentScreenContentFilter(IntSet.wrap(mWorkspaceScreens.keySet())))
+                .forEach(itemInfo -> {
+                    switch (itemInfo.itemType) {
+                        case Favorites.ITEM_TYPE_APPLICATION:
+                        case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                            inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
+                            break;
+                        case Favorites.ITEM_TYPE_FOLDER:
+                        case Favorites.ITEM_TYPE_APP_PAIR:
+                            inflateAndAddCollectionIcon((CollectionInfo) itemInfo);
+                            break;
+                        case Favorites.ITEM_TYPE_APPWIDGET:
+                        case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
+                            if (widgetsMap[0] == null) {
+                                widgetsMap[0] = dataModel.widgetsModel.getWidgetsByComponentKey()
+                                        .entrySet()
+                                        .stream()
+                                        .filter(entry -> entry.getValue().widgetInfo != null)
+                                        .collect(Collectors.toMap(
+                                                Entry::getKey,
+                                                entry -> entry.getValue().widgetInfo
+                                        ));
+                            }
+                            inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, widgetsMap[0]);
+                            break;
+                        default:
+                            break;
                     }
-                    inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, widgetsMap);
-                    break;
-                default:
-                    break;
-            }
-        }
-        IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
-                mDp.numShownHotseatIcons);
+
+                    if (itemInfo.container == CONTAINER_HOTSEAT) {
+                        missingHotseatRank.remove(itemInfo.screenId);
+                    }
+                });
+
+        IntArray ranks = missingHotseatRank.getArray();
         FixedContainerItems hotseatPredictions =
                 dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
         List<ItemInfo> predictions = hotseatPredictions == null
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index c251114..de74ae8 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -19,8 +19,10 @@
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
 import static com.android.launcher3.Flags.enableWorkspaceInflation;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
-import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
+import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -44,11 +46,11 @@
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
@@ -102,14 +104,12 @@
         Trace.beginSection("BaseLauncherBinder#bindWorkspace");
         try {
             // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
-            ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
+            IntSparseArrayMap<ItemInfo> itemsIdMap;
             final IntArray orderedScreenIds = new IntArray();
             ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
             final int workspaceItemCount;
             synchronized (mBgDataModel) {
-                workspaceItems.addAll(mBgDataModel.workspaceItems);
-                appWidgets.addAll(mBgDataModel.appWidgets);
+                itemsIdMap = mBgDataModel.itemsIdMap.clone();
                 orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
                 mBgDataModel.extraItems.forEach(extraItems::add);
                 if (incrementBindId) {
@@ -122,7 +122,7 @@
 
             for (Callbacks cb : mCallbacksList) {
                 new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                        workspaceItems, appWidgets, extraItems, orderedScreenIds)
+                        itemsIdMap, extraItems, orderedScreenIds)
                         .bind(isBindSync, workspaceItemCount);
             }
         } finally {
@@ -258,8 +258,7 @@
         private final BgDataModel mBgDataModel;
 
         private final int mMyBindingId;
-        private final ArrayList<ItemInfo> mWorkspaceItems;
-        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+        private final IntSparseArrayMap<ItemInfo> mItemIdMap;
         private final IntArray mOrderedScreenIds;
         private final ArrayList<FixedContainerItems> mExtraItems;
 
@@ -268,8 +267,7 @@
                 LauncherAppState app,
                 BgDataModel bgDataModel,
                 int myBindingId,
-                ArrayList<ItemInfo> workspaceItems,
-                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                IntSparseArrayMap<ItemInfo> itemIdMap,
                 ArrayList<FixedContainerItems> extraItems,
                 IntArray orderedScreenIds) {
             mCallbacks = callbacks;
@@ -277,8 +275,7 @@
             mApp = app;
             mBgDataModel = bgDataModel;
             mMyBindingId = myBindingId;
-            mWorkspaceItems = workspaceItems;
-            mAppWidgets = appWidgets;
+            mItemIdMap = itemIdMap;
             mExtraItems = extraItems;
             mOrderedScreenIds = orderedScreenIds;
         }
@@ -294,10 +291,15 @@
             ArrayList<ItemInfo> currentAppWidgets = new ArrayList<>();
             ArrayList<ItemInfo> otherAppWidgets = new ArrayList<>();
 
-            filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
-                    otherWorkspaceItems);
-            filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets,
-                    otherAppWidgets);
+            Predicate<ItemInfo> currentScreenCheck = currentScreenContentFilter(currentScreenIds);
+            mItemIdMap.forEach(item -> {
+                if (currentScreenCheck.test(item)) {
+                    (WIDGET_FILTER.test(item) ? currentAppWidgets : currentWorkspaceItems)
+                            .add(item);
+                } else if (item.container == CONTAINER_DESKTOP) {
+                    (WIDGET_FILTER.test(item) ? otherAppWidgets : otherWorkspaceItems).add(item);
+                }
+            });
             final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
             sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
             sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index b9b1e98..a04cbfb 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -20,6 +20,11 @@
 import static com.android.launcher3.BuildConfig.QSB_ON_FIRST_SCREEN;
 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
 import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
 
@@ -31,7 +36,6 @@
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 import android.text.TextUtils;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 import android.view.View;
@@ -39,14 +43,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.CollectionInfo;
-import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -93,22 +94,6 @@
     public final IntSparseArrayMap<ItemInfo> itemsIdMap = new IntSparseArrayMap<>();
 
     /**
-     * List of all the folders and shortcuts directly on the home screen (no widgets
-     * or shortcuts within folders).
-     */
-    public final ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
-
-    /**
-     * All LauncherAppWidgetInfo created by LauncherModel.
-     */
-    public final ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
-
-    /**
-     * Map of id to CollectionInfos of all the folders or app pairs created by LauncherModel
-     */
-    public final IntSparseArrayMap<CollectionInfo> collections = new IntSparseArrayMap<>();
-
-    /**
      * Extra container based items
      */
     public final IntSparseArrayMap<FixedContainerItems> extraItems = new IntSparseArrayMap<>();
@@ -144,9 +129,6 @@
      * Clears all the data
      */
     public synchronized void clear() {
-        workspaceItems.clear();
-        appWidgets.clear();
-        collections.clear();
         itemsIdMap.clear();
         deepShortcutMap.clear();
         extraItems.clear();
@@ -158,7 +140,7 @@
     public synchronized IntArray collectWorkspaceScreens() {
         IntSet screenSet = new IntSet();
         for (ItemInfo item: itemsIdMap) {
-            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+            if (item.container == CONTAINER_DESKTOP) {
                 screenSet.add(item.screenId);
             }
         }
@@ -173,26 +155,14 @@
     public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
             String[] args) {
         writer.println(prefix + "Data Model:");
-        writer.println(prefix + " ---- workspace items ");
-        for (int i = 0; i < workspaceItems.size(); i++) {
-            writer.println(prefix + '\t' + workspaceItems.get(i).toString());
-        }
-        writer.println(prefix + " ---- appwidget items ");
-        for (int i = 0; i < appWidgets.size(); i++) {
-            writer.println(prefix + '\t' + appWidgets.get(i).toString());
-        }
-        writer.println(prefix + " ---- collection items ");
-        for (int i = 0; i < collections.size(); i++) {
-            writer.println(prefix + '\t' + collections.valueAt(i).toString());
+        writer.println(prefix + " ---- items id map ");
+        for (int i = 0; i < itemsIdMap.size(); i++) {
+            writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString());
         }
         writer.println(prefix + " ---- extra items ");
         for (int i = 0; i < extraItems.size(); i++) {
             writer.println(prefix + '\t' + extraItems.valueAt(i).toString());
         }
-        writer.println(prefix + " ---- items id map ");
-        for (int i = 0; i < itemsIdMap.size(); i++) {
-            writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString());
-        }
 
         if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
             writer.println(prefix + "shortcut counts ");
@@ -207,94 +177,38 @@
         removeItem(context, Arrays.asList(items));
     }
 
-    public synchronized void removeItem(Context context, Iterable<? extends ItemInfo> items) {
-        ArraySet<UserHandle> updatedDeepShortcuts = new ArraySet<>();
-        for (ItemInfo item : items) {
-            switch (item.itemType) {
-                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
-                    collections.remove(item.id);
-                    if (FeatureFlags.IS_STUDIO_BUILD) {
-                        for (ItemInfo info : itemsIdMap) {
-                            if (info.container == item.id) {
-                                // We are deleting a collection which still contains items that
-                                // think they are contained by that collection.
-                                String msg = "deleting a collection (" + item + ") which still "
-                                        + "contains items (" + info + ")";
-                                Log.e(TAG, msg);
-                            }
-                        }
-                    }
-                    workspaceItems.remove(item);
-                    break;
-                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
-                    updatedDeepShortcuts.add(item.user);
-                    // Fall through.
-                }
-                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                    workspaceItems.remove(item);
-                    break;
-                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                    appWidgets.remove(item);
-                    break;
-            }
-            itemsIdMap.remove(item.id);
+    public synchronized void removeItem(Context context, List<? extends ItemInfo> items) {
+        if (BuildConfig.IS_STUDIO_BUILD) {
+            items.stream()
+                    .filter(item -> item.itemType == ITEM_TYPE_FOLDER
+                            || item.itemType == ITEM_TYPE_APP_PAIR)
+                    .forEach(item -> itemsIdMap.stream()
+                            .filter(info -> info.container == item.id)
+                            // We are deleting a collection which still contains items that
+                            // think they are contained by that collection.
+                            .forEach(info -> Log.e(TAG,
+                                    "deleting a collection (" + item + ") which still contains"
+                                            + " items (" + info + ")")));
         }
-        updatedDeepShortcuts.forEach(user -> updateShortcutPinnedState(context, user));
+
+        items.forEach(item -> itemsIdMap.remove(item.id));
+        items.stream().map(info -> info.user).distinct().forEach(
+                user -> updateShortcutPinnedState(context, user));
     }
 
     public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
-        addItem(context, item, newItem, null);
-    }
-
-    public synchronized void addItem(
-            Context context, ItemInfo item, boolean newItem, @Nullable LoaderMemoryLogger logger) {
-        if (logger != null) {
-            logger.addLog(
-                    Log.DEBUG,
-                    TAG,
-                    String.format("Adding item to ID map: %s", item.toString()),
-                    /* stackTrace= */ null);
-        }
         itemsIdMap.put(item.id, item);
-        switch (item.itemType) {
-            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                collections.put(item.id, (FolderInfo) item);
-                workspaceItems.add(item);
-                break;
-            case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
-                collections.put(item.id, (AppPairInfo) item);
-                // Fall through here. App pairs are both containers (like folders) and containable
-                // items (can be placed in folders). So we need to add app pairs to the folders
-                // array (above) but also verify the existence of their container, like regular
-                // apps (below).
-            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
-                        item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                    workspaceItems.add(item);
-                } else {
-                    if (newItem) {
-                        if (!collections.containsKey(item.container)) {
-                            // Adding an item to a nonexistent collection.
-                            String msg = "attempted to add item: " + item + " to a nonexistent app"
-                                    + " collection";
-                            Log.e(TAG, msg);
-                        }
-                    } else {
-                        findOrMakeFolder(item.container).add(item);
-                    }
-                }
-                break;
-            case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-            case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                appWidgets.add((LauncherAppWidgetInfo) item);
-                break;
-        }
-        if (newItem && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+        if (newItem && item.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
             updateShortcutPinnedState(context, item.user);
         }
+        if (BuildConfig.IS_DEBUG_DEVICE
+                && newItem
+                && item.container != CONTAINER_DESKTOP
+                && item.container != CONTAINER_HOTSEAT
+                && !(itemsIdMap.get(item.container) instanceof CollectionInfo)) {
+            // Adding an item to a nonexistent collection.
+            Log.e(TAG, "attempted to add item: " + item + " to a nonexistent app collection");
+        }
     }
 
     /**
@@ -334,7 +248,7 @@
         Map<String, Set<String>> modelMap = Stream.concat(
                     // Model shortcuts
                     itemStream.build()
-                        .filter(wi -> wi.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                        .filter(wi -> wi.itemType == ITEM_TYPE_DEEP_SHORTCUT)
                         .map(ShortcutKey::fromItemInfo),
                     // Pending shortcuts
                     ItemInstallQueue.INSTANCE.get(context).getPendingShortcuts(user))
@@ -375,24 +289,6 @@
     }
 
     /**
-     * Return an existing FolderInfo object if we have encountered this ID previously,
-     * or make a new one.
-     */
-    public synchronized CollectionInfo findOrMakeFolder(int id) {
-        // See if a placeholder was created for us already
-        CollectionInfo collectionInfo = collections.get(id);
-        if (collectionInfo == null) {
-            // No placeholder -- create a new blank folder instance. At this point, we don't know
-            // if the desired container is supposed to be a folder or an app pair. In the case that
-            // it is an app pair, the blank folder will be replaced by a blank app pair when the app
-            // pair is getting processed, in WorkspaceItemProcessor.processFolderOrAppPair().
-            collectionInfo = new FolderInfo();
-            collections.put(id, collectionInfo);
-        }
-        return collectionInfo;
-    }
-
-    /**
      * Clear all the deep shortcut counts for the given package, and re-add the new shortcut counts.
      */
     public synchronized void updateDeepShortcutCounts(
@@ -424,16 +320,6 @@
     }
 
     /**
-     * Returns a list containing all workspace items including widgets.
-     */
-    public synchronized ArrayList<ItemInfo> getAllWorkspaceItems() {
-        ArrayList<ItemInfo> items = new ArrayList<>(workspaceItems.size() + appWidgets.size());
-        items.addAll(workspaceItems);
-        items.addAll(appWidgets);
-        return items;
-    }
-
-    /**
      * Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted
      * items and dynamic/predicted items for the provided {@code userHandle}.
      * Note the call is not synchronized over the model, that should be handled by the called.
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
index aa62c32..6ad52ea 100644
--- a/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
+++ b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
@@ -30,7 +30,6 @@
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
-import com.android.launcher3.pm.InstallSessionHelper
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
@@ -80,21 +79,22 @@
         packageManagerHelper: PackageManagerHelper,
         firstScreenItems: List<ItemInfo>,
         userKeyToSessionMap: Map<PackageUserKey, SessionInfo>,
-        allWidgets: List<LauncherAppWidgetInfo>
+        allWidgets: List<ItemInfo>,
     ): List<FirstScreenBroadcastModel> {
 
         // installers for installing items
-        val pendingItemInstallerMap: Map<String, MutableSet<String>> =
+        val pendingItemInstallerMap: Map<String, Set<String>> =
             createPendingItemsMap(userKeyToSessionMap)
+
         val installingPackages = pendingItemInstallerMap.values.flatten().toSet()
 
         // installers for installed items on first screen
-        val installedItemInstallerMap: Map<String, MutableSet<ItemInfo>> =
+        val installedItemInstallerMap: Map<String, List<ItemInfo>> =
             createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper)
 
         // installers for widgets on all screens
-        val allInstalledWidgetsMap: Map<String, MutableSet<LauncherAppWidgetInfo>> =
-            createAllInstalledWidgetsMap(allWidgets, installingPackages, packageManagerHelper)
+        val allInstalledWidgetsMap: Map<String, List<ItemInfo>> =
+            createInstalledItemsMap(allWidgets, installingPackages, packageManagerHelper)
 
         val allInstallers: Set<String> =
             pendingItemInstallerMap.keys +
@@ -131,39 +131,39 @@
                             context,
                             0 /* requestCode */,
                             Intent(),
-                            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
-                        )
+                            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
+                        ),
                     )
                     .putStringArrayListExtra(
                         PENDING_COLLECTION_ITEM_EXTRA,
-                        ArrayList(model.pendingCollectionItems)
+                        ArrayList(model.pendingCollectionItems),
                     )
                     .putStringArrayListExtra(
                         PENDING_WORKSPACE_ITEM_EXTRA,
-                        ArrayList(model.pendingWorkspaceItems)
+                        ArrayList(model.pendingWorkspaceItems),
                     )
                     .putStringArrayListExtra(
                         PENDING_HOTSEAT_ITEM_EXTRA,
-                        ArrayList(model.pendingHotseatItems)
+                        ArrayList(model.pendingHotseatItems),
                     )
                     .putStringArrayListExtra(
                         PENDING_WIDGET_ITEM_EXTRA,
-                        ArrayList(model.pendingWidgetItems)
+                        ArrayList(model.pendingWidgetItems),
                     )
                     .putStringArrayListExtra(
                         INSTALLED_WORKSPACE_ITEMS_EXTRA,
-                        ArrayList(model.installedWorkspaceItems)
+                        ArrayList(model.installedWorkspaceItems),
                     )
                     .putStringArrayListExtra(
                         INSTALLED_HOTSEAT_ITEMS_EXTRA,
-                        ArrayList(model.installedHotseatItems)
+                        ArrayList(model.installedHotseatItems),
                     )
                     .putStringArrayListExtra(
                         ALL_INSTALLED_WIDGETS_ITEM_EXTRA,
                         ArrayList(
                             model.firstScreenInstalledWidgets +
                                 model.secondaryScreenInstalledWidgets
-                        )
+                        ),
                     )
             context.sendBroadcast(intent)
         }
@@ -172,66 +172,46 @@
     /** Maps Installer packages to Set of app packages from install sessions */
     private fun createPendingItemsMap(
         userKeyToSessionMap: Map<PackageUserKey, SessionInfo>
-    ): Map<String, MutableSet<String>> {
+    ): Map<String, Set<String>> {
         val myUser = Process.myUserHandle()
-        val result = mutableMapOf<String, MutableSet<String>>()
-        userKeyToSessionMap.forEach { entry ->
-            if (!myUser.equals(InstallSessionHelper.getUserHandle(entry.value))) return@forEach
-            val installer = entry.value.installerPackageName
-            val appPackage = entry.value.appPackageName
-            if (installer.isNullOrEmpty() || appPackage.isNullOrEmpty()) return@forEach
-            result.getOrPut(installer) { mutableSetOf() }.add(appPackage)
-        }
-        return result
-    }
-
-    /**
-     * Maps Installer packages to Set of ItemInfo from first screen. Filter out installing packages.
-     */
-    private fun createInstalledItemsMap(
-        firstScreenItems: List<ItemInfo>,
-        installingPackages: Set<String>,
-        packageManagerHelper: PackageManagerHelper
-    ): Map<String, MutableSet<ItemInfo>> {
-        val result = mutableMapOf<String, MutableSet<ItemInfo>>()
-        firstScreenItems.forEach { item ->
-            val appPackage = getPackageName(item) ?: return@forEach
-            if (installingPackages.contains(appPackage)) return@forEach
-            val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
-            if (installer.isNullOrEmpty()) return@forEach
-            result.getOrPut(installer) { mutableSetOf() }.add(item)
-        }
-        return result
-    }
-
-    /**
-     * Maps Installer packages to Set of AppWidget packages installed on all screens. Filter out
-     * installing packages.
-     */
-    private fun createAllInstalledWidgetsMap(
-        allWidgets: List<LauncherAppWidgetInfo>,
-        installingPackages: Set<String>,
-        packageManagerHelper: PackageManagerHelper
-    ): Map<String, MutableSet<LauncherAppWidgetInfo>> {
-        val result = mutableMapOf<String, MutableSet<LauncherAppWidgetInfo>>()
-        allWidgets
-            .sortedBy { widget -> widget.screenId }
-            .forEach { widget ->
-                val appPackage = getPackageName(widget) ?: return@forEach
-                if (installingPackages.contains(appPackage)) return@forEach
-                val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
-                if (installer.isNullOrEmpty()) return@forEach
-                result.getOrPut(installer) { mutableSetOf() }.add(widget)
+        return userKeyToSessionMap.values
+            .filter {
+                it.user == myUser &&
+                    !it.installerPackageName.isNullOrEmpty() &&
+                    !it.appPackageName.isNullOrEmpty()
             }
-        return result
+            .groupBy(
+                keySelector = { it.installerPackageName },
+                valueTransform = { it.appPackageName },
+            )
+            .mapValues { it.value.filterNotNull().toSet() } as Map<String, Set<String>>
     }
 
+    /** Maps Installer packages to Set of ItemInfos. Filter out installing packages. */
+    private fun createInstalledItemsMap(
+        allItems: Iterable<ItemInfo>,
+        installingPackages: Set<String>,
+        packageManagerHelper: PackageManagerHelper,
+    ): Map<String, List<ItemInfo>> =
+        allItems
+            .sortedBy { it.screenId }
+            .groupByTo(mutableMapOf()) {
+                getPackageName(it)?.let { pkg ->
+                    if (installingPackages.contains(pkg)) {
+                        null
+                    } else {
+                        packageManagerHelper.getAppInstallerPackage(pkg)
+                    }
+                }
+            }
+            .apply { remove(null) } as Map<String, List<ItemInfo>>
+
     /**
      * Add first screen Pending Items from Map to [FirstScreenBroadcastModel] for given installer
      */
     private fun FirstScreenBroadcastModel.addPendingItems(
         installingItems: Set<String>?,
-        firstScreenItems: List<ItemInfo>
+        firstScreenItems: List<ItemInfo>,
     ) {
         if (installingItems == null) return
         for (info in firstScreenItems) {
@@ -251,7 +231,7 @@
      */
     private fun FirstScreenBroadcastModel.addInstalledItems(
         installer: String,
-        installedItemInstallerMap: Map<String, Set<ItemInfo>>,
+        installedItemInstallerMap: Map<String, List<ItemInfo>>,
     ) {
         installedItemInstallerMap[installer]?.forEach { info ->
             val packageName: String = getPackageName(info) ?: return@forEach
@@ -265,7 +245,7 @@
     /** Add Widgets on every screen from Map to [FirstScreenBroadcastModel] for given installer */
     private fun FirstScreenBroadcastModel.addAllScreenWidgets(
         installer: String,
-        allInstalledWidgetsMap: Map<String, Set<LauncherAppWidgetInfo>>
+        allInstalledWidgetsMap: Map<String, List<ItemInfo>>,
     ) {
         allInstalledWidgetsMap[installer]?.forEach { widget ->
             val packageName: String = getPackageName(widget) ?: return@forEach
@@ -279,7 +259,7 @@
 
     private fun FirstScreenBroadcastModel.addCollectionItems(
         info: ItemInfo,
-        installingPackages: Set<String>
+        installingPackages: Set<String>,
     ) {
         if (info !is CollectionInfo) return
         pendingCollectionItems.addAll(
@@ -336,7 +316,7 @@
             Log.d(
                 TAG,
                 "Sending First Screen Broadcast for installer=$installerPackage" +
-                    ", total packages=${getTotalItemCount()}"
+                    ", total packages=${getTotalItemCount()}",
             )
             pendingCollectionItems.forEach {
                 Log.d(TAG, "$installerPackage:Pending Collection item:$it")
@@ -361,15 +341,7 @@
         }
     }
 
-    private fun getPackageName(info: ItemInfo): String? {
-        var packageName: String? = null
-        if (info is LauncherAppWidgetInfo) {
-            info.providerName?.let { packageName = info.providerName.packageName }
-        } else if (info.targetComponent != null) {
-            packageName = info.targetComponent?.packageName
-        }
-        return packageName
-    }
+    private fun getPackageName(info: ItemInfo): String? = info.targetComponent?.packageName
 
     /**
      * Clone the provided list on UI thread. This is used for [FolderInfo.getContents] which is
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 536d4c9..1623881 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -16,6 +16,11 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
 import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
@@ -48,6 +53,8 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.CollectionInfo;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -84,6 +91,11 @@
     private final IntArray mRestoredRows = new IntArray();
     private final IntSparseArrayMap<GridOccupancy> mOccupied = new IntSparseArrayMap<>();
 
+    // CollectionInfo objects, which have not yet been loaded from the DB, but are expected to
+    // found eventually as the loading progresses
+    private final IntSparseArrayMap<CollectionInfo> mPendingCollectionInfo =
+            new IntSparseArrayMap<>();
+
     private final int mIconIndex;
     public final int mTitleIndex;
 
@@ -479,8 +491,26 @@
         info.cellY = getInt(mCellYIndex);
     }
 
-    public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
-        checkAndAddItem(info, dataModel, null);
+    /**
+     * Return an existing FolderInfo object if we have encountered this ID previously,
+     * or make a new one.
+     */
+    public CollectionInfo findOrMakeFolder(int id, BgDataModel dataModel) {
+        // See if a placeholder was created for us already
+        ItemInfo info = dataModel.itemsIdMap.get(id);
+        if (info instanceof CollectionInfo c) return c;
+
+        CollectionInfo pending = mPendingCollectionInfo.get(id);
+        if (pending != null) return pending;
+
+        // No placeholder -- create a new blank folder instance. At this point, we don't know
+        // if the desired container is supposed to be a folder or an app pair. In the case that
+        // it is an app pair, the blank folder will be replaced by a blank app pair when the app
+        // pair is getting processed, in WorkspaceItemProcessor.processFolderOrAppPair().
+        pending = new FolderInfo();
+        pending.id = id;
+        mPendingCollectionInfo.put(id, pending);
+        return pending;
     }
 
     /**
@@ -495,7 +525,21 @@
             ShortcutKey.fromItemInfo(info);
         }
         if (checkItemPlacement(info, dataModel.isFirstPagePinnedItemEnabled)) {
-            dataModel.addItem(mContext, info, false, logger);
+            if (logger != null) {
+                logger.addLog(
+                        Log.DEBUG,
+                        TAG,
+                        String.format("Adding item to ID map: %s", info),
+                        /* stackTrace= */ null);
+            }
+            dataModel.addItem(mContext, info, false);
+            if ((info.itemType == ITEM_TYPE_APP_PAIR
+                    || info.itemType == ITEM_TYPE_DEEP_SHORTCUT
+                    || info.itemType == ITEM_TYPE_APPLICATION)
+                    && info.container != CONTAINER_DESKTOP
+                    && info.container != CONTAINER_HOTSEAT) {
+                findOrMakeFolder(info.container, dataModel).add(info);
+            }
             if (mRestoreEventLogger != null) {
                 mRestoreEventLogger.logSingleFavoritesItemRestored(itemType);
             }
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 44b7e8b..fee9696 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -31,7 +31,8 @@
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
-import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
+import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
@@ -82,7 +83,6 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.AppPairInfo;
-import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -210,10 +210,10 @@
         final int firstScreen = allScreens.get(0);
         IntSet firstScreens = IntSet.wrap(firstScreen);
 
-        ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
-        ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
-        filterCurrentWorkspaceItems(firstScreens, allItems, firstScreenItems,
-                new ArrayList<>() /* otherScreenItems are ignored */);
+        List<ItemInfo> firstScreenItems =
+                mBgDataModel.itemsIdMap.stream()
+                        .filter(currentScreenContentFilter(firstScreens))
+                        .toList();
         final int disableArchivingLauncherBroadcast = Settings.Secure.getInt(
                 mApp.getContext().getContentResolver(),
                 "disable_launcher_broadcast_installed_apps",
@@ -227,7 +227,7 @@
                             mPmHelper,
                             firstScreenItems,
                             mInstallingPkgsCached,
-                            mBgDataModel.appWidgets
+                            mBgDataModel.itemsIdMap.stream().filter(WIDGET_FILTER).toList()
                     );
             logASplit("Sending first screen broadcast with additional archiving Extras");
             FirstScreenBroadcastHelper.sendBroadcastsForModels(mApp.getContext(), broadcastModels);
@@ -523,14 +523,13 @@
      * requests high-res icons for the items that are part of an app pair.
      */
     private void processAppPairItems() {
-        for (CollectionInfo collection : mBgDataModel.collections) {
-            if (!(collection instanceof AppPairInfo appPair)) {
-                continue;
-            }
-
-            appPair.getContents().sort(Folder.ITEM_POS_COMPARATOR);
-            appPair.fetchHiResIconsIfNeeded(mIconCache);
-        }
+        mBgDataModel.itemsIdMap.stream()
+                .filter(item -> item instanceof AppPairInfo)
+                .forEach(item -> {
+                    AppPairInfo appPair = (AppPairInfo) item;
+                    appPair.getContents().sort(Folder.ITEM_POS_COMPARATOR);
+                    appPair.fetchHiResIconsIfNeeded(mIconCache);
+                });
     }
 
     /**
@@ -586,8 +585,8 @@
         // Sort the folder items, update ranks, and make sure all preview items are high res.
         List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
                 .stream().map(FolderGridOrganizer::createFolderGridOrganizer).toList();
-        for (CollectionInfo collection : mBgDataModel.collections) {
-            if (!(collection instanceof FolderInfo folder)) {
+        for (ItemInfo itemInfo : mBgDataModel.itemsIdMap) {
+            if (!(itemInfo instanceof FolderInfo folder)) {
                 continue;
             }
 
@@ -657,8 +656,6 @@
             IntArray deletedFolderIds = mApp.getModel().getModelDbController().deleteEmptyFolders();
             synchronized (mBgDataModel) {
                 for (int folderId : deletedFolderIds) {
-                    mBgDataModel.workspaceItems.remove(mBgDataModel.collections.get(folderId));
-                    mBgDataModel.collections.remove(folderId);
                     mBgDataModel.itemsIdMap.remove(folderId);
                 }
             }
@@ -676,8 +673,6 @@
 
         synchronized (mBgDataModel) {
             for (int id : deleted) {
-                mBgDataModel.workspaceItems.remove(mBgDataModel.collections.get(id));
-                mBgDataModel.collections.remove(id);
                 mBgDataModel.itemsIdMap.remove(id);
             }
         }
@@ -819,18 +814,19 @@
 
     private void loadFolderNames() {
         FolderNameProvider provider = FolderNameProvider.newInstance(mApp.getContext(),
-                mBgAllAppsList.data, mBgDataModel.collections);
+                mBgAllAppsList.data, FolderNameProvider.getCollectionForSuggestions(mBgDataModel));
 
         synchronized (mBgDataModel) {
-            for (int i = 0; i < mBgDataModel.collections.size(); i++) {
-                FolderNameInfos suggestionInfos = new FolderNameInfos();
-                CollectionInfo info = mBgDataModel.collections.valueAt(i);
-                if (info instanceof FolderInfo fi && fi.suggestedFolderNames == null) {
-                    provider.getSuggestedFolderName(mApp.getContext(), fi.getAppContents(),
-                            suggestionInfos);
-                    fi.suggestedFolderNames = suggestionInfos;
-                }
-            }
+            mBgDataModel.itemsIdMap.stream()
+                    .filter(item ->
+                            item instanceof FolderInfo fi && fi.suggestedFolderNames == null)
+                    .forEach(info -> {
+                        FolderInfo fi = (FolderInfo) info;
+                        FolderNameInfos suggestionInfos = new FolderNameInfos();
+                        provider.getSuggestedFolderName(mApp.getContext(), fi.getAppContents(),
+                                suggestionInfos);
+                        fi.suggestedFolderNames = suggestionInfos;
+                    });
         }
     }
 
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index 9e72e28..da79982 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -15,15 +15,15 @@
  */
 package com.android.launcher3.model;
 
-import com.android.launcher3.LauncherSettings;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
+
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.IntStream;
+import java.util.function.Predicate;
 
 /**
  * Utils class for {@link com.android.launcher3.LauncherModel}.
@@ -31,54 +31,17 @@
 public class ModelUtils {
 
     /**
-     * Filters the set of items who are directly or indirectly (via another container) on the
-     * specified screen.
+     * Returns a filter for items on hotseat or current screens
      */
-    public static <T extends ItemInfo> void filterCurrentWorkspaceItems(
-            final IntSet currentScreenIds,
-            List<? extends T> allWorkspaceItems,
-            List<T> currentScreenItems,
-            List<T> otherScreenItems) {
-        // Purge any null ItemInfos
-        allWorkspaceItems.removeIf(Objects::isNull);
-        // Order the set of items by their containers first, this allows use to walk through the
-        // list sequentially, build up a list of containers that are in the specified screen,
-        // as well as all items in those containers.
-        IntSet itemsOnScreen = new IntSet();
-        Collections.sort(allWorkspaceItems,
-                (lhs, rhs) -> Integer.compare(lhs.container, rhs.container));
-        for (T info : allWorkspaceItems) {
-            if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                if (currentScreenIds.contains(info.screenId)) {
-                    currentScreenItems.add(info);
-                    itemsOnScreen.add(info.id);
-                } else {
-                    otherScreenItems.add(info);
-                }
-            } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                currentScreenItems.add(info);
-                itemsOnScreen.add(info.id);
-            } else {
-                if (itemsOnScreen.contains(info.container)) {
-                    currentScreenItems.add(info);
-                    itemsOnScreen.add(info.id);
-                } else {
-                    otherScreenItems.add(info);
-                }
-            }
-        }
+    public static Predicate<ItemInfo> currentScreenContentFilter(IntSet currentScreenIds) {
+        return item -> item.container == CONTAINER_HOTSEAT
+                || (item.container == CONTAINER_DESKTOP
+                        && currentScreenIds.contains(item.screenId));
     }
 
     /**
-     * Iterates though current workspace items and returns available hotseat ranks for prediction.
+     * Returns a filter for widget items
      */
-    public static IntArray getMissingHotseatRanks(List<ItemInfo> items, int len) {
-        IntSet seen = new IntSet();
-        items.stream().filter(
-                info -> info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT)
-                .forEach(i -> seen.add(i.screenId));
-        IntArray result = new IntArray(len);
-        IntStream.range(0, len).filter(i -> !seen.contains(i)).forEach(result::add);
-        return result;
-    }
+    public static final Predicate<ItemInfo> WIDGET_FILTER = item ->
+            item.itemType == ITEM_TYPE_APPWIDGET || item.itemType == ITEM_TYPE_CUSTOM_APPWIDGET;
 }
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index b477cb1..0332775 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -459,37 +459,14 @@
                 if (item.container != Favorites.CONTAINER_DESKTOP &&
                         item.container != Favorites.CONTAINER_HOTSEAT) {
                     // Item is in a collection, make sure this collection exists
-                    if (!mBgDataModel.collections.containsKey(item.container)) {
+                    if (!(mBgDataModel.itemsIdMap.get(item.container) instanceof CollectionInfo)) {
                         // An items container is being set to a that of an item which is not in
-                        // the list of Folders.
+                        // the list of collections.
                         String msg = "item: " + item + " container being set to: " +
                                 item.container + ", not in the list of collections";
                         Log.e(TAG, msg);
                     }
                 }
-
-                // Items are added/removed from the corresponding FolderInfo elsewhere, such
-                // as in Workspace.onDrop. Here, we just add/remove them from the list of items
-                // that are on the desktop, as appropriate
-                ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId);
-                if (modelItem != null &&
-                        (modelItem.container == Favorites.CONTAINER_DESKTOP ||
-                                modelItem.container == Favorites.CONTAINER_HOTSEAT)) {
-                    switch (modelItem.itemType) {
-                        case Favorites.ITEM_TYPE_APPLICATION:
-                        case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                        case Favorites.ITEM_TYPE_FOLDER:
-                        case Favorites.ITEM_TYPE_APP_PAIR:
-                            if (!mBgDataModel.workspaceItems.contains(modelItem)) {
-                                mBgDataModel.workspaceItems.add(modelItem);
-                            }
-                            break;
-                        default:
-                            break;
-                    }
-                } else {
-                    mBgDataModel.workspaceItems.remove(modelItem);
-                }
                 mVerifier.verifyModel();
             }
         }
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index d238213..4103937 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
+
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -85,12 +87,16 @@
                 }
             });
 
-            for (LauncherAppWidgetInfo widget : dataModel.appWidgets) {
-                if (widget.providerName.getPackageName().equals(mInstallInfo.packageName)) {
-                    widget.installProgress = mInstallInfo.progress;
-                    updates.add(widget);
-                }
-            }
+            dataModel.itemsIdMap.stream()
+                    .filter(WIDGET_FILTER)
+                    .filter(item -> mInstallInfo.user.equals(item.user))
+                    .map(item -> (LauncherAppWidgetInfo) item)
+                    .filter(widget -> widget.providerName.getPackageName()
+                            .equals(mInstallInfo.packageName))
+                    .forEach(widget -> {
+                        widget.installProgress = mInstallInfo.progress;
+                        updates.add(widget);
+                    });
 
             if (!updates.isEmpty()) {
                 taskController.scheduleCallbackTask(
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index d619965..1153f48 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -18,7 +18,9 @@
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
+import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON;
 
@@ -347,24 +349,25 @@
                     }
                 });
 
-                for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) {
-                    if (mUser.equals(widgetInfo.user)
-                            && widgetInfo.hasRestoreFlag(
-                            LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
-                            && packageSet.contains(widgetInfo.providerName.getPackageName())) {
-                        widgetInfo.restoreStatus &=
-                                ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
-                                        & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
+                dataModel.itemsIdMap.stream()
+                        .filter(WIDGET_FILTER)
+                        .filter(item -> mUser.equals(item.user))
+                        .map(item -> (LauncherAppWidgetInfo) item)
+                        .filter(widget -> widget.hasRestoreFlag(FLAG_PROVIDER_NOT_READY)
+                                && packageSet.contains(widget.providerName.getPackageName()))
+                        .forEach(widgetInfo -> {
+                            widgetInfo.restoreStatus &=
+                                    ~FLAG_PROVIDER_NOT_READY
+                                            & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
 
-                        // adding this flag ensures that launcher shows 'click to setup'
-                        // if the widget has a config activity. In case there is no config
-                        // activity, it will be marked as 'restored' during bind.
-                        widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+                            // adding this flag ensures that launcher shows 'click to setup'
+                            // if the widget has a config activity. In case there is no config
+                            // activity, it will be marked as 'restored' during bind.
+                            widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
 
-                        widgets.add(widgetInfo);
-                        taskController.getModelWriter().updateItemInDatabase(widgetInfo);
-                    }
-                }
+                            widgets.add(widgetInfo);
+                            taskController.getModelWriter().updateItemInDatabase(widgetInfo);
+                        });
             }
 
             taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItems);
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index dad78dd..de1df2e 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -404,18 +404,14 @@
      * stored in the BgDataModel.
      */
     private fun processFolderOrAppPair() {
-        var collection = bgDataModel.findOrMakeFolder(c.id)
+        var collection = c.findOrMakeFolder(c.id, bgDataModel)
         // If we generated a placeholder Folder before this point, it may need to be replaced with
         // an app pair.
         if (c.itemType == Favorites.ITEM_TYPE_APP_PAIR && collection is FolderInfo) {
-            val folderInfo: FolderInfo = collection
             val newAppPair = AppPairInfo()
             // Move the placeholder's contents over to the new app pair.
-            folderInfo.getContents().forEach(newAppPair::add)
+            collection.getContents().forEach(newAppPair::add)
             collection = newAppPair
-            // Remove the placeholder and add the app pair into the data model.
-            bgDataModel.collections.remove(c.id)
-            bgDataModel.collections.put(c.id, collection)
         }
 
         c.applyCommonProperties(collection)
@@ -569,7 +565,7 @@
                 logWidgetInfo(app.invariantDeviceProfile, lapi)
             }
         }
-        c.checkAndAddItem(appWidgetInfo, bgDataModel)
+        c.checkAndAddItem(appWidgetInfo, bgDataModel, memoryLogger)
     }
 
     companion object {
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 772ea7f..7fb0152 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.model.data;
 
+import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
+
 import android.content.Context;
 import android.content.Intent;
 import android.os.Process;
@@ -23,6 +25,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Flags;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
 import com.android.launcher3.icons.FastBitmapDrawable;
@@ -320,6 +323,9 @@
      * Returns a FastBitmapDrawable with the icon and context theme applied
      */
     public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
+        if (!ThemeManager.INSTANCE.get(context).isMonoThemeEnabled()) {
+            creationFlags &= ~FLAG_THEMED;
+        }
         FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags);
         drawable.setIsDisabled(isDisabled());
         return drawable;
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 836ea4a..864fed0 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -170,6 +170,9 @@
                     for (NotificationsChangedListener listener : sNotificationsChangedListeners) {
                         listener.onNotificationPosted(msg.first, msg.second);
                     }
+                    Log.i(TAG, "received notification posted event - " + msg.first);
+                } else {
+                    Log.i(TAG, "received notification posted event, but there are no listeners");
                 }
                 break;
             case MSG_NOTIFICATION_REMOVED:
@@ -178,6 +181,9 @@
                     for (NotificationsChangedListener listener : sNotificationsChangedListeners) {
                         listener.onNotificationRemoved(msg.first, msg.second);
                     }
+                    Log.i(TAG, "received notification removed event - " + msg.first);
+                } else {
+                    Log.i(TAG, "received notification removed event, but there are no listeners");
                 }
                 break;
             case MSG_NOTIFICATION_FULL_REFRESH:
@@ -186,6 +192,11 @@
                         listener.onNotificationFullRefresh(
                                 (List<StatusBarNotification>) message.obj);
                     }
+                    ((List<StatusBarNotification>) message.obj).forEach(sbn -> Log.i(TAG,
+                            "Handling notification state refresh for " + sbn.getPackageName() + "#"
+                                    + sbn.getUserId()));
+                } else {
+                    Log.i(TAG, "received notification refresh event, but there are no listeners");
                 }
                 break;
         }
@@ -205,6 +216,7 @@
     @Override
     public void onListenerConnected() {
         super.onListenerConnected();
+        Log.i(TAG, "onListenerConnected");
         sIsConnected = true;
 
         // Register an observer to rebind the notification listener when dots are re-enabled.
@@ -230,6 +242,7 @@
     @Override
     public void onListenerDisconnected() {
         super.onListenerDisconnected();
+        Log.i(TAG, "onListenerDisconnected");
         sIsConnected = false;
         mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener);
         onNotificationFullRefresh();
diff --git a/src/com/android/launcher3/util/IntSparseArrayMap.java b/src/com/android/launcher3/util/IntSparseArrayMap.java
index 9d5391b..70f74e3 100644
--- a/src/com/android/launcher3/util/IntSparseArrayMap.java
+++ b/src/com/android/launcher3/util/IntSparseArrayMap.java
@@ -19,6 +19,8 @@
 import android.util.SparseArray;
 
 import java.util.Iterator;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 /**
  * Extension of {@link SparseArray} with some utility methods.
@@ -43,6 +45,10 @@
         return new ValueIterator();
     }
 
+    public Stream<E> stream() {
+        return StreamSupport.stream(spliterator(), false);
+    }
+
     @Thunk class ValueIterator implements Iterator<E> {
 
         private int mNextIndex = 0;
diff --git a/src/com/android/launcher3/util/LayoutImportExportHelper.kt b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
index 0df9dae..8559f3b 100644
--- a/src/com/android/launcher3/util/LayoutImportExportHelper.kt
+++ b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
@@ -56,7 +56,7 @@
 
         model.enqueueModelUpdateTask { _, dataModel, _ ->
             val builder = LauncherLayoutBuilder()
-            dataModel.workspaceItems.forEach { info ->
+            dataModel.itemsIdMap.forEach { info ->
                 val loc =
                     when (info.container) {
                         CONTAINER_DESKTOP ->
@@ -67,9 +67,6 @@
                     }
                 loc.addItem(context, info)
             }
-            dataModel.appWidgets.forEach { info ->
-                builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(context, info)
-            }
 
             val layoutXml = builder.build()
             callback(layoutXml)
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index c8d86d4..a6a6ceb 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -20,10 +20,17 @@
 import android.os.Process
 import android.os.UserManager
 import androidx.annotation.VisibleForTesting
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import javax.inject.Inject
 
-class LockedUserState(private val mContext: Context) : SafeCloseable {
+@LauncherAppSingleton
+class LockedUserState
+@Inject
+constructor(@ApplicationContext private val context: Context, lifeCycle: DaggerSingletonTracker) {
     val isUserUnlockedAtLauncherStartup: Boolean
     var isUserUnlocked = false
         private set(value) {
@@ -36,7 +43,7 @@
     private val mUserUnlockedActions: RunnableList = RunnableList()
 
     @VisibleForTesting
-    val mUserUnlockedReceiver =
+    val userUnlockedReceiver =
         SimpleBroadcastReceiver(UI_HELPER_EXECUTOR) {
             if (Intent.ACTION_USER_UNLOCKED == it.action) {
                 isUserUnlocked = true
@@ -53,8 +60,8 @@
         isUserUnlocked = checkIsUserUnlocked()
         isUserUnlockedAtLauncherStartup = isUserUnlocked
         if (!isUserUnlocked) {
-            mUserUnlockedReceiver.register(
-                mContext,
+            userUnlockedReceiver.register(
+                context,
                 {
                     // If user is unlocked while registering broadcast receiver, we should update
                     // [isUserUnlocked], which will call [notifyUserUnlocked] in setter
@@ -62,22 +69,18 @@
                         MAIN_EXECUTOR.execute { isUserUnlocked = true }
                     }
                 },
-                Intent.ACTION_USER_UNLOCKED
+                Intent.ACTION_USER_UNLOCKED,
             )
         }
+        lifeCycle.addCloseable { userUnlockedReceiver.unregisterReceiverSafely(context) }
     }
 
     private fun checkIsUserUnlocked() =
-        mContext.getSystemService(UserManager::class.java)!!.isUserUnlocked(Process.myUserHandle())
+        context.getSystemService(UserManager::class.java)!!.isUserUnlocked(Process.myUserHandle())
 
     private fun notifyUserUnlocked() {
         mUserUnlockedActions.executeAllAndDestroy()
-        mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
-    }
-
-    /** Stops the receiver from listening for ACTION_USER_UNLOCK broadcasts. */
-    override fun close() {
-        mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
+        userUnlockedReceiver.unregisterReceiverSafely(context)
     }
 
     /**
@@ -88,9 +91,7 @@
         mUserUnlockedActions.add(action)
     }
 
-    /**
-     * Removes a previously queued `Runnable` to be run when the user is unlocked.
-     */
+    /** Removes a previously queued `Runnable` to be run when the user is unlocked. */
     fun removeOnUserUnlockedRunnable(action: Runnable) {
         mUserUnlockedActions.remove(action)
     }
@@ -98,7 +99,7 @@
     companion object {
         @VisibleForTesting
         @JvmField
-        val INSTANCE = MainThreadInitializedObject { LockedUserState(it) }
+        val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getLockedUserState)
 
         @JvmStatic fun get(context: Context): LockedUserState = INSTANCE.get(context)
     }
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index 8fe6e93..fa183c8 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -34,11 +34,11 @@
 import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.dagger.LauncherBaseAppComponent;
 
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Function;
 
 import javax.inject.Inject;
 
@@ -57,7 +57,7 @@
  * Cache will also be updated if a key queried is missing (even if it has no listeners registered).
  */
 @LauncherAppSingleton
-public class SettingsCache extends ContentObserver implements SafeCloseable {
+public class SettingsCache extends ContentObserver {
 
     /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
     public static final Uri NOTIFICATION_BADGING_URI =
@@ -79,11 +79,17 @@
     private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString();
     private static final String GLOBAL_URI_PREFIX = Settings.Global.CONTENT_URI.toString();
 
+    private final Function<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMapper = uri -> {
+        registerUriAsync(uri);
+        return new CopyOnWriteArrayList<>();
+    };
+
     /**
      * Caches the last seen value for registered keys.
      */
-    private Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>();
-    private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>();
+    private final Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>();
+    private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap =
+            new ConcurrentHashMap<>();
     protected final ContentResolver mResolver;
 
     /**
@@ -96,12 +102,8 @@
     SettingsCache(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
         super(new Handler(Looper.getMainLooper()));
         mResolver = context.getContentResolver();
-        tracker.addCloseable(this);
-    }
-
-    @Override
-    public void close() {
-        UI_HELPER_EXECUTOR.execute(() -> mResolver.unregisterContentObserver(this));
+        tracker.addCloseable(() ->
+                UI_HELPER_EXECUTOR.execute(() -> mResolver.unregisterContentObserver(this)));
     }
 
     @Override
@@ -109,11 +111,12 @@
         // We use default of 1, but if we're getting an onChange call, can assume a non-default
         // value will exist
         boolean newVal = updateValue(uri, 1 /* Effectively Unused */);
-        if (!mListenerMap.containsKey(uri)) {
+        List<OnChangeListener> listeners = mListenerMap.get(uri);
+        if (listeners == null) {
             return;
         }
 
-        for (OnChangeListener listener : mListenerMap.get(uri)) {
+        for (OnChangeListener listener : listeners) {
             listener.onSettingsChanged(newVal);
         }
     }
@@ -138,22 +141,17 @@
         }
     }
 
+    private void registerUriAsync(Uri uri) {
+        UI_HELPER_EXECUTOR.execute(() -> mResolver.registerContentObserver(uri, false, this));
+    }
+
     /**
      * Does not de-dupe if you add same listeners for the same key multiple times.
      * Unregister once complete using {@link #unregister(Uri, OnChangeListener)}
      */
     @UiThread
     public void register(Uri uri, OnChangeListener changeListener) {
-        Preconditions.assertUIThread();
-        if (mListenerMap.containsKey(uri)) {
-            mListenerMap.get(uri).add(changeListener);
-        } else {
-            CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>();
-            l.add(changeListener);
-            mListenerMap.put(uri, l);
-            UI_HELPER_EXECUTOR.execute(
-                    () -> mResolver.registerContentObserver(uri, false, this));
-        }
+        mListenerMap.computeIfAbsent(uri, mListenerMapper).add(changeListener);
     }
 
     private boolean updateValue(Uri keyUri, int defaultValue) {
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index bb4f040..abb0081 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -73,7 +73,7 @@
                 }
             };
 
-    private final ActivityContext mActivityContext;
+    protected final ActivityContext mActivityContext;
     private final Handler mHandler = new Handler();
     private boolean mIsPointingUp;
     private Runnable mOnClosed;
@@ -103,16 +103,26 @@
                 R.dimen.arrow_toast_arrow_width);
         mArrowMinOffset = context.getResources().getDimensionPixelSize(
                 R.dimen.dynamic_grid_cell_border_spacing);
-        TypedArray ta = context.obtainStyledAttributes(R.styleable.ArrowTipView);
+        Context localContext = context;
+        TypedArray ta = localContext.obtainStyledAttributes(R.styleable.ArrowTipView);
         // Set style to default to avoid inflation issues with missing attributes.
         if (!ta.hasValue(R.styleable.ArrowTipView_arrowTipBackground)
                 || !ta.hasValue(R.styleable.ArrowTipView_arrowTipTextColor)) {
-            context = new ContextThemeWrapper(context, R.style.ArrowTipStyle);
+            localContext = new ContextThemeWrapper(localContext, R.style.ArrowTipStyle);
         }
-        mArrowViewPaintColor = ta.getColor(R.styleable.ArrowTipView_arrowTipBackground,
+        mArrowViewPaintColor = applyArrowPaintColor(ta, localContext);
+        init(localContext, layoutId);
+    }
+
+    protected int applyArrowPaintColor(TypedArray typedArray, Context context) {
+        int arrowPaintColor = typedArray.getColor(R.styleable.ArrowTipView_arrowTipBackground,
                 context.getColor(R.color.arrow_tip_view_bg));
-        ta.recycle();
-        init(context, layoutId);
+        typedArray.recycle();
+        return arrowPaintColor;
+    }
+
+    protected int getArrowId() {
+        return R.id.arrow;
     }
 
     @Override
@@ -154,7 +164,7 @@
         inflate(context, layoutId, this);
         setOrientation(LinearLayout.VERTICAL);
 
-        mArrowView = findViewById(R.id.arrow);
+        mArrowView = findViewById(getArrowId());
         updateArrowTipInView(mIsPointingUp);
         setAlpha(0);
 
@@ -343,6 +353,34 @@
             parent.addView(this);
             requestLayout();
         }
+        return showAtLocation(arrowXCoord, yCoordDownPointingTip, yCoordUpPointingTip,
+                minViewMargin, parentViewWidth, parentViewHeight, shouldAutoClose);
+    }
+
+    /**
+     * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
+     * cannot fit on screen in the requested orientation.
+     *
+     * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
+     *                    center of tooltip unless the tooltip goes beyond screen margin.
+     * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the
+     *                              tooltip is placed pointing downwards.
+     * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the
+     *                            tooltip is placed pointing upwards.
+     * @param minViewMargin The view margin in pixels from the tip end to the y coordinate.
+     * @param parentViewWidth The width in pixels of the parent view.
+     * @param parentViewHeight The height in pixels of the parent view.
+     * @param shouldAutoClose If Tooltip should be auto close.
+     * @return The tool tip view. {@code null} if the tip can not be shown.
+     */
+    protected ArrowTipView showAtLocation(
+            @Px int arrowXCoord,
+            @Px int yCoordDownPointingTip,
+            @Px int yCoordUpPointingTip,
+            @Px int minViewMargin,
+            @Px int parentViewWidth,
+            @Px int parentViewHeight,
+            boolean shouldAutoClose) {
 
         post(() -> {
             // Adjust the tooltip horizontally.
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index d850fc6..ab0f9a7 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -577,14 +577,11 @@
     public void exitSearchMode() {
         if (!mIsInSearchMode) return;
         onSearchResults(new ArrayList<>());
-        WidgetsRecyclerView searchRecyclerView = mAdapters.get(
-                AdapterHolder.SEARCH).mWidgetsRecyclerView;
         // Remove all views when exiting the search mode; this prevents animating from stale results
         // to new ones the next time we enter search mode. By the time recycler view is hidden,
         // layout may not have happened to clear up existing results. So, instead of waiting for it
         // to happen, we clear the views here.
-        searchRecyclerView.swapAdapter(
-                searchRecyclerView.getAdapter(), /*removeAndRecycleExistingViews=*/ true);
+        mAdapters.get(AdapterHolder.SEARCH).reset();
         setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
         if (mHasWorkProfile) {
             mViewPager.snapToPage(AdapterHolder.PRIMARY);
@@ -613,13 +610,12 @@
             mNoWidgetsView.setVisibility(GONE);
         } else {
             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
-            mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility(
-                    VISIBLE);
-            if (mRecommendedWidgetsCount > 0) {
-                // Display recommendations immediately, if present, so that other parts of sticky
-                // header (e.g. personal / work tabs) don't flash in interim.
-                mWidgetRecommendationsContainer.setVisibility(VISIBLE);
-            }
+            AdapterHolder currentAdapterHolder = mAdapters.get(getCurrentAdapterHolderType());
+            // Remove all views when exiting the search mode; this prevents animating / flashing old
+            // list position / state.
+            currentAdapterHolder.reset();
+            currentAdapterHolder.mWidgetsRecyclerView.setVisibility(VISIBLE);
+            post(this::onRecommendedWidgetsBound);
             // Visibility of recycler views and headers are handled in methods below.
             onWidgetsBound();
         }
@@ -1126,6 +1122,21 @@
             mWidgetsListItemAnimator = new WidgetsListItemAnimator();
         }
 
+        /**
+         * Swaps the adapter to existing adapter to prevent the recycler view from using stale view
+         * to animate in the new visibility update.
+         *
+         * <p>For instance, when clearing search text and re-entering search with new list shouldn't
+         * use stale results to animate in new results. Alternative is setting list animators to
+         * null, but, we need animations with the default item animator.
+         */
+        private void reset() {
+            mWidgetsRecyclerView.swapAdapter(
+                    mWidgetsListAdapter,
+                    /*removeAndRecycleExistingViews=*/ true
+            );
+        }
+
         private int getEmptySpaceHeight() {
             return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0;
         }
diff --git a/src_no_quickstep/com/android/launcher3/dagger/Modules.kt b/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
index dab33a0..c3bf7c5 100644
--- a/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
+++ b/src_no_quickstep/com/android/launcher3/dagger/Modules.kt
@@ -25,3 +25,8 @@
 @Module abstract class ApiWrapperModule {}
 
 @Module abstract class PluginManagerWrapperModule {}
+
+@Module object StaticObjectModule {}
+
+// Module containing bindings for the final derivative app
+@Module abstract class AppModule {}
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index 553d08c..15accbd 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -17,11 +17,14 @@
 package com.android.launcher3.folder
 
 import android.R
-import android.graphics.Bitmap
 import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.graphics.PreloadIconDrawable
 import com.android.launcher3.graphics.ThemeManager
 import com.android.launcher3.icons.BitmapInfo
@@ -30,13 +33,14 @@
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver
 import com.android.launcher3.icons.PlaceHolderIconDrawable
 import com.android.launcher3.icons.UserBadgeDrawable
-import com.android.launcher3.icons.mono.MonoThemedBitmap
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED
 import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.AllModulesForTest
 import com.android.launcher3.util.Executors
+import com.android.launcher3.util.FakePrefsModule
 import com.android.launcher3.util.FlagOp
 import com.android.launcher3.util.LauncherLayoutBuilder
 import com.android.launcher3.util.LauncherModelHelper
@@ -44,10 +48,19 @@
 import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.UserIconInfo
 import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
+import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.Description
 import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.doReturn
@@ -61,6 +74,8 @@
 @RunWith(AndroidJUnit4::class)
 class PreviewItemManagerTest {
 
+    @get:Rule val theseStateRule = ThemeStateRule()
+
     private lateinit var previewItemManager: PreviewItemManager
     private lateinit var context: SandboxModelContext
     private lateinit var folderItems: ArrayList<WorkspaceItemInfo>
@@ -68,15 +83,14 @@
     private lateinit var folderIcon: FolderIcon
     private lateinit var iconCache: IconCache
 
-    private var defaultThemedIcons = false
-
-    private val themeManager: ThemeManager
-        get() = ThemeManager.INSTANCE.get(context)
-
     @Before
     fun setup() {
         modelHelper = LauncherModelHelper()
         context = modelHelper.sandboxContext
+        context.initDaggerComponent(DaggerPreviewItemManagerTestComponent.builder())
+        theseStateRule.themeState?.let {
+            LauncherPrefs.get(context).putSync(ThemeManager.THEMED_ICONS.to(it))
+        }
         folderIcon = FolderIcon(ActivityContextWrapper(context))
 
         val app = spy(LauncherAppState.getInstance(context))
@@ -99,27 +113,16 @@
             )
             .loadModelSync()
 
+        folderIcon.mInfo =
+            modelHelper.bgDataModel.itemsIdMap.find { it.itemType == ITEM_TYPE_FOLDER }
+                as FolderInfo
         // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps
-        folderItems = modelHelper.bgDataModel.collections.valueAt(0).getAppContents()
-        folderIcon.mInfo = modelHelper.bgDataModel.collections.valueAt(0) as FolderInfo
-        folderIcon.mInfo.getContents().addAll(folderItems)
-
-        // Set first icon to be themed.
-        folderItems[0].bitmap.themedBitmap =
-            MonoThemedBitmap(
-                folderItems[0].bitmap.icon,
-                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
-            )
+        folderItems = folderIcon.mInfo.getAppContents()
 
         // Set second icon to be non-themed.
         folderItems[1].bitmap.themedBitmap = null
 
         // Set third icon to be themed with badge.
-        folderItems[2].bitmap.themedBitmap =
-            MonoThemedBitmap(
-                folderItems[2].bitmap.icon,
-                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
-            )
         folderItems[2].bitmap =
             folderItems[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
 
@@ -127,20 +130,17 @@
         folderItems[3].bitmap =
             folderItems[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
         folderItems[3].bitmap.themedBitmap = null
-
-        defaultThemedIcons = themeManager.isMonoThemeEnabled
     }
 
     @After
     @Throws(Exception::class)
     fun tearDown() {
-        themeManager.isMonoThemeEnabled = defaultThemedIcons
         modelHelper.destroy()
     }
 
     @Test
+    @MonoThemeEnabled(true)
     fun checkThemedIconWithThemingOn_iconShouldBeThemed() {
-        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -149,8 +149,8 @@
     }
 
     @Test
+    @MonoThemeEnabled(false)
     fun checkThemedIconWithThemingOff_iconShouldNotBeThemed() {
-        themeManager.isMonoThemeEnabled = false
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[0])
@@ -159,8 +159,8 @@
     }
 
     @Test
+    @MonoThemeEnabled(true)
     fun checkUnthemedIconWithThemingOn_iconShouldNotBeThemed() {
-        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -169,8 +169,8 @@
     }
 
     @Test
+    @MonoThemeEnabled(false)
     fun checkUnthemedIconWithThemingOff_iconShouldNotBeThemed() {
-        themeManager.isMonoThemeEnabled = false
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[1])
@@ -179,8 +179,8 @@
     }
 
     @Test
+    @MonoThemeEnabled(true)
     fun checkThemedIconWithBadgeWithThemingOn_iconAndBadgeShouldBeThemed() {
-        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[2])
@@ -192,8 +192,8 @@
     }
 
     @Test
+    @MonoThemeEnabled(true)
     fun checkUnthemedIconWithBadgeWithThemingOn_badgeShouldBeThemed() {
-        themeManager.isMonoThemeEnabled = true
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -205,8 +205,8 @@
     }
 
     @Test
+    @MonoThemeEnabled(false)
     fun checkUnthemedIconWithBadgeWithThemingOff_iconAndBadgeShouldNotBeThemed() {
-        themeManager.isMonoThemeEnabled = false
         val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
 
         previewItemManager.setDrawable(drawingParams, folderItems[3])
@@ -278,3 +278,28 @@
     private fun profileFlagOp(type: Int) =
         UserIconInfo(Process.myUserHandle(), type).applyBitmapInfoFlags(FlagOp.NO_OP)
 }
+
+class ThemeStateRule : TestRule {
+
+    var themeState: Boolean? = null
+
+    override fun apply(base: Statement, description: Description): Statement {
+        themeState = description.getAnnotation(MonoThemeEnabled::class.java)?.value
+        return base
+    }
+}
+
+// Annotation for tests that need to be run with quickstep enabled and disabled.
+@Retention(RUNTIME)
+@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
+annotation class MonoThemeEnabled(val value: Boolean = false)
+
+@LauncherAppSingleton
+@Component(modules = [AllModulesForTest::class, FakePrefsModule::class])
+interface PreviewItemManagerTestComponent : LauncherAppComponent {
+
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        override fun build(): PreviewItemManagerTestComponent
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index 1e2431f..0ae4d00 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
 
@@ -42,6 +44,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 /**
  * Tests for layout parser for remote layout
  */
@@ -63,14 +67,23 @@
         mModelHelper.destroy();
     }
 
+    private List<ItemInfo> getWorkspaceItems() {
+        return mModelHelper
+                .getBgDataModel()
+                .itemsIdMap
+                .stream()
+                .filter(i -> i.container == CONTAINER_DESKTOP || i.container == CONTAINER_HOTSEAT)
+                .toList();
+    }
+
     @Test
     public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
         writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
                 .putApp(TEST_PACKAGE, TEST_ACTIVITY));
 
         // Verify one item in hotseat
-        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
-        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(1, getWorkspaceItems().size());
+        ItemInfo info = getWorkspaceItems().get(0);
         assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, info.itemType);
     }
@@ -84,8 +97,8 @@
                 .build());
 
         // Verify folder
-        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
-        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(1, getWorkspaceItems().size());
+        ItemInfo info = getWorkspaceItems().get(0);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
         assertEquals(3, ((FolderInfo) info).getContents().size());
     }
@@ -99,8 +112,8 @@
                 .build());
 
         // Verify folder
-        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
-        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(1, getWorkspaceItems().size());
+        ItemInfo info = getWorkspaceItems().get(0);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
         assertEquals(3, ((FolderInfo) info).getContents().size());
         assertEquals("CustomFolder", info.title.toString());
@@ -124,8 +137,8 @@
                 .putWidget(pendingAppPkg, "PlaceholderWidget", 2, 2));
 
         // Verify widget
-        assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
-        ItemInfo info = mModelHelper.getBgDataModel().appWidgets.get(0);
+        assertEquals(1, getWorkspaceItems().size());
+        ItemInfo info = getWorkspaceItems().get(0);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET, info.itemType);
         assertEquals(2, info.spanX);
         assertEquals(2, info.spanY);
@@ -138,8 +151,8 @@
                 .putShortcut(TEST_PACKAGE, "shortcut2"));
 
         // Verify one item in hotseat
-        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
-        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(1, getWorkspaceItems().size());
+        ItemInfo info = getWorkspaceItems().get(0);
         assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT, info.itemType);
     }
@@ -154,8 +167,8 @@
                 .build());
 
         // Verify folder
-        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
-        FolderInfo info = (FolderInfo) mModelHelper.getBgDataModel().workspaceItems.get(0);
+        assertEquals(1, getWorkspaceItems().size());
+        FolderInfo info = (FolderInfo) getWorkspaceItems().get(0);
         assertEquals(3, info.getContents().size());
 
         // Verify last icon
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
index e8f778f..f357487 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -18,8 +18,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
 import com.android.launcher3.icons.BitmapInfo
 import com.android.launcher3.icons.waitForUpdateHandlerToFinish
+import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.LauncherLayoutBuilder
@@ -149,11 +151,13 @@
         // Reload again with correct icon state
         app.model.forceReload()
         modelHelper.loadModelSync()
-        val collections = modelHelper.getBgDataModel().collections
-
-        assertThat(collections.size()).isEqualTo(1)
-        assertThat(collections.valueAt(0).getAppContents().size).isEqualTo(itemCount)
-        return collections.valueAt(0).getAppContents()
+        val collections =
+            modelHelper.bgDataModel.itemsIdMap
+                .filter { it.itemType == ITEM_TYPE_FOLDER }
+                .map { it as FolderInfo }
+        assertThat(collections.size).isEqualTo(1)
+        assertThat(collections[0].getAppContents().size).isEqualTo(itemCount)
+        return collections[0].getAppContents()
     }
 
     private fun verifyHighRes(items: ArrayList<WorkspaceItemInfo>, vararg indices: Int) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index d699eee..da87dfc 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -494,8 +494,7 @@
     @Test
     fun `When processing Folder then create FolderInfo and mark restored`() {
         val actualFolderInfo = FolderInfo()
-        mockBgDataModel =
-            mock<BgDataModel>().apply { whenever(findOrMakeFolder(1)).thenReturn(actualFolderInfo) }
+        mockBgDataModel = mock<BgDataModel>()
         mockCursor =
             mock<LoaderCursor>().apply {
                 user = UserHandle(0)
@@ -509,6 +508,7 @@
                 whenever(getColumnIndex(Favorites.TITLE)).thenReturn(4)
                 whenever(getString(4)).thenReturn("title")
                 whenever(options).thenReturn(5)
+                whenever(findOrMakeFolder(eq(1), any())).thenReturn(actualFolderInfo)
             }
         val expectedFolderInfo =
             FolderInfo().apply {
@@ -600,7 +600,8 @@
 
         // Then
         val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
-        verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
+        verify(mockCursor)
+            .checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel), anyOrNull())
         val actualWidgetInfo = widgetInfoCaptor.value
         with(actualWidgetInfo) {
             assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
@@ -655,7 +656,7 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).checkAndAddItem(any(), any())
+        verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt b/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
index 68da9ff..b66a9d3 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DaggerGraphs.kt
@@ -19,6 +19,8 @@
 import com.android.launcher3.FakeLauncherPrefs
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.dagger.ApiWrapperModule
+import com.android.launcher3.dagger.AppModule
+import com.android.launcher3.dagger.StaticObjectModule
 import com.android.launcher3.dagger.WindowManagerProxyModule
 import dagger.Binds
 import dagger.Module
@@ -31,11 +33,21 @@
 }
 
 /** All modules. We also exclude the plugin module from tests */
-@Module(includes = [ApiWrapperModule::class, WindowManagerProxyModule::class])
+@Module(
+    includes =
+        [
+            ApiWrapperModule::class,
+            WindowManagerProxyModule::class,
+            StaticObjectModule::class,
+            AppModule::class,
+        ]
+)
 class AllModulesForTest
 
 /** All modules except the WMProxy */
-@Module(includes = [ApiWrapperModule::class]) class AllModulesMinusWMProxy
+@Module(includes = [ApiWrapperModule::class, StaticObjectModule::class, AppModule::class])
+class AllModulesMinusWMProxy
 
 /** All modules except the ApiWrapper */
-@Module(includes = [WindowManagerProxyModule::class]) class AllModulesMinusApiWrapper
+@Module(includes = [WindowManagerProxyModule::class, StaticObjectModule::class, AppModule::class])
+class AllModulesMinusApiWrapper
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt
index 2711d7a..99f5a5b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt
@@ -22,7 +22,10 @@
 import android.os.UserManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,17 +41,24 @@
 
     private val userManager: UserManager = mock()
     private val context: Context = mock()
+    private val lifeCycle: DaggerSingletonTracker = mock()
 
     @Before
     fun setup() {
         whenever(context.getSystemService(UserManager::class.java)).thenReturn(userManager)
     }
 
+    @After
+    fun tearDown() {
+        UI_HELPER_EXECUTOR.submit {}.get()
+        MAIN_EXECUTOR.submit {}.get()
+    }
+
     @Test
     fun runOnUserUnlocked_runs_action_immediately_if_already_unlocked() {
         whenever(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(true)
         val action: Runnable = mock()
-        LockedUserState(context).runOnUserUnlocked(action)
+        LockedUserState(context, lifeCycle).runOnUserUnlocked(action)
         verify(action).run()
     }
 
@@ -56,23 +66,23 @@
     fun runOnUserUnlocked_waits_to_run_action_until_user_is_unlocked() {
         whenever(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(false)
         val action: Runnable = mock()
-        val state = LockedUserState(context)
+        val state = LockedUserState(context, lifeCycle)
         state.runOnUserUnlocked(action)
         // b/343530737
         verifyNoMoreInteractions(action)
-        state.mUserUnlockedReceiver.onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED))
+        state.userUnlockedReceiver.onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED))
         verify(action).run()
     }
 
     @Test
     fun isUserUnlocked_returns_true_when_user_is_unlocked() {
         whenever(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(true)
-        assertThat(LockedUserState(context).isUserUnlocked).isTrue()
+        assertThat(LockedUserState(context, lifeCycle).isUserUnlocked).isTrue()
     }
 
     @Test
     fun isUserUnlocked_returns_false_when_user_is_locked() {
         whenever(userManager.isUserUnlocked(Process.myUserHandle())).thenReturn(false)
-        assertThat(LockedUserState(context).isUserUnlocked).isFalse()
+        assertThat(LockedUserState(context, lifeCycle).isUserUnlocked).isFalse()
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
index efe7637..0da8891 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
@@ -48,6 +48,7 @@
 class SandboxApplication private constructor(private val base: SandboxApplicationWrapper) :
     SandboxModelContext(base), TestRule {
 
+    @JvmOverloads
     constructor(
         base: Context = ApplicationProvider.getApplicationContext()
     ) : this(SandboxApplicationWrapper(base))
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index d2229c4..f04688d 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -19,6 +19,10 @@
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.IS_FIRST_LOAD_AFTER_RESTORE
 import com.android.launcher3.LauncherPrefs.Companion.RESTORE_DEVICE
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.icons.cache.CachingLogic
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
@@ -155,9 +159,24 @@
                     widgetsFilterDataProvider,
                 )
                 .runSyncOnBackgroundThread()
-            Truth.assertThat(workspaceItems.size).isAtLeast(25)
-            Truth.assertThat(appWidgets.size).isAtLeast(7)
-            Truth.assertThat(collections.size()).isAtLeast(8)
+            Truth.assertThat(
+                    itemsIdMap
+                        .filter {
+                            it.container == CONTAINER_DESKTOP || it.container == CONTAINER_HOTSEAT
+                        }
+                        .size
+                )
+                .isAtLeast(32)
+            Truth.assertThat(itemsIdMap.filter { ModelUtils.WIDGET_FILTER.test(it) }.size)
+                .isAtLeast(7)
+            Truth.assertThat(
+                    itemsIdMap
+                        .filter {
+                            it.itemType == ITEM_TYPE_FOLDER || it.itemType == ITEM_TYPE_APP_PAIR
+                        }
+                        .size
+                )
+                .isAtLeast(8)
             Truth.assertThat(itemsIdMap.size()).isAtLeast(40)
             Truth.assertThat(widgetsModel.defaultWidgetsFilter).isNotNull()
         }
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
index d553f47..8db049c 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
@@ -58,6 +58,7 @@
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
 import org.mockito.Mockito.verify
 import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
@@ -220,7 +221,8 @@
             )
             .commit()
         val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
-        verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
+        verify(mockCursor)
+            .checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel), anyOrNull())
         val actualWidgetInfo = widgetInfoCaptor.value
         with(actualWidgetInfo) {
             assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
@@ -271,7 +273,7 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).checkAndAddItem(any(), any())
+        verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
 
     private fun createWorkspaceItemProcessorUnderTest(