Merge "Reduce the widget cell horizontal padding" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 72b3c27..b70490b 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -584,3 +584,10 @@
   description: "Changes mouse interaction behavior"
   bug: "388897603"
 }
+
+flag {
+  name: "enable_alt_tab_kqs_on_connected_displays"
+  namespace: "lse_desktop_experience"
+  description: "Enable Alt + Tab KQS support on connected displays"
+  bug: "394007677"
+}
diff --git a/quickstep/res/drawable/ic_close_option.xml b/quickstep/res/drawable/ic_close_option.xml
new file mode 100644
index 0000000..5681cb5
--- /dev/null
+++ b/quickstep/res/drawable/ic_close_option.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z" />
+</vector>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index a866bb9..281125d 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -46,8 +46,7 @@
     <string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"تم تفعيل ميزة \"التطبيقات المقترحة\"."</string>
     <string name="hotsaet_tip_prediction_disabled" msgid="1506426298884658491">"ميزة \"التطبيقات المقترحة\" غير مفعّلة."</string>
     <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"التطبيق المتوقع: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
-    <!-- no translation found for gesture_tutorial_title (2750751261768388354) -->
-    <skip />
+    <string name="gesture_tutorial_title" msgid="2750751261768388354">"دليل توجيهي للتنقُّل بالإيماءات"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"يُرجى تدوير الجهاز"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"يُرجى تدوير جهازك لإكمال الدليل التوجيهي للتنقُّل بالإيماءات."</string>
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"تأكَّد من التمرير سريعًا من أقصى الحافة اليسرى أو اليمنى."</string>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index bb56954..0af4d9c 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -89,7 +89,7 @@
     <string name="allset_title" msgid="5021126669778966707">"Valmis!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Avalehele liikumiseks pühkige üles"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Avakuvale liikumiseks puudutage avakuva nuppu"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> on nüüd kasutamiseks valmis"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Teie <xliff:g id="DEVICE">%1$s</xliff:g> on nüüd kasutamiseks valmis."</string>
     <string name="default_device_name" msgid="6660656727127422487">"seade"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Süsteemi navigeerimisseaded"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Jaga"</string>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index d2a7029..8e70a2b 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -30,6 +30,8 @@
     <string name="recent_task_option_desktop">Desktop</string>
     <!-- Title and content description for an option to move app to external display. -->
     <string name="recent_task_option_external_display">Move to external display</string>
+    <!-- Title and content description for an option to close the app [CHAR LIMIT=30] -->
+    <string name="recent_task_option_close">Close</string>
 
     <!-- Title and content description for Desktop tile in Recents screen that contains apps opened inside desktop windowing mode [CHAR LIMIT=NONE] -->
     <string name="recent_task_desktop">Desktop</string>
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index dc0f899..1cf7dda 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -23,6 +23,8 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import static java.lang.Math.max;
+import static java.lang.Math.min;
 import static java.util.Collections.emptyList;
 
 import android.appwidget.AppWidgetManager;
@@ -53,6 +55,7 @@
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetCategoryFilter;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
@@ -81,6 +84,10 @@
     // the intent, then widgets will not be filtered for size.
     private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
     private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
+    // Unlike the AppWidgetManager.EXTRA_CATEGORY_FILTER, this filter removes certain categories.
+    // Filter is ignore if it is not a negative value.
+    // Example usage: WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
+    private static final String EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter";
     /**
      * Widgets currently added by the user in the UI surface.
      * <p>This allows widget picker to exclude existing widgets from suggestions.</p>
@@ -120,7 +127,8 @@
 
     private int mDesiredWidgetWidth;
     private int mDesiredWidgetHeight;
-    private int mWidgetCategoryFilter;
+    private WidgetCategoryFilter mWidgetCategoryInclusionFilter;
+    private WidgetCategoryFilter mWidgetCategoryExclusionFilter;
     @Nullable
     private String mUiSurface;
     // Widgets existing on the host surface.
@@ -194,8 +202,19 @@
                 getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
 
         // Defaults to '0' to indicate that there isn't a category filter.
-        mWidgetCategoryFilter =
-                getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+        // Negative value indicates it's an exclusion filter (e.g. NOT_KEYGUARD_CATEGORY.inv())
+        // Positive value indicates it's inclusion filter (e.g. HOME_SCREEN or KEYGUARD)
+        // Note: A filter can either be inclusion or exclusion filter; not both.
+        int inclusionFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+        if (inclusionFilter < 0) {
+            Log.w(TAG, "Invalid EXTRA_CATEGORY_FILTER: " + inclusionFilter);
+        }
+        mWidgetCategoryInclusionFilter = new WidgetCategoryFilter(max(0, inclusionFilter));
+        int exclusionFilter = getIntent().getIntExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, 0);
+        if (exclusionFilter > 0) {
+            Log.w(TAG, "Invalid EXTRA_CATEGORY_EXCLUSION_FILTER: " + exclusionFilter);
+        }
+        mWidgetCategoryExclusionFilter = new WidgetCategoryFilter(min(0 , exclusionFilter));
 
         String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE);
         if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) {
@@ -436,11 +455,13 @@
                     widget.user.getIdentifier());
         }
 
-        if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
+        if (!mWidgetCategoryInclusionFilter.matches(info.widgetCategory)
+                || !mWidgetCategoryExclusionFilter.matches(info.widgetCategory)) {
             return rejectWidget(
                     widget,
-                    "doesn't match category filter [filter=%d, widget=%d]",
-                    mWidgetCategoryFilter,
+                    "doesn't match category filter [inclusion=%d, exclusion=%d, widget=%d]",
+                    mWidgetCategoryInclusionFilter.getCategoryMask(),
+                    mWidgetCategoryExclusionFilter.getCategoryMask(),
                     info.widgetCategory);
         }
 
@@ -463,7 +484,7 @@
                             mDesiredWidgetWidth);
                 }
 
-                final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
+                final int minWidth = min(info.minResizeWidth, info.minWidth);
                 if (minWidth > mDesiredWidgetWidth) {
                     return rejectWidget(
                             widget,
@@ -487,7 +508,7 @@
                             mDesiredWidgetHeight);
                 }
 
-                final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
+                final int minHeight = min(info.minResizeHeight, info.minHeight);
                 if (minHeight > mDesiredWidgetHeight) {
                     return rejectWidget(
                             widget,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 3736e6d..23065b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -233,7 +233,8 @@
     }
 
     private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
-        return Flags.taskbarOverflow() && taskIdsToExclude.contains(task.task1.key.id);
+        return Flags.taskbarOverflow() && task.getTasks().stream().anyMatch(
+                t -> taskIdsToExclude.contains(t.key.id));
     }
 
     private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 306443e..4581119 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -51,6 +51,9 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 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.system.InteractionJankMonitorWrapper;
 
 import java.util.HashMap;
@@ -255,17 +258,24 @@
                     layoutInflater,
                     previousTaskView);
 
-            final boolean firstTaskIsLeftTopTask =
-                    groupTask.mSplitBounds == null
-                            || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id
-                            || groupTask.task2 == null;
+            Task task1;
+            Task task2;
+            if (groupTask instanceof SplitTask splitTask) {
+                task1 = splitTask.getTopLeftTask();
+                task2 = splitTask.getBottomRightTask();
+            } else if (groupTask instanceof SingleTask singleTask) {
+                task1 = singleTask.getTask();
+                task2 = null;
+            } else {
+                continue;
+            }
 
             currentTaskView.setThumbnailsForSplitTasks(
-                    firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2,
-                    firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1,
+                    task1,
+                    task2,
                     updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
                     updateTasks ? mViewCallbacks::updateIconInBackground : null,
-                    groupTask.mSplitBounds);
+                    groupTask instanceof SplitTask splitTask ? splitTask.getSplitBounds() : null);
 
             previousTaskView = currentTaskView;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 8cb43d2..5af7ff8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
 import com.android.quickstep.util.SlideInRemoteTransition;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -281,9 +282,10 @@
             return -1;
         }
         RemoteTransition remoteTransition = slideInTransition;
-        if (mOnDesktop
-                && mControllers.taskbarActivityContext.canUnminimizeDesktopTask(task.task1.key.id)
-        ) {
+        boolean canUnminimizeDesktopTask = task instanceof SingleTask singleTask
+                && mControllers.taskbarActivityContext.canUnminimizeDesktopTask(
+                        singleTask.getTask().key.id);
+        if (mOnDesktop && canUnminimizeDesktopTask) {
             // This app is being unminimized - use our own transition runner.
             remoteTransition = new RemoteTransition(
                     new DesktopAppLaunchTransition(
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 5a8fba6..4143157 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -49,7 +49,7 @@
 import com.android.quickstep.HomeVisibilityState;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SplitTask;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -480,8 +480,8 @@
 
     @Override
     public void launchSplitTasks(
-            @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
-        mLauncher.launchSplitTasks(groupTask, remoteTransition);
+            @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) {
+        mLauncher.launchSplitTasks(splitTask, remoteTransition);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 8a06b11..3963d40 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -156,6 +156,8 @@
 import com.android.quickstep.SystemUiProxy;
 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.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -1294,11 +1296,13 @@
 
         mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
 
-        if (tag instanceof GroupTask groupTask) {
+        // TODO: b/316004172, b/343289567: Handle `DesktopTask` and `SplitTask`.
+        if (tag instanceof SingleTask singleTask) {
             RemoteTransition remoteTransition =
-                    (areDesktopTasksVisible() && canUnminimizeDesktopTask(groupTask.task1.key.id))
+                    (areDesktopTasksVisible() && canUnminimizeDesktopTask(
+                            singleTask.getTask().key.id))
                             ? createDesktopAppLaunchRemoteTransition(AppLaunchType.UNMINIMIZE,
-                                    Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
+                            Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
                             : null;
             if (areDesktopTasksVisible() && mControllers.uiController.isInOverviewUi()) {
                 RunnableList runnableList = recents.launchRunningDesktopTaskView();
@@ -1306,12 +1310,12 @@
                 // launch.
                 if (runnableList != null) {
                     runnableList.add(() -> UI_HELPER_EXECUTOR.execute(
-                            () -> handleGroupTaskLaunch(groupTask, remoteTransition,
+                            () -> handleGroupTaskLaunch(singleTask, remoteTransition,
                                     areDesktopTasksVisible(),
                                     DesktopTaskToFrontReason.TASKBAR_TAP)));
                 }
             } else {
-                handleGroupTaskLaunch(groupTask, remoteTransition, areDesktopTasksVisible(),
+                handleGroupTaskLaunch(singleTask, remoteTransition, areDesktopTasksVisible(),
                         DesktopTaskToFrontReason.TASKBAR_TAP);
             }
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1481,13 +1485,13 @@
                             remoteTransition));
             return;
         }
-        if (onDesktop) {
-            boolean useRemoteTransition = canUnminimizeDesktopTask(task.task1.key.id);
+        if (onDesktop && task instanceof SingleTask singleTask) {
+            boolean useRemoteTransition = canUnminimizeDesktopTask(singleTask.getTask().key.id);
             UI_HELPER_EXECUTOR.execute(() -> {
                 if (onStartCallback != null) {
                     onStartCallback.run();
                 }
-                SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id,
+                SystemUiProxy.INSTANCE.get(this).showDesktopApp(singleTask.getTask().key.id,
                         useRemoteTransition ? remoteTransition : null, toFrontReason);
                 if (onFinishCallback != null) {
                     onFinishCallback.run();
@@ -1495,18 +1499,19 @@
             });
             return;
         }
-        if (task.task2 == null) {
+        if (task instanceof SingleTask singleTask) {
             UI_HELPER_EXECUTOR.execute(() -> {
                 ActivityOptions activityOptions =
                         makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
                 activityOptions.setRemoteTransition(remoteTransition);
 
                 ActivityManagerWrapper.getInstance().startActivityFromRecents(
-                        task.task1.key, activityOptions);
+                        singleTask.getTask().key, activityOptions);
             });
             return;
         }
-        mControllers.uiController.launchSplitTasks(task, remoteTransition);
+        assert task instanceof SplitTask;
+        mControllers.uiController.launchSplitTasks((SplitTask) task, remoteTransition);
     }
 
     /** Returns whether the given task is minimized and can be unminimized. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index a9e8d6d..3a83db2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -80,9 +80,9 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.views.BubbleTextHolder;
-import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LogUtils;
 import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.util.SingleTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
 
@@ -433,8 +433,8 @@
                                 null, item.user));
             }
             intent.putExtra(Intent.EXTRA_USER, item.user);
-        } else if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
-            Task task = groupTask.task1;
+        } else if (tag instanceof SingleTask singleTask) {
+            Task task = singleTask.getTask();
             clipDescription = new ClipDescription(task.titleDescription,
                     new String[] {
                             ClipDescription.MIMETYPE_APPLICATION_TASK
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 17fb959..36185b1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -115,9 +115,8 @@
     private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
-    private final Context mWindowContext;
+    private final Context mParentContext;
     private final @Nullable Context mNavigationBarPanelContext;
-    private WindowManager mWindowManager;
     private final TaskbarNavButtonController mDefaultNavButtonController;
     private final ComponentCallbacks mDefaultComponentCallbacks;
 
@@ -132,6 +131,8 @@
             new NonDestroyableScopedUnfoldTransitionProgressProvider();
     /** DisplayId - {@link TaskbarActivityContext} map for Connected Display. */
     private final SparseArray<TaskbarActivityContext> mTaskbars = new SparseArray<>();
+    /** DisplayId - {@link Context} map for Connected Display. */
+    private final SparseArray<Context> mWindowContexts = new SparseArray<>();
     /** DisplayId - {@link FrameLayout} map for Connected Display. */
     private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
     /** DisplayId - {@link Boolean} map indicating if RootLayout was added to window. */
@@ -243,36 +244,35 @@
             Context context,
             AllAppsActionManager allAppsActionManager,
             TaskbarNavButtonCallbacks navCallbacks) {
-        Display display =
-                context.getSystemService(DisplayManager.class).getDisplay(context.getDisplayId());
-        mWindowContext = context.createWindowContext(display,
-                ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
-                null);
+        mParentContext = context;
+        createWindowContext(context.getDisplayId());
         mAllAppsActionManager = allAppsActionManager;
+        Display display = context.getSystemService(DisplayManager.class).getDisplay(
+                getDefaultDisplayId());
         mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 ? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
                 : null;
         if (enableTaskbarNoRecreate()) {
-            mWindowManager = mWindowContext.getSystemService(WindowManager.class);
             createTaskbarRootLayout(getDefaultDisplayId());
         }
         mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
         mDefaultComponentCallbacks = createDefaultComponentCallbacks();
-        SettingsCache.INSTANCE.get(mWindowContext)
+        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
                 .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
-        SettingsCache.INSTANCE.get(mWindowContext)
+        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
-        mWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.register(mWindowContext, Intent.ACTION_SHUTDOWN);
+        getPrimaryWindowContext().registerComponentCallbacks(mDefaultComponentCallbacks);
+        mShutdownReceiver.register(getPrimaryWindowContext(), Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
             mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
-                    mWindowContext,
+                    getPrimaryWindowContext(),
                     SYSTEM_ACTION_ID_TASKBAR,
-                    new Intent(ACTION_SHOW_TASKBAR).setPackage(mWindowContext.getPackageName()),
+                    new Intent(ACTION_SHOW_TASKBAR).setPackage(
+                            getPrimaryWindowContext().getPackageName()),
                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
             mTaskbarBroadcastReceiver.register(
-                    mWindowContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
+                    getPrimaryWindowContext(), RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
         });
 
         debugWhyTaskbarNotDestroyed("TaskbarManager created");
@@ -285,14 +285,15 @@
         return new TaskbarNavButtonController(
                 context,
                 navCallbacks,
-                SystemUiProxy.INSTANCE.get(mWindowContext),
+                SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()),
                 new Handler(),
-                new ContextualSearchInvoker(mWindowContext));
+                new ContextualSearchInvoker(getPrimaryWindowContext()));
     }
 
     private ComponentCallbacks createDefaultComponentCallbacks() {
         return new ComponentCallbacks() {
-            private Configuration mOldConfig = mWindowContext.getResources().getConfiguration();
+            private Configuration mOldConfig =
+                    getPrimaryWindowContext().getResources().getConfiguration();
 
             @Override
             public void onConfigurationChanged(Configuration newConfig) {
@@ -302,7 +303,8 @@
                         "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
                 // TODO: adapt this logic to be specific to different displays.
                 DeviceProfile dp = mUserUnlocked
-                        ? LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext)
+                        ? LauncherAppState.getIDP(getPrimaryWindowContext()).getDeviceProfile(
+                        getPrimaryWindowContext())
                         : null;
                 int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
 
@@ -354,6 +356,7 @@
             int displayId = mTaskbars.keyAt(i);
             destroyTaskbarForDisplay(displayId);
             removeTaskbarRootViewFromWindow(displayId);
+            removeWindowContextFromMap(displayId);
         }
     }
 
@@ -372,7 +375,8 @@
         }
         // make this display-specific
         DeviceProfile dp = mUserUnlocked ?
-                LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
+                LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
+                        getWindowContext(displayId)) : null;
         if (dp == null || !isTaskbarEnabled(dp)) {
             removeTaskbarRootViewFromWindow(displayId);
         }
@@ -419,7 +423,8 @@
      */
     public void onUserUnlocked() {
         mUserUnlocked = true;
-        DisplayController.INSTANCE.get(mWindowContext).addChangeListener(mRecreationListener);
+        DisplayController.INSTANCE.get(getPrimaryWindowContext()).addChangeListener(
+                mRecreationListener);
         recreateTaskbar();
         addTaskbarRootViewToWindow(getDefaultDisplayId());
     }
@@ -482,7 +487,8 @@
                 return ql.getUnfoldTransitionProgressProvider();
             }
         } else {
-            return SystemUiProxy.INSTANCE.get(mWindowContext).getUnfoldTransitionProvider();
+            return SystemUiProxy.INSTANCE.get(
+                    getPrimaryWindowContext()).getUnfoldTransitionProvider();
         }
         return null;
     }
@@ -528,7 +534,8 @@
             Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "recreateTaskbarForDisplay: " + displayId);
             // TODO: make this code display specific
             DeviceProfile dp = mUserUnlocked ?
-                    LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
+                    LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
+                            getWindowContext(displayId)) : null;
 
             // All Apps action is unrelated to navbar unification, so we only need to check DP.
             final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
@@ -542,7 +549,7 @@
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
                 + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
             if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
-                SystemUiProxy.INSTANCE.get(mWindowContext)
+                SystemUiProxy.INSTANCE.get(getPrimaryWindowContext())
                     .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
                 if (!isTaskbarEnabled) {
                     return;
@@ -754,23 +761,24 @@
         mRecentsViewContainer = null;
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
-        mTaskbarBroadcastReceiver.unregisterReceiverSafely(mWindowContext);
-        destroyAllTaskbars();
+        mTaskbarBroadcastReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+
         if (mUserUnlocked) {
-            DisplayController.INSTANCE.get(mWindowContext).removeChangeListener(
+            DisplayController.INSTANCE.get(getPrimaryWindowContext()).removeChangeListener(
                     mRecreationListener);
         }
-        SettingsCache.INSTANCE.get(mWindowContext)
+        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
                 .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
-        SettingsCache.INSTANCE.get(mWindowContext)
+        SettingsCache.INSTANCE.get(getPrimaryWindowContext())
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        mWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.unregisterReceiverSafely(mWindowContext);
+        getPrimaryWindowContext().unregisterComponentCallbacks(mDefaultComponentCallbacks);
+        mShutdownReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+        destroyAllTaskbars();
     }
 
     public @Nullable TaskbarActivityContext getCurrentActivityContext() {
-        return getTaskbarForDisplay(mWindowContext.getDisplayId());
+        return getTaskbarForDisplay(getDefaultDisplayId());
     }
 
     public void dumpLogs(String prefix, PrintWriter pw) {
@@ -786,7 +794,6 @@
                 taskbar.dumpLogs(prefix + "\t\t", pw);
             }
         }
-
     }
 
     private void addTaskbarRootViewToWindow(int displayId) {
@@ -800,8 +807,7 @@
         if (!isTaskbarRootLayoutAddedForDisplay(displayId)) {
             FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
             if (rootLayout != null) {
-                mWindowManager.addView(getTaskbarRootLayoutForDisplay(displayId),
-                        taskbar.getWindowLayoutParams());
+                getWindowManager(displayId).addView(rootLayout, taskbar.getWindowLayoutParams());
                 mAddedRootLayouts.put(displayId, true);
             } else {
                 Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW,
@@ -822,7 +828,7 @@
         }
 
         if (isTaskbarRootLayoutAddedForDisplay(displayId)) {
-            mWindowManager.removeViewImmediate(rootLayout);
+            getWindowManager(displayId).removeViewImmediate(rootLayout);
             mAddedRootLayouts.put(displayId, false);
             removeTaskbarRootLayoutFromMap(displayId);
         }
@@ -855,10 +861,10 @@
      * Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
      */
     private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
-        TaskbarActivityContext newTaskbar = new TaskbarActivityContext(mWindowContext,
+        TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
                 mNavigationBarPanelContext, dp, mDefaultNavButtonController,
                 mUnfoldProgressProvider, isDefaultDisplay(displayId),
-                SystemUiProxy.INSTANCE.get(mWindowContext));
+                SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()));
 
         addTaskbarToMap(displayId, newTaskbar);
         return newTaskbar;
@@ -892,7 +898,7 @@
      */
     private void createTaskbarRootLayout(int displayId) {
         Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "createTaskbarRootLayout: " + displayId);
-        FrameLayout newTaskbarRootLayout = new FrameLayout(mWindowContext) {
+        FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) {
             @Override
             public boolean dispatchTouchEvent(MotionEvent ev) {
                 // The motion events can be outside the view bounds of task bar, and hence
@@ -958,8 +964,75 @@
         Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "mRootLayouts.size()=" + mRootLayouts.size());
     }
 
+    /**
+     * Creates {@link Context} for the taskbar on the specified display and›› adds it to map.
+     * @param displayId The ID of the display for which to create the window context.
+     */
+    private void createWindowContext(int displayId) {
+        DisplayManager displayManager = mParentContext.getSystemService(DisplayManager.class);
+        if (displayManager == null) {
+            return;
+        }
+
+        Display display = displayManager.getDisplay(displayId);
+        if (display != null) {
+            int windowType = (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId))
+                    ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL;
+            Context newContext = mParentContext.createWindowContext(display, windowType, null);
+            addWindowContextToMap(displayId, newContext);
+        }
+    }
+
+    /**
+     * Retrieves the window context of the taskbar for the specified display.
+     *
+     * @param displayId The ID of the display for which to retrieve the window context.
+     * @return The Window Context {@link Context} for a given display or {@code null}.
+     */
+    private Context getWindowContext(int displayId) {
+        return mWindowContexts.get(displayId);
+    }
+
+    @VisibleForTesting
+    public Context getPrimaryWindowContext() {
+        return getWindowContext(getDefaultDisplayId());
+    }
+
+    /**
+     * Retrieves the window manager {@link WindowManager} of the taskbar for the specified display.
+     *
+     * @param displayId The ID of the display for which to retrieve the window manager.
+     * @return The window manager {@link WindowManager} for a given display or {@code null}.
+     */
+    private WindowManager getWindowManager(int displayId) {
+        return getWindowContext(displayId).getSystemService(WindowManager.class);
+    }
+
+    /**
+     * Adds the window context {@link Context} to taskbar map, mapped to display ID.
+     *
+     * @param displayId The ID of the display to associate with the taskbar root layout.
+     * @param windowContext The window context {@link Context} to add to the map.
+     */
+    private void addWindowContextToMap(int displayId, @NonNull Context windowContext) {
+        if (!mWindowContexts.contains(displayId)) {
+            mWindowContexts.put(displayId, windowContext);
+        }
+    }
+
+    /**
+     * Removes the window context {@link Context} for given display ID from the taskbar map.
+     *
+     * @param displayId The ID of the display for which to remove the taskbar root layout.
+     */
+    private void removeWindowContextFromMap(int displayId) {
+        if (mWindowContexts.contains(displayId)) {
+            mWindowContexts.delete(displayId);
+        }
+    }
+
     private int getDefaultDisplayId() {
-        return mWindowContext.getDisplayId();
+        return mParentContext.getDisplayId();
     }
 
     /** Temp logs for b/254119092. */
@@ -974,9 +1047,14 @@
 
         boolean activityTaskbarPresent = mActivity != null
                 && mActivity.getDeviceProfile().isTaskbarPresent;
-        // TODO: make this display specific
-        boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(mWindowContext)
-                .getDeviceProfile(mWindowContext).isTaskbarPresent;
+        Context windowContext = getWindowContext(displayId);
+        if (windowContext == null) {
+            log.add("window context for displayId" + displayId);
+            return;
+        }
+
+        boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(windowContext)
+                .getDeviceProfile(windowContext).isTaskbarPresent;
         if (activityTaskbarPresent == contextTaskbarPresent) {
             log.add("mActivity and mWindowContext agree taskbarIsPresent=" + contextTaskbarPresent);
             Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
@@ -993,12 +1071,12 @@
             log.add("\t\tmActivity.getDeviceProfile().isTaskbarPresent="
                     + activityTaskbarPresent);
         }
-        log.add("\tmWindowContext logs:");
-        log.add("\t\tmWindowContext=" + mWindowContext);
-        log.add("\t\tmWindowContext.getResources().getConfiguration()="
-                + mWindowContext.getResources().getConfiguration());
+        log.add("\tWindowContext logs:");
+        log.add("\t\tWindowContext=" + windowContext);
+        log.add("\t\tWindowContext.getResources().getConfiguration()="
+                + windowContext.getResources().getConfiguration());
         if (mUserUnlocked) {
-            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mWindowContext)"
+            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(getPrimaryWindowContext())"
                     + ".isTaskbarPresent=" + contextTaskbarPresent);
         } else {
             log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
@@ -1010,8 +1088,4 @@
     private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
             dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
 
-    @VisibleForTesting
-    public Context getWindowContext() {
-        return mWindowContext;
-    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 6047999..417ef7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -76,12 +76,14 @@
     var shownTasks: List<GroupTask> = emptyList()
         private set
 
+    val shownTaskIds: List<Int>
+        get() = shownTasks.flatMap { shownTask -> shownTask.tasks }.map { it.key.id }
+
     /**
-     * The task-state of an app, i.e. whether the app has a task and what state
-     * that task is in.
+     * The task-state of an app, i.e. whether the app has a task and what state that task is in.
      *
-     * @property taskId The ID of the task if one exists (i.e. if the state is
-     * RUNNING or MINIMIZED), null otherwise (NOT_RUNNING).
+     * @property taskId The ID of the task if one exists (i.e. if the state is RUNNING or
+     *   MINIMIZED), null otherwise (NOT_RUNNING).
      */
     data class TaskState(val runningAppState: RunningAppState, val taskId: Int? = null)
 
@@ -214,9 +216,9 @@
         return shownHotseatItems.toTypedArray()
     }
 
-    private fun getOrderedAndWrappedDesktopTasks(): List<GroupTask> {
+    private fun getOrderedAndWrappedDesktopTasks(): List<SingleTask> {
         val tasks = desktopTask?.tasks ?: emptyList()
-        // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+        // We wrap each task in the Desktop as a `SingleTask`.
         val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
         val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
         return sortedTasks.map { SingleTask(it) }
@@ -286,7 +288,7 @@
     }
 
     private fun updateOrderedRunningTaskIds(): MutableList<Int> {
-        val desktopTasksAsList = getOrderedAndWrappedDesktopTasks().flatMap { it.tasks }
+        val desktopTasksAsList = getOrderedAndWrappedDesktopTasks().map { it.task }
         val desktopTaskIds = desktopTasksAsList.map { it.key.id }
         var newOrder =
             orderedRunningTaskIds
@@ -311,42 +313,43 @@
         val newShownTasks =
             if (Flags.enableMultiInstanceMenuTaskbar()) {
                 val deduplicatedDesktopTasks =
-                    desktopTasks.distinctBy { Pair(it.task1.key.packageName, it.task1.key.userId) }
+                    desktopTasks.distinctBy { Pair(it.task.key.packageName, it.task.key.userId) }
 
                 shownTasks
                     .filter {
-                        !it.supportsMultipleTasks() &&
-                            it.task1.key.id in deduplicatedDesktopTasks.map { it.task1.key.id }
+                        it is SingleTask &&
+                            it.task.key.id in deduplicatedDesktopTasks.map { it.task.key.id }
                     }
                     .toMutableList()
                     .apply {
                         addAll(
                             deduplicatedDesktopTasks.filter { currentTask ->
-                                val currentTaskKey = currentTask.task1.key
-                                currentTaskKey.id !in shownTasks.map { it.task1.key.id } &&
+                                val currentTaskKey = currentTask.task.key
+                                currentTaskKey.id !in shownTaskIds &&
                                     shownHotseatItems.none { hotseatItem ->
-                                        hotseatItem.targetPackage == currentTaskKey.packageName &&
-                                            hotseatItem.user.identifier == currentTaskKey.userId
+                                        currentTask.containsPackage(
+                                            hotseatItem.targetPackage,
+                                            hotseatItem.user.identifier,
+                                        )
                                     }
                             }
                         )
                     }
             } else {
-                val desktopTaskIds = desktopTasks.map { it.task1.key.id }
+                val desktopTaskIds = desktopTasks.map { it.task.key.id }
                 val shownHotseatItemTaskIds =
                     shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
 
                 shownTasks
-                    .filter { !it.supportsMultipleTasks() && it.task1.key.id in desktopTaskIds }
+                    .filter { it is SingleTask && it.task.key.id in desktopTaskIds }
                     .toMutableList()
                     .apply {
                         addAll(
                             desktopTasks.filter { desktopTask ->
-                                desktopTask.task1.key.id !in
-                                    shownTasks.map { shownTask -> shownTask.task1.key.id }
+                                desktopTask.task.key.id !in shownTaskIds
                             }
                         )
-                        removeAll { it.task1.key.id in shownHotseatItemTaskIds }
+                        removeAll { it is SingleTask && it.task.key.id in shownHotseatItemTaskIds }
                     }
             }
 
@@ -371,21 +374,28 @@
         groupTasks: List<GroupTask>,
         shownHotseatItems: List<ItemInfo>,
     ): List<GroupTask> {
+        // TODO: b/393476333 - Check the behavior of the Taskbar recents section when empty desks
+        // become supported.
         return if (Flags.enableMultiInstanceMenuTaskbar()) {
             groupTasks.filter { groupTask ->
-                val taskKey = groupTask.task1.key
                 // Keep tasks that are group tasks or unique package name/user combinations
-                groupTask.hasMultipleTasks() ||
-                    shownHotseatItems.none {
-                        it.targetPackage == taskKey.packageName &&
-                            it.user.identifier == taskKey.userId
-                    }
+                when (groupTask) {
+                    is SingleTask ->
+                        shownHotseatItems.none {
+                            groupTask.containsPackage(it.targetPackage, it.user.identifier)
+                        }
+
+                    else -> true
+                }
             }
         } else {
             val hotseatPackages = shownHotseatItems.map { it.targetPackage }
             groupTasks.filter { groupTask ->
-                groupTask.hasMultipleTasks() ||
-                    !hotseatPackages.contains(groupTask.task1.key.packageName)
+                when (groupTask) {
+                    is SingleTask -> hotseatPackages.none { groupTask.containsPackage(it) }
+
+                    else -> true
+                }
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index f29f95d..e5d642d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -39,7 +39,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SplitTask;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
@@ -332,7 +332,7 @@
      * Launches the given task in split-screen.
      */
     public void launchSplitTasks(
-            @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { }
+            @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) { }
 
     /**
      * Returns the matching view (if any) in the taskbar.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 457ba3d..a59c9e3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -26,8 +26,6 @@
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
-import static java.util.function.Predicate.not;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -68,6 +66,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
 import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -398,7 +397,7 @@
                 .filter(Objects::nonNull)
                 .toArray(ItemInfo[]::new);
         // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
-        recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList();
+        recentTasks = recentTasks.stream().filter(it -> it instanceof SingleTask).toList();
 
         if (taskbarRecentsLayoutTransition()) {
             updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks);
@@ -636,9 +635,10 @@
         final Set<GroupTask> recentTasksSet = new ArraySet<>(recentTasks);
         for (GroupTask task : recentTasks) {
             if (mTaskbarOverflowView != null && overflownTasks != null
-                    && overflownTasks.size() < itemsToAddToOverflow) {
+                    && overflownTasks.size() < itemsToAddToOverflow
+                    && task instanceof SingleTask singleTask) {
                 // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
-                overflownTasks.add(task.task1);
+                overflownTasks.add(singleTask.getTask());
                 if (overflownTasks.size() == itemsToAddToOverflow) {
                     mTaskbarOverflowView.setItems(overflownTasks);
                 }
@@ -648,7 +648,7 @@
             // Replace any Recent views with the appropriate type if it's not already that type.
             final int expectedLayoutResId;
             boolean isCollection = false;
-            if (task.supportsMultipleTasks()) {
+            if (!(task instanceof SingleTask)) {
                 if (task.taskViewType == TaskViewType.DESKTOP) {
                     // TODO(b/316004172): use Desktop tile layout.
                     expectedLayoutResId = -1;
@@ -712,18 +712,22 @@
                 && tagClass.isInstance(getChildAt(mNextViewIndex).getTag());
     }
 
-    /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
+    /** Binds the SingleTask to the BubbleTextView to be ready to present to the user. */
     public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) {
-        // TODO(b/343289567): support app pairs.
-        Task task1 = groupTask.task1;
+        if (!(groupTask instanceof SingleTask singleTask)) {
+            // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+            return;
+        }
+
+        Task task = singleTask.getTask();
         // TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state
         //  while dragging.
-        Drawable taskIcon = groupTask.task1.icon;
+        Drawable taskIcon = task.icon;
         if (taskIcon != null) {
             taskIcon = taskIcon.getConstantState().newDrawable().mutate();
         }
-        btv.applyIconAndLabel(taskIcon, task1.titleDescription);
-        btv.setTag(groupTask);
+        btv.applyIconAndLabel(taskIcon, task.titleDescription);
+        btv.setTag(singleTask);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 0f05887..cbc5d3d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -92,6 +92,7 @@
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
@@ -739,9 +740,9 @@
             return mControllers.taskbarRecentAppsController.getRunningAppState(
                     itemInfo.getTaskId());
         }
-        if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+        if (tag instanceof SingleTask singleTask) {
             return mControllers.taskbarRecentAppsController.getRunningAppState(
-                    groupTask.task1.key.id);
+                    singleTask.getTask().key.id);
         }
         return BubbleTextView.RunningAppState.NOT_RUNNING;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index f672840..23f4f67 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -65,7 +65,6 @@
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -150,8 +149,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.TaskViewDismissTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewLaunchTouchController;
 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;
@@ -178,10 +178,10 @@
 import com.android.quickstep.TouchInteractionService.TISBinder;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AsyncClockEventDelegate;
-import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LauncherUnfoldAnimationController;
 import com.android.quickstep.util.QuickstepOnboardingPrefs;
 import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SplitTask;
 import com.android.quickstep.util.SplitToWorkspaceController;
 import com.android.quickstep.util.SplitWithKeyboardShortcutController;
 import com.android.quickstep.util.TISBindHelper;
@@ -460,7 +460,7 @@
 
     protected void onItemClicked(View view) {
         if (!mSplitToWorkspaceController.handleSecondAppSelectionForSplit(view)) {
-            QuickstepLauncher.super.getItemOnClickListener().onClick(view);
+            super.getItemOnClickListener().onClick(view);
         }
     }
 
@@ -688,7 +688,8 @@
         }
 
         if (enableExpressiveDismissTaskMotion()) {
-            list.add(new TaskViewTouchController<>(this, mTaskViewRecentsTouchContext));
+            list.add(new TaskViewLaunchTouchController<>(this, mTaskViewRecentsTouchContext));
+            list.add(new TaskViewDismissTouchController<>(this, mTaskViewRecentsTouchContext));
         } else {
             list.add(new TaskViewTouchControllerDeprecated<>(this, mTaskViewRecentsTouchContext));
         }
@@ -732,6 +733,9 @@
         final boolean ret = super.initDeviceProfile(idp);
         mDeviceProfile.isPredictiveBackSwipe =
                 getApplicationInfo().isOnBackInvokedCallbackEnabled();
+        if (ret) {
+            SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
+        }
         return ret;
     }
 
@@ -1161,9 +1165,9 @@
                     .getHotseatTranslationXForNavBar(this, isBubblesOnLeft);
         }
         if (isBubbleBarEnabled()
-                && mDeviceProfile.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles())) {
+                && mDeviceProfile.shouldAdjustHotseatForBubbleBar(asContext(), hasBubbles())) {
             translationX += (int) mDeviceProfile
-                    .getHotseatAdjustedTranslation(getContext(), itemInfo.cellX);
+                    .getHotseatAdjustedTranslation(asContext(), itemInfo.cellX);
         }
         return translationX;
     }
@@ -1238,6 +1242,7 @@
         }
     }
 
+    @NonNull
     @Override
     public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
         ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(
@@ -1368,12 +1373,6 @@
     }
 
     @Override
-    protected void onDeviceProfileInitiated() {
-        super.onDeviceProfileInitiated();
-        SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
-    }
-
-    @Override
     public void dispatchDeviceProfileChanged() {
         super.dispatchDeviceProfileChanged();
         Trace.instantForTrack(TRACE_TAG_APP, "QuickstepLauncher#DeviceProfileChanged",
@@ -1386,33 +1385,20 @@
     }
 
     /**
-     * Launches the given {@link GroupTask} in splitscreen.
+     * Launches the given {@link SplitTask} in splitscreen.
      */
     public void launchSplitTasks(
-            @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
-        // SplitBounds can be null if coming from Taskbar launch.
-        final boolean firstTaskIsLeftTopTask = isFirstTaskLeftTopTask(groupTask);
-        // task2 should never be null when calling this method. Allow a crash to catch invalid calls
-        Task task1 = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
-        Task task2 = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
-        mSplitSelectStateController.launchExistingSplitPair(
-                null /* launchingTaskView */,
-                task1.key.id,
-                task2.key.id,
+            @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) {
+        mSplitSelectStateController.launchExistingSplitPair(null /* launchingTaskView */,
+                splitTask.getTopLeftTask().key.id,
+                splitTask.getBottomRightTask().key.id,
                 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
                 /* callback= */ success -> mSplitSelectStateController.resetState(),
                 /* freezeTaskList= */ false,
-                groupTask.mSplitBounds == null
-                        ? SNAP_TO_2_50_50
-                        : groupTask.mSplitBounds.snapPosition,
+                splitTask.getSplitBounds().snapPosition,
                 remoteTransition);
     }
 
-    private static boolean isFirstTaskLeftTopTask(@NonNull GroupTask groupTask) {
-        return groupTask.mSplitBounds == null
-                || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
-    }
-
     /**
      * Launches two apps as an app pair.
      */
@@ -1522,4 +1508,9 @@
     public void setCanShowAllAppsEducationView(boolean canShowAllAppsEducationView) {
         mCanShowAllAppsEducationView = canShowAllAppsEducationView;
     }
+
+    @Override
+    public void returnToHomescreen() {
+        getStateManager().goToState(LauncherState.NORMAL);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index dae63af..10513c0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -48,8 +48,7 @@
     }
 
     @Override
-    public <DEVICE_PROFILE_CONTEXT extends Context & ActivityContext>
-    int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         return isToState
                 ? context.getDeviceProfile().allAppsOpenDuration
                 : context.getDeviceProfile().allAppsCloseDuration;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 932d241..80fc5fa 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -17,12 +17,12 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 
-import android.content.Context;
 import android.graphics.Rect;
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -39,7 +39,7 @@
     }
 
     @Override
-    public int getTransitionDuration(Context launcher, boolean isToState) {
+    public int getTransitionDuration(ActivityContext launcher, boolean isToState) {
         return 300;
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 5c16a62..15216fe 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.util.BaseDepthController;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
@@ -62,10 +63,10 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         if (isToState) {
             // In gesture modes, overview comes in all the way from the side, so give it more time.
-            return DisplayController.getNavigationMode(context).hasGestures
+            return DisplayController.getNavigationMode(context.asContext()).hasGestures
                     ? OVERVIEW_SLIDE_IN_DURATION
                     : OVERVIEW_POP_IN_DURATION;
         } else {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
index 3ae221b..2631fbf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -16,9 +16,8 @@
 
 package com.android.launcher3.uioverrides.states;
 
-import android.content.Context;
-
 import com.android.launcher3.Launcher;
+import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.views.RecentsView;
 
@@ -43,11 +42,10 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
-        boolean isTablet = ((Launcher) context).getDeviceProfile().isTablet;
-        if (isToState && isTablet) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
+        if (isToState && context.getDeviceProfile().isTablet) {
             return SplitAnimationTimings.TABLET_ENTER_DURATION;
-        } else if (isToState && !isTablet) {
+        } else if (isToState) {
             return SplitAnimationTimings.PHONE_ENTER_DURATION;
         } else {
             return SplitAnimationTimings.ABORT_DURATION;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index cbfcda7..05d12c3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -186,7 +186,7 @@
             mIsTrackpadSwipe = isTrackpadFourFingerSwipe(ev);
             return mIsTrackpadSwipe;
         }
-        if (DesktopModeStatus.canEnterDesktopMode(mLauncher.getContext())
+        if (DesktopModeStatus.canEnterDesktopMode(mLauncher)
                 //TODO(b/345296916): replace with dev option once in teamfood
                 && Flags.enableQuickswitchDesktopSplitBugfix()
                 && mRecentsView.getNonDesktopTaskViewCount() < 1) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
new file mode 100644
index 0000000..99b962b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.content.Context
+import android.view.MotionEvent
+import androidx.dynamicanimation.animation.SpringAnimation
+import com.android.app.animation.Interpolators.DECELERATE
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.Utilities.EDGE_NAV_BAR
+import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.Utilities.isRtl
+import com.android.launcher3.Utilities.mapToRange
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.TouchController
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskView
+import kotlin.math.abs
+import kotlin.math.sign
+
+/** Touch controller for handling task view card dismiss swipes */
+class TaskViewDismissTouchController<CONTAINER>(
+    private val container: CONTAINER,
+    private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext,
+) : 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 isRtl = isRtl(container.resources)
+
+    private var taskBeingDragged: TaskView? = null
+    private var springAnimation: SpringAnimation? = null
+    private var dismissLength: Int = 0
+    private var verticalFactor: Int = 0
+    private var initialDisplacement: Float = 0f
+
+    private fun canInterceptTouch(ev: MotionEvent): Boolean =
+        when {
+            // Don't intercept swipes on the nav bar, as user might be trying to go home during a
+            // task dismiss animation.
+            (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> {
+                false
+            }
+
+            // Floating views that a TouchController should not try to intercept touches from.
+            AbstractFloatingView.getTopOpenViewWithType(
+                container,
+                AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT,
+            ) != null -> false
+
+            // Disable swiping if the task overlay is modal.
+            taskViewRecentsTouchContext.isRecentsModal -> {
+                false
+            }
+
+            else -> taskViewRecentsTouchContext.isRecentsInteractive
+        }
+
+    override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
+        if ((ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL)) {
+            clearState()
+        }
+        if (ev.action == MotionEvent.ACTION_DOWN) {
+            if (!onActionDown(ev)) {
+                return false
+            }
+        }
+
+        onControllerTouchEvent(ev)
+        return detector.isDraggingState && detector.wasInitialTouchPositive()
+    }
+
+    override fun onControllerTouchEvent(ev: MotionEvent?): Boolean = detector.onTouchEvent(ev)
+
+    private fun onActionDown(ev: MotionEvent): Boolean {
+        springAnimation?.cancel()
+        if (!canInterceptTouch(ev)) {
+            return false
+        }
+
+        taskBeingDragged =
+            recentsView.taskViews
+                .firstOrNull {
+                    recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev)
+                }
+                ?.also {
+                    dismissLength = recentsView.pagedOrientationHandler.getSecondaryDimension(it)
+                    verticalFactor =
+                        recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
+                }
+
+        detector.setDetectableScrollConditions(
+            recentsView.pagedOrientationHandler.getUpDirection(isRtl),
+            /* ignoreSlop = */ false,
+        )
+
+        return true
+    }
+
+    override fun onDragStart(start: Boolean, startDisplacement: Float) {
+        val taskBeingDragged = taskBeingDragged ?: return
+
+        initialDisplacement =
+            taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged)
+
+        // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant
+        // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we
+        // want the dragged task to stay above all other views.
+        taskBeingDragged.translationZ = 0.1f
+    }
+
+    override fun onDrag(displacement: Float): Boolean {
+        val taskBeingDragged = taskBeingDragged ?: return false
+        val currentDisplacement = displacement + initialDisplacement
+        val boundedDisplacement =
+            boundToRange(abs(currentDisplacement), 0f, dismissLength.toFloat())
+        // When swiping below origin, allow slight undershoot to simulate resisting the movement.
+        val totalDisplacement =
+            if (isDisplacementPositiveDirection(currentDisplacement))
+                boundedDisplacement * sign(currentDisplacement)
+            else
+                mapToRange(
+                    boundedDisplacement,
+                    0f,
+                    dismissLength.toFloat(),
+                    0f,
+                    DISMISS_MAX_UNDERSHOOT,
+                    DECELERATE,
+                )
+        taskBeingDragged.secondaryDismissTranslationProperty.setValue(
+            taskBeingDragged,
+            totalDisplacement,
+        )
+        if (taskBeingDragged.isRunningTask && recentsView.enableDrawingLiveTile) {
+            recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+                remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+                    totalDisplacement
+            }
+            recentsView.redrawLiveTile()
+        }
+        return true
+    }
+
+    override fun onDragEnd(velocity: Float) {
+        val taskBeingDragged = taskBeingDragged ?: return
+
+        val currentDisplacement =
+            taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged)
+        if (currentDisplacement == 0f) {
+            clearState()
+            return
+        }
+        val isBeyondDismissThreshold =
+            abs(currentDisplacement) > abs(DISMISS_THRESHOLD_FRACTION * dismissLength)
+        val isFlingingTowardsDismiss = detector.isFling(velocity) && velocity < 0
+        val isFlingingTowardsRestState = detector.isFling(velocity) && velocity > 0
+        val isDismissing =
+            isFlingingTowardsDismiss || (isBeyondDismissThreshold && !isFlingingTowardsRestState)
+        springAnimation =
+            recentsView
+                .createTaskDismissSettlingSpringAnimation(
+                    taskBeingDragged,
+                    velocity,
+                    isDismissing,
+                    detector,
+                    dismissLength,
+                    this::clearState,
+                )
+                .apply {
+                    animateToFinalPosition(
+                        if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f
+                    )
+                }
+    }
+
+    // Returns if the current task being dragged is towards "positive" (e.g. dismissal).
+    private fun isDisplacementPositiveDirection(displacement: Float): Boolean =
+        sign(displacement) == sign(verticalFactor.toFloat())
+
+    private fun clearState() {
+        detector.finishedScrolling()
+        detector.setDetectableScrollConditions(0, false)
+        taskBeingDragged?.translationZ = 0f
+        taskBeingDragged = null
+        springAnimation = null
+    }
+
+    companion object {
+        private const val DISMISS_THRESHOLD_FRACTION = 0.5f
+        private const val DISMISS_MAX_UNDERSHOOT = 25f
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
new file mode 100644
index 0000000..c740dad
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.content.Context
+import android.graphics.Rect
+import android.view.MotionEvent
+import com.android.app.animation.Interpolators.ZOOM_IN
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.LauncherAnimUtils
+import com.android.launcher3.Utilities.EDGE_NAV_BAR
+import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.Utilities.isRtl
+import com.android.launcher3.anim.AnimatorPlaybackController
+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.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskView
+import kotlin.math.abs
+
+/** Touch controller which handles dragging task view cards for launch. */
+class TaskViewLaunchTouchController<CONTAINER>(
+    private val container: CONTAINER,
+    private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext,
+) : TouchController, SingleAxisSwipeDetector.Listener where
+CONTAINER : Context,
+CONTAINER : RecentsViewContainer {
+    private val tempRect = Rect()
+    private val flingBlockCheck = FlingBlockCheck()
+    private val recentsView: RecentsView<*, *> = container.getOverviewPanel()
+    private val detector: SingleAxisSwipeDetector =
+        SingleAxisSwipeDetector(
+            container as Context,
+            this,
+            recentsView.pagedOrientationHandler.upDownSwipeDirection,
+        )
+    private val isRtl = isRtl(container.resources)
+
+    private var taskBeingDragged: TaskView? = null
+    private var launchEndDisplacement: Float = 0f
+    private var playbackController: AnimatorPlaybackController? = null
+    private var verticalFactor: Int = 0
+
+    private fun canTaskLaunchTaskView(taskView: TaskView?) =
+        taskView != null &&
+            taskView === recentsView.currentPageTaskView &&
+            DisplayController.getNavigationMode(container).hasGestures &&
+            (!recentsView.showAsGrid() || taskView.isLargeTile) &&
+            recentsView.isTaskInExpectedScrollPosition(taskView)
+
+    private fun canInterceptTouch(ev: MotionEvent): Boolean =
+        when {
+            // Don't intercept swipes on the nav bar, as user might be trying to go home during a
+            // task dismiss animation.
+            (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> {
+                false
+            }
+
+            // Floating views that a TouchController should not try to intercept touches from.
+            AbstractFloatingView.getTopOpenViewWithType(
+                container,
+                AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT,
+            ) != null -> {
+                false
+            }
+
+            // Disable swiping if the task overlay is modal.
+            taskViewRecentsTouchContext.isRecentsModal -> {
+                false
+            }
+
+            else -> taskViewRecentsTouchContext.isRecentsInteractive
+        }
+
+    override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
+        if (
+            (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) &&
+                playbackController == null
+        ) {
+            clearState()
+        }
+        if (ev.action == MotionEvent.ACTION_DOWN) {
+            if (!onActionDown(ev)) {
+                clearState()
+                return false
+            }
+        }
+        onControllerTouchEvent(ev)
+        return detector.isDraggingState && !detector.wasInitialTouchPositive()
+    }
+
+    override fun onControllerTouchEvent(ev: MotionEvent) = detector.onTouchEvent(ev)
+
+    private fun onActionDown(ev: MotionEvent): Boolean {
+        if (!canInterceptTouch(ev)) {
+            return false
+        }
+        taskBeingDragged =
+            recentsView.taskViews
+                .firstOrNull {
+                    recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev)
+                }
+                ?.also {
+                    verticalFactor =
+                        recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
+                }
+        if (!canTaskLaunchTaskView(taskBeingDragged)) {
+            return false
+        }
+        detector.setDetectableScrollConditions(
+            recentsView.pagedOrientationHandler.getDownDirection(isRtl),
+            /* ignoreSlop = */ false,
+        )
+        return true
+    }
+
+    override fun onDragStart(start: Boolean, startDisplacement: Float) {
+        val taskBeingDragged = taskBeingDragged ?: return
+
+        val secondaryLayerDimension: Int =
+            recentsView.pagedOrientationHandler.getSecondaryDimension(container.getDragLayer())
+        val maxDuration = 2L * secondaryLayerDimension
+        recentsView.clearPendingAnimation()
+        val pendingAnimation =
+            recentsView.createTaskLaunchAnimation(taskBeingDragged, maxDuration, ZOOM_IN)
+        // Since the thumbnail is what is filling the screen, based the end displacement on it.
+        taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true)
+        launchEndDisplacement = (secondaryLayerDimension - tempRect.bottom).toFloat()
+        playbackController =
+            pendingAnimation.createPlaybackController()?.apply {
+                taskViewRecentsTouchContext.onUserControlledAnimationCreated(this)
+                dispatchOnStart()
+            }
+    }
+
+    override fun onDrag(displacement: Float): Boolean {
+        playbackController?.setPlayFraction(
+            boundToRange(displacement / launchEndDisplacement, 0f, 1f)
+        )
+        return true
+    }
+
+    override fun onDragEnd(velocity: Float) {
+        val playbackController = playbackController ?: return
+
+        val isBeyondLaunchThreshold =
+            abs(playbackController.progressFraction) > abs(LAUNCH_THRESHOLD_FRACTION)
+        val isFlingingTowardsLaunch = detector.isFling(velocity) && velocity > 0
+        val isFlingingTowardsRestState = detector.isFling(velocity) && velocity < 0
+        val isLaunching =
+            isFlingingTowardsLaunch || (isBeyondLaunchThreshold && !isFlingingTowardsRestState)
+
+        val progress = playbackController.progressFraction
+        var animationDuration =
+            BaseSwipeDetector.calculateDuration(
+                velocity,
+                if (isLaunching) (1 - progress) else progress,
+            )
+        if (detector.isFling(velocity) && flingBlockCheck.isBlocked && !isLaunching) {
+            animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity).toLong()
+        }
+
+        playbackController.setEndAction(this::clearState)
+        playbackController.startWithVelocity(
+            container,
+            isLaunching,
+            velocity,
+            launchEndDisplacement,
+            animationDuration,
+        )
+    }
+
+    private fun clearState() {
+        detector.finishedScrolling()
+        detector.setDetectableScrollConditions(0, false)
+        taskBeingDragged = null
+        playbackController = null
+    }
+
+    companion object {
+        private const val LAUNCH_THRESHOLD_FRACTION: Float = 0.5f
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt
deleted file mode 100644
index c996f34..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt
+++ /dev/null
@@ -1,394 +0,0 @@
-/*
- * 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/widget/picker/WidgetCategoryFilter.kt b/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt
new file mode 100644
index 0000000..69feb4a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker
+
+/**
+ * A filter that can be applied on the widgetCategory attribute from appwidget-provider to identify
+ * if the widget can be displayed on a specific widget surface.
+ * - Negative value (e.g. "category_a.inv() and category_b.inv()" excludes the widgets with given
+ *   categories.
+ * - Positive value (e.g. "category_a or category_b" includes widgets with those categories.
+ * - 0 means no filter.
+ */
+class WidgetCategoryFilter(val categoryMask: Int) {
+    /** Applies the [categoryMask] to return if the [widgetCategory] matches. */
+    fun matches(widgetCategory: Int): Boolean {
+        return if (categoryMask > 0) { // inclusion filter
+            (widgetCategory and categoryMask) != 0
+        } else if (categoryMask < 0) { // exclusion filter
+            (widgetCategory and categoryMask) == widgetCategory
+        } else {
+            true // no filter
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/HomeVisibilityState.kt b/quickstep/src/com/android/quickstep/HomeVisibilityState.kt
index 241e16d..1345e0b 100644
--- a/quickstep/src/com/android/quickstep/HomeVisibilityState.kt
+++ b/quickstep/src/com/android/quickstep/HomeVisibilityState.kt
@@ -18,6 +18,7 @@
 
 import android.os.RemoteException
 import android.util.Log
+import com.android.launcher3.Utilities
 import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.util.Executors
 import com.android.wm.shell.shared.IHomeTransitionListener.Stub
@@ -41,10 +42,13 @@
             transitions?.setHomeTransitionListener(
                 object : Stub() {
                     override fun onHomeVisibilityChanged(isVisible: Boolean) {
-                        Executors.MAIN_EXECUTOR.execute {
-                            isHomeVisible = isVisible
-                            listeners.forEach { it.onHomeVisibilityChanged(isVisible) }
-                        }
+                        Utilities.postAsyncCallback(
+                            Executors.MAIN_EXECUTOR.handler,
+                            {
+                                isHomeVisible = isVisible
+                                listeners.forEach { it.onHomeVisibilityChanged(isVisible) }
+                            },
+                        )
                     }
                 }
             )
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 5e8ea37..fca67c3 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_PRE_DELAY;
 import static com.android.launcher3.testing.shared.TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
+import static com.android.launcher3.util.WallpaperThemeManager.setWallpaperDependentTheme;
 import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
@@ -247,7 +248,6 @@
 
     @Override
     public void returnToHomescreen() {
-        super.returnToHomescreen();
         // TODO(b/137318995) This should go home, but doing so removes freeform windows
     }
 
@@ -261,6 +261,7 @@
         }
     }
 
+    @NonNull
     @Override
     public ActivityOptionsWrapper getActivityLaunchOptions(final View v, @Nullable ItemInfo item) {
         if (!(v instanceof TaskView)) {
@@ -371,6 +372,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        setWallpaperDependentTheme(this);
 
         mStateManager = new StateManager<>(this, RecentsState.BG_LAUNCHER);
 
@@ -431,7 +433,6 @@
      */
     private void initDeviceProfile() {
         mDeviceProfile = createDeviceProfile();
-        onDeviceProfileInitiated();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index dc5d59f..87b58e6 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
@@ -41,6 +42,7 @@
 import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
+import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.recents.data.RecentTasksDataSource;
@@ -63,6 +65,8 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
+import javax.inject.Provider;
+
 /**
  * Singleton class to load and manage recents model.
  */
@@ -86,15 +90,19 @@
     private final TaskIconCache mIconCache;
     private final TaskThumbnailCache mThumbnailCache;
     private final ComponentCallbacks mCallbacks;
-    private final ThemeManager mThemeManager;
 
     private final TaskStackChangeListeners mTaskStackChangeListeners;
     private final SafeCloseable mIconChangeCloseable;
 
+    private final LockedUserState mLockedUserState;
+    private final Provider<ThemeManager> mThemeManagerProvider;
+    private final Runnable mUnlockCallback;
+
     private RecentsModel(Context context) {
         this(context, new IconProvider(context));
     }
 
+    @SuppressLint("VisibleForTests")
     private RecentsModel(Context context, IconProvider iconProvider) {
         this(context,
                 new RecentTasksList(
@@ -107,14 +115,16 @@
                 new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
                 iconProvider,
                 TaskStackChangeListeners.getInstance(),
-                ThemeManager.INSTANCE.get(context));
+                LockedUserState.get(context),
+                () -> ThemeManager.INSTANCE.get(context));
     }
 
     @VisibleForTesting
     RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache,
             TaskThumbnailCache thumbnailCache, IconProvider iconProvider,
             TaskStackChangeListeners taskStackChangeListeners,
-            ThemeManager themeManager) {
+            LockedUserState lockedUserState,
+            Provider<ThemeManager> themeManagerProvider) {
         mContext = context;
         mTaskList = taskList;
         mIconCache = iconCache;
@@ -140,8 +150,11 @@
         mTaskStackChangeListeners.registerTaskStackListener(this);
         mIconChangeCloseable = iconProvider.registerIconChangeListener(
                 this::onAppIconChanged, MAIN_EXECUTOR.getHandler());
-        mThemeManager = themeManager;
-        themeManager.addChangeListener(this);
+
+        mLockedUserState = lockedUserState;
+        mThemeManagerProvider = themeManagerProvider;
+        mUnlockCallback = () -> mThemeManagerProvider.get().addChangeListener(this);
+        lockedUserState.runOnUserUnlocked(mUnlockCallback);
     }
 
     public TaskIconCache getIconCache() {
@@ -402,7 +415,12 @@
         mIconCache.removeTaskVisualsChangeListener();
         mTaskStackChangeListeners.unregisterTaskStackListener(this);
         mIconChangeCloseable.close();
-        mThemeManager.removeChangeListener(this);
+
+        if (mLockedUserState.isUserUnlocked()) {
+            mThemeManagerProvider.get().removeChangeListener(this);
+        } else {
+            mLockedUserState.removeOnUserUnlockedRunnable(mUnlockCallback);
+        }
     }
 
     private boolean isCachePreloadingEnabled() {
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 0772159..a594e49 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -120,7 +120,8 @@
             TaskShortcutFactory.WELLBEING,
             TaskShortcutFactory.SAVE_APP_PAIR,
             TaskShortcutFactory.SCREENSHOT,
-            TaskShortcutFactory.MODAL
+            TaskShortcutFactory.MODAL,
+            TaskShortcutFactory.CLOSE,
     };
 
     /**
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index ee1d8a6..2dbd5e9 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -21,6 +21,7 @@
 import static android.view.Surface.ROTATION_0;
 
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 
@@ -297,6 +298,29 @@
         }
     }
 
+    class CloseSystemShortcut extends SystemShortcut {
+        private final TaskContainer mTaskContainer;
+
+        public CloseSystemShortcut(int iconResId, int textResId, RecentsViewContainer container,
+                TaskContainer taskContainer) {
+            super(iconResId, textResId, container, taskContainer.getTaskView().getFirstItemInfo(),
+                    taskContainer.getTaskView());
+            mTaskContainer = taskContainer;
+        }
+
+        @Override
+        public void onClick(View view) {
+            TaskView taskView = mTaskContainer.getTaskView();
+            RecentsView<?, ?> recentsView = taskView.getRecentsView();
+            if (recentsView != null) {
+                dismissTaskMenuView();
+                recentsView.dismissTask(taskView, true, true);
+                mTarget.getStatsLogManager().logger().withItemInfo(mTaskContainer.getItemInfo())
+                        .log(LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP);
+            }
+        }
+    }
+
     /**
      * Does NOT add split options in the following scenarios:
      * * 1. Taskbar is not present AND aren't at least 2 tasks in overview to show split options for
@@ -517,4 +541,24 @@
             return createSingletonShortcutList(modalStateSystemShortcut);
         }
     };
+
+    TaskShortcutFactory CLOSE = new TaskShortcutFactory() {
+        @Override
+        public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
+                TaskContainer taskContainer) {
+            return Collections.singletonList(new CloseSystemShortcut(
+                    R.drawable.ic_close_option,
+                    R.string.recent_task_option_close, container, taskContainer));
+        }
+
+        @Override
+        public boolean showForGroupedTask() {
+            return true;
+        }
+
+        @Override
+        public boolean showForDesktopTask() {
+            return true;
+        }
+    };
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 44fdaec..b4b80c5 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -128,8 +128,7 @@
                 state.getScrimColor(mRecentsViewContainer.asContext()),
                 config.getInterpolator(ANIM_SCRIM_FADE, LINEAR));
         if (isSplitSelectionState(state)) {
-            int duration =
-                    state.getTransitionDuration(mRecentsViewContainer.asContext(), true);
+            int duration = state.getTransitionDuration(mRecentsViewContainer, true);
             // TODO (b/246851887): Pass in setter as a NO_ANIM PendingAnimation instead
             PendingAnimation pa = new PendingAnimation(duration);
             mRecentsView.createSplitSelectInitAnimation(pa, duration);
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
index 7e5afc3..5d4f1db 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -21,8 +21,9 @@
 import android.util.AttributeSet;
 
 import com.android.launcher3.statemanager.StatefulContainer;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewDismissTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewLaunchTouchController;
 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;
@@ -54,10 +55,18 @@
 
     @Override
     public void recreateControllers() {
-        mControllers = new TouchController[]{
-                enableExpressiveDismissTaskMotion() ? new TaskViewTouchController<>(mContainer,
-                        mTaskViewRecentsTouchContext) : new TaskViewTouchControllerDeprecated<>(
-                        mContainer, mTaskViewRecentsTouchContext),
-                new FallbackNavBarTouchController(mContainer)};
+        mControllers = enableExpressiveDismissTaskMotion()
+                ? new TouchController[]{
+                        new TaskViewLaunchTouchController<>(mContainer,
+                                mTaskViewRecentsTouchContext),
+                        new TaskViewDismissTouchController<>(mContainer,
+                                mTaskViewRecentsTouchContext),
+                        new FallbackNavBarTouchController(mContainer)
+                }
+                : new TouchController[]{
+                        new TaskViewTouchControllerDeprecated<>(mContainer,
+                                mTaskViewRecentsTouchContext),
+                        new FallbackNavBarTouchController(mContainer)
+                };
     }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index c2e7536..f27b60c 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.views.RecentsViewContainer;
 
 /**
@@ -92,7 +93,7 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         return 250;
     }
 
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 3082dc6..07288d8 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -390,10 +390,6 @@
         return systemUiController
     }
 
-    override fun getContext(): Context {
-        return this
-    }
-
     override fun getScrimView(): ScrimView? {
         return scrimView
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 42e8694..be47df9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -24,11 +24,10 @@
 import android.graphics.PointF;
 import android.view.MotionEvent;
 
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationDeviceState;
@@ -80,7 +79,7 @@
     @Override
     public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
         startHomeIntentSafely(mContext, mGestureState.getHomeIntent(), null, TAG);
-        BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
+        ActivityContext activity = ActivityContext.lookupContext(mContext);
         int state = (mGestureState != null && mGestureState.getEndTarget() != null)
                 ? mGestureState.getEndTarget().containerType
                 : LAUNCHER_STATE_HOME;
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index b2a30ca..594c99a 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -425,6 +425,22 @@
                 case LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END:
                     InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK);
                     break;
+                case LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN:
+                    InteractionJankMonitorWrapper.begin(
+                            view,
+                            Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND);
+                    break;
+                case LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END:
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND);
+                    break;
+                case LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN:
+                    InteractionJankMonitorWrapper.begin(
+                            view,
+                            Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK);
+                    break;
+                case LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END:
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK);
+                    break;
                 default:
                     break;
             }
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index 88ef0a8..e72ccbf 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -306,6 +306,10 @@
         if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE
         else SingleAxisSwipeDetector.DIRECTION_POSITIVE
 
+    override fun getDownDirection(isRtl: Boolean): Int =
+        if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE
+        else SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+
     override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
         if (isRtl) displacement < 0 else displacement > 0
 
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index c0b697d..c1e1c2b 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -310,6 +310,12 @@
     }
 
     @Override
+    public int getDownDirection(boolean isRtl) {
+        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+        return SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+    }
+
+    @Override
     public boolean isGoingUp(float displacement, boolean isRtl) {
         // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
         return displacement < 0;
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
index b8d0412..78f9a0a 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -332,6 +332,9 @@
     /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is up. */
     fun getUpDirection(isRtl: Boolean): Int
 
+    /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is down. */
+    fun getDownDirection(isRtl: Boolean): Int
+
     /** @return Whether the displacement is going towards the top of the screen. */
     fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean
 
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index bc91911..3fb4f54 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -351,6 +351,10 @@
         if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE
         else SingleAxisSwipeDetector.DIRECTION_NEGATIVE
 
+    override fun getDownDirection(isRtl: Boolean): Int =
+        if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+        else SingleAxisSwipeDetector.DIRECTION_POSITIVE
+
     override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
         if (isRtl) displacement > 0 else displacement < 0
 
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 34b0206..28152ec 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -151,8 +151,8 @@
         }
     }
 
-    fun setState(state: TaskThumbnailUiState) {
-        Log.d(TAG, "viewModelUiState changed from: $uiState to: $state")
+    fun setState(state: TaskThumbnailUiState, taskId: Int? = null) {
+        logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
         if (uiState == state) return
         uiState = state
         resetViews()
@@ -245,6 +245,10 @@
         thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
     }
 
+    private fun logDebug(message: String) {
+        Log.d(TAG, "[TaskThumbnailView@${Integer.toHexString(hashCode())}] $message")
+    }
+
     private fun maybeCreateHeader() {
         if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
             taskThumbnailViewHeader =
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 31aca03..fda0c29 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -78,8 +78,7 @@
             @NonNull SplitAnimationController animationController) {
         StateAnimationConfig config = new StateAnimationConfig();
         BaseState startState = stateManager.getState();
-        long duration = startState.getTransitionDuration(container.asContext(),
-                false /*isToState*/);
+        long duration = startState.getTransitionDuration(container, false /*isToState*/);
         if (duration == 0) {
             // Case where we're in contextual on workspace (NORMAL), which by default has 0
             // transition duration
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.kt b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
index 5463cf7..53ea022 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.kt
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
@@ -20,16 +20,9 @@
 
 /**
  * A [Task] container that can contain N number of tasks that are part of the desktop in recent
- * tasks list.
+ * tasks list. Note that desktops can be empty with no tasks in them.
  */
-class DesktopTask(override val tasks: List<Task>) :
-    GroupTask(tasks[0], null, null, TaskViewType.DESKTOP) {
-
-    override fun containsTask(taskId: Int) = tasks.any { it.key.id == taskId }
-
-    override fun hasMultipleTasks() = tasks.size > 1
-
-    override fun supportsMultipleTasks() = true
+class DesktopTask(tasks: List<Task>) : GroupTask(tasks, TaskViewType.DESKTOP) {
 
     override fun copy() = DesktopTask(tasks)
 
diff --git a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
index e1a8578..455b312 100644
--- a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
+++ b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.util
 
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.Display.INVALID_DISPLAY
 import com.android.systemui.shared.recents.model.Task
 
 /** Whether this displayId belongs to an external display */
@@ -25,7 +26,14 @@
 
 /** Returns displayId of this [Task], default to [DEFAULT_DISPLAY] */
 val Task?.displayId
-    get() = this?.key?.displayId ?: DEFAULT_DISPLAY
+    get() =
+        this?.key?.displayId.let { displayId ->
+            when (displayId) {
+                null -> DEFAULT_DISPLAY
+                INVALID_DISPLAY -> DEFAULT_DISPLAY
+                else -> displayId
+            }
+        }
 
 /** Returns if this task belongs tto [DEFAULT_DISPLAY] */
 val Task?.isExternalDisplay
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.kt b/quickstep/src/com/android/quickstep/util/GroupTask.kt
index d5bbcd3..49c37dc 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.kt
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.kt
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep.util
 
-import androidx.annotation.VisibleForTesting
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
@@ -25,47 +24,26 @@
  * An abstract class for creating [Task] containers that can be [SingleTask]s, [SplitTask]s, or
  * [DesktopTask]s in the recent tasks list.
  */
-abstract class GroupTask
-@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-constructor(
-    @Deprecated("Prefer using `getTasks()` instead") @JvmField val task1: Task,
-    @Deprecated("Prefer using `getTasks()` instead") @JvmField val task2: Task?,
-    @JvmField val mSplitBounds: SplitConfigurationOptions.SplitBounds?,
-    @JvmField val taskViewType: TaskViewType,
-) {
-    protected constructor(
-        task1: Task,
-        task2: Task?,
-        splitBounds: SplitConfigurationOptions.SplitBounds?,
-    ) : 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)
+abstract class GroupTask(val tasks: List<Task>, @JvmField val taskViewType: TaskViewType) {
+    fun containsTask(taskId: Int) = tasks.any { it.key.id == taskId }
 
     /**
      * Returns true if a task in this group has a package name that matches the given `packageName`.
      */
-    fun containsPackage(packageName: String) = tasks.any { it.key.packageName == packageName }
+    fun containsPackage(packageName: String?) = tasks.any { it.key.packageName == packageName }
 
-    open fun hasMultipleTasks() = task2 != null
+    /**
+     * Returns true if a task in this group has a package name that matches the given `packageName`,
+     * and its user ID matches the given `userId`.
+     */
+    fun containsPackage(packageName: String?, userId: Int) =
+        tasks.any { it.key.packageName == packageName && it.key.userId == userId }
 
-    /** Returns whether this task supports multiple tasks or not. */
-    open fun supportsMultipleTasks() = taskViewType == TaskViewType.GROUPED
-
-    /** Returns a List of all the Tasks in this GroupTask */
-    open val tasks: List<Task>
-        get() = listOfNotNull(task1, task2)
+    fun isEmpty() = tasks.isEmpty()
 
     /** Creates a copy of this instance */
     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
@@ -76,11 +54,14 @@
 }
 
 /** 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)
+class SingleTask(task: Task) : GroupTask(listOf(task), TaskViewType.SINGLE) {
 
-    override fun toString() = "type=$taskViewType task=$task1"
+    val task: Task
+        get() = tasks[0]
+
+    override fun copy() = SingleTask(task)
+
+    override fun toString() = "type=$taskViewType task=$task"
 
     override fun equals(o: Any?): Boolean {
         if (this === o) return true
@@ -93,19 +74,26 @@
  * 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) {
+class SplitTask(task1: Task, task2: Task, val splitBounds: SplitConfigurationOptions.SplitBounds) :
+    GroupTask(listOf(task1, task2), TaskViewType.GROUPED) {
 
-    override fun copy() = SplitTask(task1, task2!!, mSplitBounds!!)
+    val topLeftTask: Task
+        get() = if (splitBounds.leftTopTaskId == tasks[0].key.id) tasks[0] else tasks[1]
 
-    override fun toString() = "type=$taskViewType task1=$task1 task2=$task2"
+    val bottomRightTask: Task
+        get() = if (topLeftTask == tasks[0]) tasks[1] else tasks[0]
+
+    override fun copy() = SplitTask(tasks[0], tasks[1], splitBounds)
+
+    override fun toString() =
+        "type=$taskViewType topLeftTask=$topLeftTask bottomRightTask=$bottomRightTask"
 
     override fun equals(o: Any?): Boolean {
         if (this === o) return true
         if (o !is SplitTask) return false
-        if (mSplitBounds!! != o.mSplitBounds!!) return false
+        if (splitBounds != o.splitBounds) return false
         return super.equals(o)
     }
 
-    override fun hashCode() = Objects.hash(super.hashCode(), mSplitBounds)
+    override fun hashCode() = Objects.hash(super.hashCode(), splitBounds)
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 9b4c772..0182969 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -749,8 +749,8 @@
         // launcher side animation)
         val leftTopApp =
             leafRoots.single { change ->
-                (isLeftRightSplit && change.endAbsBounds.left == 0) ||
-                    (!isLeftRightSplit && change.endAbsBounds.top == 0)
+                (isLeftRightSplit && change.endAbsBounds.left <= 0) ||
+                    (!isLeftRightSplit && change.endAbsBounds.top <= 0)
             }
         val dividerPos =
             if (isLeftRightSplit) leftTopApp.endAbsBounds.right
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 5f8b4d9..fd8b356 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -71,7 +71,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.apppairs.AppPairIcon;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
@@ -260,7 +259,7 @@
                     GroupTask groupTask = taskGroups.get(i);
                     if (isInstanceOfAppPair(
                             groupTask, componentKeys.get(0), componentKeys.get(1))) {
-                        lastActiveTasks[0] = groupTask.task1;
+                        lastActiveTasks[0] = ((SplitTask) groupTask).getTopLeftTask();
                         break;
                     }
                 }
@@ -314,11 +313,15 @@
      */
     public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1,
             @NonNull ComponentKey componentKey2) {
-        return ((isInstanceOfComponent(groupTask.task1, componentKey1)
-                && isInstanceOfComponent(groupTask.task2, componentKey2))
-                ||
-                (isInstanceOfComponent(groupTask.task1, componentKey2)
-                        && isInstanceOfComponent(groupTask.task2, componentKey1)));
+        if (groupTask instanceof SplitTask splitTask) {
+            return ((isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey1)
+                    && isInstanceOfComponent(splitTask.getBottomRightTask(), componentKey2))
+                    ||
+                    (isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey2)
+                            && isInstanceOfComponent(splitTask.getBottomRightTask(),
+                            componentKey1)));
+        }
+        return false;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 1b59f5b..a8f9dd4 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -136,6 +136,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.core.graphics.ColorUtils;
+import androidx.dynamicanimation.animation.SpringAnimation;
 
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.AbstractFloatingView;
@@ -165,6 +166,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.IntArray;
@@ -213,9 +215,11 @@
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SingleTask;
 import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps;
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SplitTask;
 import com.android.quickstep.util.SurfaceTransaction;
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.TaskGridNavHelper;
@@ -238,6 +242,7 @@
 import kotlin.Unit;
 import kotlin.collections.CollectionsKt;
 
+import kotlin.jvm.functions.Function0;
 import kotlinx.coroutines.CoroutineScope;
 
 import java.util.ArrayList;
@@ -1952,7 +1957,7 @@
             GroupTask groupTask = taskGroups.get(i);
             boolean containsStagedTask = stagedTaskIdToBeRemoved != INVALID_TASK_ID
                     && groupTask.containsTask(stagedTaskIdToBeRemoved);
-            boolean shouldSkipGroupTask = containsStagedTask && !groupTask.hasMultipleTasks();
+            boolean shouldSkipGroupTask = containsStagedTask && groupTask instanceof SingleTask;
 
             if ((isSplitSelectionActive() && groupTask.taskViewType == TaskViewType.DESKTOP)
                     || shouldSkipGroupTask) {
@@ -1966,25 +1971,27 @@
             // to be a temporary container for the remaining task.
             TaskView taskView = getTaskViewFromPool(
                     containsStagedTask ? TaskViewType.SINGLE : groupTask.taskViewType);
-            if (taskView instanceof GroupedTaskView) {
-                boolean firstTaskIsLeftTopTask =
-                        groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
-                Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
-                Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
-                ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
-                        mTaskOverlayFactory, groupTask.mSplitBounds);
-            } else if (taskView instanceof DesktopTaskView) {
+            if (taskView instanceof GroupedTaskView groupedTaskView) {
+                var splitTask = (SplitTask) groupTask;
+                groupedTaskView.bind(splitTask.getTopLeftTask(),
+                        splitTask.getBottomRightTask(), mOrientationState,
+                        mTaskOverlayFactory, splitTask.getSplitBounds());
+            } else if (taskView instanceof DesktopTaskView desktopTaskView) {
                 // Minimized tasks should not be shown in Overview
                 List<Task> nonMinimizedTasks =
                         groupTask.getTasks().stream()
                                 .filter(task -> !task.isMinimized)
                                 .toList();
-                ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
+                desktopTaskView.bind(nonMinimizedTasks, mOrientationState,
                         mTaskOverlayFactory);
-            } else {
-                Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
-                        : groupTask.task1;
+            } else if (groupTask instanceof SplitTask splitTask) {
+                Task task = splitTask.getTopLeftTask().key.id == stagedTaskIdToBeRemoved
+                        ? splitTask.getBottomRightTask()
+                        : splitTask.getTopLeftTask();
                 taskView.bind(task, mOrientationState, mTaskOverlayFactory);
+            } else {
+                taskView.bind(((SingleTask) groupTask).getTask(), mOrientationState,
+                        mTaskOverlayFactory);
             }
             addView(taskView);
 
@@ -5699,6 +5706,13 @@
                 mTempRect, mContainer.getDeviceProfile(), mTempPointF);
     }
 
+    /**
+     * Clears the existing PendingAnimation.
+     */
+    public void clearPendingAnimation() {
+        mPendingAnimation = null;
+    }
+
     public PendingAnimation createTaskLaunchAnimation(
             TaskView taskView, long duration, Interpolator interpolator) {
         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
@@ -5860,6 +5874,10 @@
         mEnableDrawingLiveTile = enableDrawingLiveTile;
     }
 
+    public boolean getEnableDrawingLiveTile() {
+        return mEnableDrawingLiveTile;
+    }
+
     public void redrawLiveTile() {
         runActionOnRemoteHandles(remoteTargetHandle -> {
             TransformParams params = remoteTargetHandle.getTransformParams();
@@ -6893,6 +6911,19 @@
         return Typeface.Builder.NORMAL_WEIGHT;
     }
 
+    /**
+     * Creates the spring animations which run as a task settles back into its place in overview.
+     *
+     * <p>When a task dismiss is cancelled, the task will return to its original position via a
+     * spring animation.
+     */
+    public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
+            float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
+            int dismissLength, Function0<Unit> onEndRunnable) {
+        return mUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity,
+                isDismissing, detector, dismissLength, onEndRunnable);
+    }
+
     public interface TaskLaunchListener {
         void onTaskLaunched();
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index b1a4808..e61d402 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -63,19 +63,6 @@
     <T extends View> T getOverviewPanel();
 
     /**
-     * Returns the RootView
-     */
-    View getRootView();
-
-    /**
-     * Dispatches a generic motion event to the view hierarchy.
-     * Returns the current RecentsViewContainer as context
-     */
-    default Context asContext() {
-        return (Context) this;
-    }
-
-    /**
      * @see Window.Callback#dispatchGenericMotionEvent(MotionEvent)
      */
     boolean dispatchGenericMotionEvent(MotionEvent ev);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 94e8c03..f610335 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -19,14 +19,21 @@
 import android.graphics.Rect
 import android.view.View
 import androidx.core.view.children
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
+import com.android.launcher3.R
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.DynamicResource
 import com.android.launcher3.util.IntArray
 import com.android.quickstep.util.GroupTask
 import com.android.quickstep.util.isExternalDisplay
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
 import java.util.function.BiConsumer
+import kotlin.math.abs
 
 /**
  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
@@ -294,6 +301,58 @@
         }
     }
 
+    /**
+     * Creates the spring animations which run when a dragged task view in overview is released.
+     *
+     * <p>When a task dismiss is cancelled, the task will return to its original position via a
+     * spring animation.
+     */
+    fun createTaskDismissSettlingSpringAnimation(
+        draggedTaskView: TaskView?,
+        velocity: Float,
+        isDismissing: Boolean,
+        detector: SingleAxisSwipeDetector,
+        dismissLength: Int,
+        onEndRunnable: () -> Unit,
+    ): SpringAnimation? {
+        draggedTaskView ?: return null
+        val taskDismissFloatProperty =
+            FloatPropertyCompat.createFloatPropertyCompat(
+                draggedTaskView.secondaryDismissTranslationProperty
+            )
+        val rp = DynamicResource.provider(recentsView.mContainer)
+        return SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+            .setSpring(
+                SpringForce()
+                    .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
+                    .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness))
+            )
+            .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
+            .addUpdateListener { animation, value, _ ->
+                if (isDismissing && abs(value) >= abs(dismissLength)) {
+                    // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
+                    draggedTaskView.alpha = 0f
+                    animation.cancel()
+                } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+                    recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+                        remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+                            taskDismissFloatProperty.getValue(draggedTaskView)
+                    }
+                    recentsView.redrawLiveTile()
+                }
+            }
+            .addEndListener { _, _, _, _ ->
+                if (isDismissing) {
+                    recentsView.dismissTask(
+                        draggedTaskView,
+                        /* animateTaskView = */ false,
+                        /* removeTask = */ true,
+                    )
+                }
+                onEndRunnable()
+            }
+    }
+
     companion object {
         val TEMP_RECT = Rect()
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index a9e84ef..0350c78 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -153,7 +153,10 @@
     }
 
     fun setState(state: TaskData?, liveTile: Boolean, hasHeader: Boolean) {
-        thumbnailView.setState(TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader))
+        thumbnailView.setState(
+            TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader),
+            state?.taskId,
+        )
         splitAnimationThumbnail =
             if (state is TaskData.Data) state.thumbnailData?.thumbnail else null
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 9807b0d..062955b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -36,7 +36,6 @@
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.view.accessibility.AccessibilityNodeInfo
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import android.widget.FrameLayout
 import android.widget.Toast
 import androidx.annotation.IntDef
@@ -214,7 +213,7 @@
         get() =
             pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
 
-    protected val secondaryDismissTranslationProperty: FloatProperty<TaskView>
+    val secondaryDismissTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
 
@@ -669,13 +668,6 @@
             val shouldPopulateAccessibilityMenu =
                 modalness == 0f && recentsView?.isSplitSelectionActive == false
             if (shouldPopulateAccessibilityMenu) {
-                addAction(
-                    AccessibilityAction(
-                        R.id.action_close,
-                        context.getText(R.string.accessibility_close),
-                    )
-                )
-
                 taskContainers.forEach {
                     TraceHelper.allowIpcs("TV.a11yInfo") {
                         TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut
@@ -706,11 +698,6 @@
 
     override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
         // TODO(b/343708271): Add support for multiple tasks per action.
-        if (action == R.id.action_close) {
-            recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/)
-            return true
-        }
-
         taskContainers.forEach {
             if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) {
                 return true
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index e150568..90c9553 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -125,7 +125,8 @@
                     // Needs to be set on window context instead of sandbox context, because it does
                     // does not propagate between them. However, this change will impact created
                     // TaskbarActivityContext instances, since they wrap the window context.
-                    taskbarManager.windowContext.resources.configuration.setLayoutDirection(
+                    // TODO: iterate through all window contexts and do this.
+                    taskbarManager.primaryWindowContext.resources.configuration.setLayoutDirection(
                         RTL_LOCALE
                     )
                 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt
new file mode 100644
index 0000000..9b0a95a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.widget.picker
+
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WidgetCategoryFilterTest {
+
+    @Test
+    fun filterValueZero_everythingMatches() {
+        val noFilter = WidgetCategoryFilter(categoryMask = 0)
+
+        noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+        noFilter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+        noFilter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD)
+        noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+        noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD)
+        noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_NOT_KEYGUARD)
+        noFilter.assertMatches(
+            WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD
+        )
+        noFilter.assertMatches(
+            WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_NOT_KEYGUARD
+        )
+        noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+    }
+
+    @Test
+    fun includeHomeScreen_matchesOnlyIfHomeScreenExists() {
+        val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN)
+
+        filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX)
+    }
+
+    @Test
+    fun includeHomeScreenOrKeyguard_matchesIfEitherHomeScreenOrKeyguardExists() {
+        val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD)
+
+        filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+        filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+        filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+    }
+
+    @Test
+    fun excludeNotKeyguard_doesNotMatchIfNotKeyguardExists() {
+        val filter = WidgetCategoryFilter(WIDGET_CATEGORY_NOT_KEYGUARD.inv())
+
+        filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+        filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+        filter.assertDoesNotMatch(
+            WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN
+        )
+    }
+
+    @Test
+    fun multipleExclusions_doesNotMatchIfExcludedCategoriesExist() {
+        val filter =
+            WidgetCategoryFilter(
+                WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
+            )
+
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+        filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+        filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+        filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+        filter.assertDoesNotMatch(
+            WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN
+        )
+    }
+
+    private fun WidgetCategoryFilter.assertMatches(category: Int) {
+        assertThat(matches(category)).isTrue()
+    }
+
+    private fun WidgetCategoryFilter.assertDoesNotMatch(category: Int) {
+        assertThat(matches(category)).isFalse()
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
index 99a1c59..722e1da 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SplitTask;
@@ -76,6 +77,12 @@
     @Mock
     private HighResLoadingState mHighResLoadingState;
 
+    @Mock
+    private LockedUserState mLockedUserState;
+
+    @Mock
+    private ThemeManager mThemeManager;
+
     private RecentsModel mRecentsModel;
 
     private RecentTasksList.TaskLoadResult mTaskResult;
@@ -102,7 +109,7 @@
 
         mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class),
                 mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class),
-                mock(ThemeManager.class));
+                mLockedUserState, () -> mThemeManager);
 
         mResource = mock(Resources.class);
         when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
@@ -167,6 +174,17 @@
                 .updateThumbnailInCache(any(), anyBoolean());
     }
 
+    @Test
+    public void themeCallbackAttachedOnUnlock() {
+        verify(mThemeManager, never()).addChangeListener(any());
+
+        ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mLockedUserState).runOnUserUnlocked(callbackCaptor.capture());
+
+        callbackCaptor.getAllValues().forEach(Runnable::run);
+        verify(mThemeManager, times(1)).addChangeListener(any());
+    }
+
     private RecentTasksList.TaskLoadResult getTaskResult() {
         RecentTasksList.TaskLoadResult allTasks = new RecentTasksList.TaskLoadResult(0, false, 1);
         ActivityManager.RecentTaskInfo taskInfo1 = new ActivityManager.RecentTaskInfo();
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 0491c07..e4bdba5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -102,12 +102,12 @@
     fun activeTasks_noMatchingTasks() {
         val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
         val groupTask1 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pomegranate", "juice"),
                 ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("hotdog", "juice"),
                 ComponentName("personal", "computer"),
             )
@@ -143,12 +143,12 @@
         val matchingComponent =
             ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
         val groupTask1 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName(matchingPackage, matchingClass),
                 ComponentName("pomegranate", "juice"),
             )
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pumpkin", "pie"),
                 ComponentName("personal", "computer"),
             )
@@ -170,7 +170,7 @@
                     it[0].key.baseIntent.component?.className,
                     matchingClass,
                 )
-                assertEquals(it[0], groupTask1.task1)
+                assertEquals(it[0], groupTask1.topLeftTask)
             }
 
         // Capture callback from recentsModel#getTasks()
@@ -196,12 +196,12 @@
         val nonPrimaryUserComponent =
             ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle)
         val groupTask1 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName(matchingPackage, matchingClass),
                 ComponentName("pomegranate", "juice"),
             )
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pumpkin", "pie"),
                 ComponentName("personal", "computer"),
             )
@@ -237,14 +237,14 @@
         val nonPrimaryUserComponent =
             ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle)
         val groupTask1 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName(matchingPackage, matchingClass),
                 nonPrimaryUserHandle,
                 ComponentName("pomegranate", "juice"),
                 nonPrimaryUserHandle,
             )
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pumpkin", "pie"),
                 ComponentName("personal", "computer"),
             )
@@ -267,7 +267,7 @@
                     matchingClass,
                 )
                 assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
-                assertEquals(it[0], groupTask1.task1)
+                assertEquals(it[0], groupTask1.topLeftTask)
             }
 
         // Capture callback from recentsModel#getTasks()
@@ -293,12 +293,12 @@
         val matchingComponent =
             ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
         val groupTask1 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName(matchingPackage, matchingClass),
                 ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pomegranate", "juice"),
                 ComponentName(matchingPackage, matchingClass),
             )
@@ -320,7 +320,7 @@
                     it[0].key.baseIntent.component?.className,
                     matchingClass,
                 )
-                assertEquals(it[0], groupTask1.task1)
+                assertEquals(it[0], groupTask1.topLeftTask)
             }
 
         // Capture callback from recentsModel#getTasks()
@@ -348,9 +348,9 @@
             ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
 
         val groupTask1 =
-            generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+            generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pomegranate", "juice"),
                 ComponentName(matchingPackage, matchingClass),
             )
@@ -374,7 +374,7 @@
                     it[1].key.baseIntent.component?.className,
                     matchingClass,
                 )
-                assertEquals(it[1], groupTask2.task2)
+                assertEquals(it[1], groupTask2.bottomRightTask)
             }
 
         // Capture callback from recentsModel#getTasks()
@@ -401,9 +401,9 @@
             ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
 
         val groupTask1 =
-            generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+            generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pomegranate", "juice"),
                 ComponentName(matchingPackage, matchingClass),
             )
@@ -426,7 +426,7 @@
                     it[0].key.baseIntent.component?.className,
                     matchingClass,
                 )
-                assertEquals(it[0], groupTask2.task2)
+                assertEquals(it[0], groupTask2.bottomRightTask)
                 assertNull("No tasks should have matched", it[1] /*task*/)
             }
 
@@ -454,12 +454,12 @@
             ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
 
         val groupTask1 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName(matchingPackage, matchingClass),
                 ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("pomegranate", "juice"),
                 ComponentName(matchingPackage, matchingClass),
             )
@@ -482,7 +482,7 @@
                     it[0].key.baseIntent.component?.className,
                     matchingClass,
                 )
-                assertEquals(it[0], groupTask1.task1)
+                assertEquals(it[0], groupTask1.topLeftTask)
                 assertEquals(
                     "ComponentName package mismatched",
                     it[1].key.baseIntent.component?.packageName,
@@ -493,7 +493,7 @@
                     it[1].key.baseIntent.component?.className,
                     matchingClass,
                 )
-                assertEquals(it[1], groupTask2.task2)
+                assertEquals(it[1], groupTask2.bottomRightTask)
             }
 
         // Capture callback from recentsModel#getTasks()
@@ -524,14 +524,14 @@
             ComponentKey(ComponentName(matchingPackage2, matchingClass2), primaryUserHandle)
 
         val groupTask1 =
-            generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+            generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
         val groupTask2 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName(matchingPackage2, matchingClass2),
                 ComponentName(matchingPackage, matchingClass),
             )
         val groupTask3 =
-            generateGroupTask(
+            generateSplitTask(
                 ComponentName("hotdog", "pie"),
                 ComponentName(matchingPackage, matchingClass),
             )
@@ -545,7 +545,7 @@
         val taskConsumer =
             Consumer<Array<Task>> {
                 assertEquals("Expected array length 2", 2, it.size)
-                assertEquals("Found wrong task", it[0], groupTask2.task1)
+                assertEquals("Found wrong task", it[0], groupTask2.topLeftTask)
             }
 
         // Capture callback from recentsModel#getTasks()
@@ -640,11 +640,11 @@
         verify(recentsView, times(0)).resetDesktopTaskFromSplitSelectState()
     }
 
-    // Generate GroupTask with default userId.
-    private fun generateGroupTask(
+    /** Generates a [SplitTask] with default userId. */
+    private fun generateSplitTask(
         task1ComponentName: ComponentName,
         task2ComponentName: ComponentName,
-    ): GroupTask {
+    ): SplitTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
         taskInfo.taskId = getUniqueId()
@@ -666,20 +666,20 @@
             SplitConfigurationOptions.SplitBounds(
                 /* leftTopBounds = */ Rect(),
                 /* rightBottomBounds = */ Rect(),
-                /* leftTopTaskId = */ -1,
-                /* rightBottomTaskId = */ -1,
+                /* leftTopTaskId = */ task1.key.id,
+                /* rightBottomTaskId = */ task2.key.id,
                 /* snapPosition = */ SNAP_TO_2_50_50,
             ),
         )
     }
 
-    // Generate GroupTask with custom user handles.
-    private fun generateGroupTask(
+    /** Generates a [SplitTask] with custom user handles. */
+    private fun generateSplitTask(
         task1ComponentName: ComponentName,
         userHandle1: UserHandle,
         task2ComponentName: ComponentName,
         userHandle2: UserHandle,
-    ): GroupTask {
+    ): SplitTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
         taskInfo.taskId = getUniqueId()
@@ -704,8 +704,8 @@
             SplitConfigurationOptions.SplitBounds(
                 /* leftTopBounds = */ Rect(),
                 /* rightBottomBounds = */ Rect(),
-                /* leftTopTaskId = */ -1,
-                /* rightBottomTaskId = */ -1,
+                /* leftTopTaskId = */ task1.key.id,
+                /* rightBottomTaskId = */ task2.key.id,
                 /* snapPosition = */ SNAP_TO_2_50_50,
             ),
         )
diff --git a/res/layout/work_mode_utility_view.xml b/res/layout/work_mode_utility_view.xml
index fc112ce..b68ff3e 100644
--- a/res/layout/work_mode_utility_view.xml
+++ b/res/layout/work_mode_utility_view.xml
@@ -14,6 +14,7 @@
   ~ limitations under the License.
   -->
 <com.android.launcher3.allapps.WorkUtilityView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/work_utility_view"
     android:layout_height="wrap_content"
     android:layout_width="wrap_content"
     android:orientation="vertical"
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index a925866..0b8e52a 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -38,13 +38,13 @@
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"لا يمكن استخدام هذين التطبيقَين في الوقت نفسه على هذا الجهاز"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"افتح الجهاز لاستخدام هذين التطبيقَين في الوقت نفسه"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"ميزة \"استخدام تطبيقين في الوقت نفسه\" غير متوفّرة"</string>
-    <string name="long_press_widget_to_add" msgid="3587712543577675817">"انقر مع الاستمرار لنقل أداة."</string>
-    <string name="long_accessible_way_to_add" msgid="2733588281439571974">"انقر مرتين مع تثبيت إصبعك لنقل أداة أو استخدام الإجراءات المخصّصة."</string>
+    <string name="long_press_widget_to_add" msgid="3587712543577675817">"انقر مع الاستمرار لنقل تطبيق مصغَّر."</string>
+    <string name="long_accessible_way_to_add" msgid="2733588281439571974">"انقر مرتين مع تثبيت إصبعك لنقل تطبيق مصغَّر أو استخدام الإجراءات المخصّصة."</string>
     <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"خيارات إضافية"</string>
     <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"عرض كل التطبيقات المصغّرة"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"‏العرض %1$d الطول %2$d"</string>
-    <string name="widget_preview_context_description" msgid="9045841361655787574">"أداة <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <string name="widget_preview_context_description" msgid="9045841361655787574">"التطبيق المصغَّر <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
     <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"‏التطبيق المصغّرة \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\"، بعرض ‎%2$d وارتفاع ‎%3$d"</string>
     <string name="add_item_request_drag_hint" msgid="8730547755622776606">"يُرجى النقر مع الاستمرار على التطبيق المصغّر لنقله إلى الشاشة الرئيسية"</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"إضافة إلى الشاشة الرئيسية"</string>
@@ -75,8 +75,8 @@
     <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"عرض الكل"</string>
     <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"عرض كل التطبيقات المصغّرة"</string>
     <string name="widgets_list_expanded" msgid="7374857868788557730">"جارٍ عرض كل التطبيقات المصغّرة"</string>
-    <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"انقر لتغيير إعدادات الأداة"</string>
-    <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغيير إعدادات الأداة"</string>
+    <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"انقر لتغيير إعدادات التطبيق المصغَّر"</string>
+    <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغيير إعدادات التطبيق المصغَّر"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"بحث في التطبيقات"</string>
     <string name="all_apps_loading_message" msgid="5813968043155271636">"جارٍ تحميل التطبيقات…"</string>
     <string name="all_apps_no_search_results" msgid="3200346862396363786">"لم يتم العثور على أي تطبيقات تتطابق مع \"<xliff:g id="QUERY">%1$s</xliff:g>\""</string>
@@ -108,7 +108,7 @@
     <string name="permdesc_read_settings" msgid="4208061150510996676">"يسمح هذا الإذن للتطبيق بالاطلاع على الإعدادات والاختصارات على الشاشة الرئيسية."</string>
     <string name="permlab_write_settings" msgid="4820028712156303762">"تعديل الإعدادات والاختصارات على الشاشة الرئيسية"</string>
     <string name="permdesc_write_settings" msgid="726859348127868466">"يسمح هذا الإذن للتطبيق بتغيير الإعدادات والاختصارات على الشاشة الرئيسية."</string>
-    <string name="gadget_error_text" msgid="740356548025791839">"يتعذّر تحميل الأداة."</string>
+    <string name="gadget_error_text" msgid="740356548025791839">"يتعذّر تحميل التطبيق المصغَّر."</string>
     <string name="gadget_setup_text" msgid="8348374825537681407">"إعدادات التطبيق المصغّر"</string>
     <string name="gadget_complete_setup_text" msgid="309040266978007925">"انقر لإكمال الإعداد."</string>
     <string name="uninstall_system_app_text" msgid="4172046090762920660">"هذا تطبيق نظام وتتعذر إزالته."</string>
@@ -181,7 +181,7 @@
     <string name="action_increase_height" msgid="459390020612501122">"زيادة الارتفاع"</string>
     <string name="action_decrease_width" msgid="1374549771083094654">"تقليل العرض"</string>
     <string name="action_decrease_height" msgid="282377193880900022">"تقليل الارتفاع"</string>
-    <string name="widget_resized" msgid="9130327887929620">"تم تغيير حجم الأداة إلى العرض <xliff:g id="NUMBER_0">%1$s</xliff:g> والارتفاع <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
+    <string name="widget_resized" msgid="9130327887929620">"تم تغيير حجم التطبيق المصغَّر إلى العرض <xliff:g id="NUMBER_0">%1$s</xliff:g> والارتفاع <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
     <string name="action_deep_shortcut" msgid="4766835855579976045">"قائمة الاختصارات"</string>
     <string name="action_dismiss_notification" msgid="5909461085055959187">"تجاهل"</string>
     <string name="accessibility_close" msgid="2277148124685870734">"إغلاق"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 754bd76..1dad001 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -46,7 +46,7 @@
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Szerokość %1$d, wysokość %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
     <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d (szerokość), %3$d (wysokość)"</string>
-    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Aby poruszać widżetem po ekranie głównym, kliknij go i przytrzymaj"</string>
+    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Aby przesunąć widżet na ekranie głównym, kliknij go i przytrzymaj"</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"Dodaj do ekranu głównego"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g> został dodany do ekranu głównego"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Sugestie"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e06895c..f740489 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -174,7 +174,8 @@
 
     <declare-styleable name="GridDisplayOption">
         <attr name="name" format="string" />
-        <attr name="title" />
+        <attr name="gridTitle" format="string" />
+        <attr name="gridIconId" format="reference"/>
 
         <attr name="numRows" format="integer" />
         <attr name="numColumns" format="integer" />
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 2e75261..6277e41 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
@@ -29,17 +30,27 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.util.Log;
+import android.view.ActionMode;
+import android.view.View;
 import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+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.RunnableList;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.ScrimView;
 
@@ -52,7 +63,8 @@
 /**
  * Launcher BaseActivity
  */
-public abstract class BaseActivity extends Activity implements ActivityContext {
+public abstract class BaseActivity extends Activity implements ActivityContext,
+        DisplayInfoChangeListener {
 
     private static final String TAG = "BaseActivity";
     static final boolean DEBUG = false;
@@ -126,6 +138,10 @@
     public @interface ActivityFlags {
     }
 
+    // When starting an action mode, setting this tag will cause the action mode to be cancelled
+    // automatically when user interacts with the launcher.
+    public static final Object AUTO_CANCEL_ACTION_MODE = new Object();
+
     /** Returns a human-readable string for the specified {@link ActivityFlags}. */
     public static String getActivityStateString(@ActivityFlags int flags) {
         StringJoiner result = new StringJoiner("|");
@@ -160,6 +176,8 @@
     private final RunnableList[] mEventCallbacks =
             {new RunnableList(), new RunnableList(), new RunnableList(), new RunnableList()};
 
+    private ActionMode mCurrentActionMode;
+
     @Override
     public ViewCache getViewCache() {
         return mViewCache;
@@ -206,6 +224,7 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         registerBackDispatcher();
+        DisplayController.INSTANCE.get(this).addChangeListener(this);
     }
 
     @Override
@@ -253,6 +272,7 @@
     protected void onDestroy() {
         super.onDestroy();
         mEventCallbacks[EVENT_DESTROYED].executeAllAndClear();
+        DisplayController.INSTANCE.get(this).removeChangeListener(this);
     }
 
     @Override
@@ -403,6 +423,61 @@
         writer.println(prefix + "mForceInvisible: " + mForceInvisible);
     }
 
+
+    @Override
+    public void onActionModeStarted(ActionMode mode) {
+        super.onActionModeStarted(mode);
+        mCurrentActionMode = mode;
+    }
+
+    @Override
+    public void onActionModeFinished(ActionMode mode) {
+        super.onActionModeFinished(mode);
+        mCurrentActionMode = null;
+    }
+
+    protected boolean isInAutoCancelActionMode() {
+        return mCurrentActionMode != null && AUTO_CANCEL_ACTION_MODE == mCurrentActionMode.getTag();
+    }
+
+    @Override
+    public boolean finishAutoCancelActionMode() {
+        if (isInAutoCancelActionMode()) {
+            mCurrentActionMode.finish();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    @NonNull
+    public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
+        ActivityOptionsWrapper wrapper = ActivityContext.super.getActivityLaunchOptions(v, item);
+        addEventCallback(EVENT_RESUMED, wrapper.onEndCallback::executeAllAndDestroy);
+        return wrapper;
+    }
+
+    @Override
+    public ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) {
+        ActivityOptionsWrapper wrapper =
+                ActivityContext.super.makeDefaultActivityOptions(splashScreenStyle);
+        addEventCallback(EVENT_RESUMED, wrapper.onEndCallback::executeAllAndDestroy);
+        return wrapper;
+    }
+
+    protected WindowBounds getMultiWindowDisplaySize() {
+        return WindowBounds.fromWindowMetrics(getWindowManager().getCurrentWindowMetrics());
+    }
+
+    @Override
+    public void onDisplayInfoChanged(Context context, Info info, int flags) {
+        if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.isVerticalBarLayout()) {
+            reapplyUi();
+        }
+    }
+
+    protected void reapplyUi() {}
+
     public static <T extends BaseActivity> T fromContext(Context context) {
         if (context instanceof BaseActivity) {
             return (T) context;
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
deleted file mode 100644
index 3b93cf4..0000000
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ /dev/null
@@ -1,171 +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.launcher3;
-
-import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.view.ActionMode;
-import android.view.View;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.util.ActivityOptionsWrapper;
-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.OnColorHintListener;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.WallpaperColorHints;
-import com.android.launcher3.util.WindowBounds;
-
-/**
- * Extension of BaseActivity allowing support for drag-n-drop
- */
-@SuppressWarnings("NewApi")
-public abstract class BaseDraggingActivity extends BaseActivity
-        implements OnColorHintListener, DisplayInfoChangeListener {
-
-    // When starting an action mode, setting this tag will cause the action mode to be cancelled
-    // automatically when user interacts with the launcher.
-    public static final Object AUTO_CANCEL_ACTION_MODE = new Object();
-
-    private ActionMode mCurrentActionMode;
-
-    private int mThemeRes = R.style.AppTheme;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        DisplayController.INSTANCE.get(this).addChangeListener(this);
-
-        // Update theme
-        WallpaperColorHints.get(this).registerOnColorHintsChangedListener(this);
-        int themeRes = Themes.getActivityThemeRes(this);
-        if (themeRes != mThemeRes) {
-            mThemeRes = themeRes;
-            setTheme(themeRes);
-        }
-    }
-
-    @MainThread
-    @Override
-    public void onColorHintsChanged(int colorHints) {
-        updateTheme();
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        updateTheme();
-    }
-
-    private void updateTheme() {
-        if (mThemeRes != Themes.getActivityThemeRes(this)) {
-            recreateToUpdateTheme();
-        }
-    }
-
-    protected void recreateToUpdateTheme() {
-        recreate();
-    }
-
-    @Override
-    public void onActionModeStarted(ActionMode mode) {
-        super.onActionModeStarted(mode);
-        mCurrentActionMode = mode;
-    }
-
-    @Override
-    public void onActionModeFinished(ActionMode mode) {
-        super.onActionModeFinished(mode);
-        mCurrentActionMode = null;
-    }
-
-    protected boolean isInAutoCancelActionMode() {
-        return mCurrentActionMode != null && AUTO_CANCEL_ACTION_MODE == mCurrentActionMode.getTag();
-    }
-
-    @Override
-    public boolean finishAutoCancelActionMode() {
-        if (isInAutoCancelActionMode()) {
-            mCurrentActionMode.finish();
-            return true;
-        }
-        return false;
-    }
-
-    public abstract View getRootView();
-
-    public void returnToHomescreen() {
-        // no-op
-    }
-
-    @Override
-    @NonNull
-    public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
-        ActivityOptionsWrapper wrapper = super.getActivityLaunchOptions(v, item);
-        addEventCallback(EVENT_RESUMED, wrapper.onEndCallback::executeAllAndDestroy);
-        return wrapper;
-    }
-
-    @Override
-    public ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) {
-        ActivityOptionsWrapper wrapper = super.makeDefaultActivityOptions(splashScreenStyle);
-        addEventCallback(EVENT_RESUMED, wrapper.onEndCallback::executeAllAndDestroy);
-        return wrapper;
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        DisplayController.INSTANCE.get(this).removeChangeListener(this);
-        WallpaperColorHints.get(this).unregisterOnColorsChangedListener(this);
-    }
-
-    protected void onDeviceProfileInitiated() {
-    }
-
-    @Override
-    public void onDisplayInfoChanged(Context context, Info info, int flags) {
-        if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.isVerticalBarLayout()) {
-            reapplyUi();
-        }
-    }
-
-    @Override
-    public View.OnClickListener getItemOnClickListener() {
-        return ItemClickHandler.INSTANCE;
-    }
-
-    protected abstract void reapplyUi();
-
-    protected WindowBounds getMultiWindowDisplaySize() {
-        return WindowBounds.fromWindowMetrics(getWindowManager().getCurrentWindowMetrics());
-    }
-
-    @Override
-    public boolean isAppBlockedForSafeMode() {
-        return LauncherAppState.getInstance(this).isSafeModeEnabled();
-    }
-}
diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index 66c948a..0cc7fc7 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -2,7 +2,7 @@
 
 import android.content.ComponentName
 import android.view.View
-import com.android.launcher3.BaseDraggingActivity.EVENT_RESUMED
+import com.android.launcher3.BaseActivity.EVENT_RESUMED
 import com.android.launcher3.DropTarget.DragObject
 import com.android.launcher3.LauncherConstants.ActivityCodes
 import com.android.launcher3.SecondaryDropTarget.DeferredOnComplete
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 753e017..e47a44a 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -993,7 +993,8 @@
         private static final int DONT_INLINE_QSB = 0;
 
         public final String name;
-        public final String title;
+        public final String gridTitle;
+        public final int gridIconId;
         public final int numRows;
         public final int numColumns;
         public final int numSearchContainerColumns;
@@ -1042,7 +1043,9 @@
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.GridDisplayOption);
             name = a.getString(R.styleable.GridDisplayOption_name);
-            title = a.getString(R.styleable.GridDisplayOption_title);
+            gridTitle = a.getString(R.styleable.GridDisplayOption_gridTitle);
+            gridIconId = a.getResourceId(
+                    R.styleable.GridDisplayOption_gridIconId, INVALID_RESOURCE_HANDLE);
             deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
                     DEVICE_CATEGORY_ALL);
             mGridSizeSpecsId = a.getResourceId(
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 647d2ad..a7a68d1 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -102,6 +102,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch;
 import static com.android.launcher3.util.SettingsCache.TOUCHPAD_NATURAL_SCROLLING;
+import static com.android.launcher3.util.WallpaperThemeManager.setWallpaperDependentTheme;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -144,6 +145,7 @@
 import android.view.Menu;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver.OnPreDrawListener;
 import android.view.WindowInsets;
@@ -222,6 +224,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
+import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.ActivityResultInfo;
 import com.android.launcher3.util.BackPressHandler;
@@ -509,6 +512,7 @@
         }
 
         super.onCreate(savedInstanceState);
+        setWallpaperDependentTheme(this);
 
         LauncherAppState app = LauncherAppState.getInstance(this);
         mModel = app.getModel();
@@ -819,7 +823,6 @@
                     this, getMultiWindowDisplaySize());
         }
 
-        onDeviceProfileInitiated();
         if (FOLDABLE_SINGLE_PAGE.get() && mDeviceProfile.isTwoPanels) {
             mCellPosMapper = new TwoPanelCellPosMapper(mDeviceProfile.inv.numColumns);
         } else {
@@ -2840,12 +2843,6 @@
         // Overridden
     }
 
-    @Override
-    public void returnToHomescreen() {
-        super.returnToHomescreen();
-        getStateManager().goToState(LauncherState.NORMAL);
-    }
-
     public void closeOpenViews() {
         closeOpenViews(true);
     }
@@ -3170,5 +3167,10 @@
         return findViewById(R.id.popup_container);
     }
 
+    @Override
+    public OnClickListener getItemOnClickListener() {
+        return ItemClickHandler.INSTANCE;
+    }
+
     // End of Getters and Setters
 }
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index a5b95c7..b8a0abd 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -12,6 +12,7 @@
 import com.android.launcher3.graphics.SysUiScrim;
 import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.window.WindowManagerProxy;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.Collections;
 import java.util.List;
@@ -36,7 +37,7 @@
 
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mStatefulContainer = StatefulContainer.fromContext(context);
+        mStatefulContainer = ActivityContext.lookupContext(context);
         mSysUiScrim = new SysUiScrim(this);
     }
 
@@ -54,7 +55,7 @@
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mStatefulContainer.handleConfigurationChanged(
-                mStatefulContainer.getContext().getResources().getConfiguration());
+                mStatefulContainer.asContext().getResources().getConfiguration());
         return updateInsets(insets);
     }
 
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 7d5e481..8c6555e 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -118,7 +118,7 @@
             LAUNCHER_STATE_HOME,
             FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED | FLAG_HAS_SYS_UI_SCRIM) {
         @Override
-        public int getTransitionDuration(Context context, boolean isToState) {
+        public int getTransitionDuration(ActivityContext context, boolean isToState) {
             // Arbitrary duration, when going to NORMAL we use the state we're coming from instead.
             return 0;
         }
@@ -345,7 +345,7 @@
     public final  <DEVICE_PROFILE_CONTEXT extends Context & ActivityContext>
             float getDepth(DEVICE_PROFILE_CONTEXT context) {
         return getDepth(context,
-                BaseDraggingActivity.fromContext(context).getDeviceProfile().isMultiWindowMode);
+                ActivityContext.lookupContext(context).getDeviceProfile().isMultiWindowMode);
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
index bccc279..e42a6b9 100644
--- a/src/com/android/launcher3/allapps/WorkUtilityView.java
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -15,6 +15,11 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -46,6 +51,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedPropertySetter;
 import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.views.ActivityContext;
 
@@ -88,6 +94,8 @@
     private TextView mPauseText;
     private ImageView mWorkIcon;
     private ImageButton mSchedulerButton;
+    private final StatsLogManager mStatsLogManager;
+    private LinearLayout mWorkUtilityView;
 
     public WorkUtilityView(@NonNull Context context) {
         this(context, null, 0);
@@ -111,6 +119,7 @@
                 R.dimen.work_fab_icon_start_margin_expanded);
         mWorkSchedulerIntentAction = mContext.getResources().getString(
                 R.string.work_profile_scheduler_intent);
+        mStatsLogManager = mActivityContext.getStatsLogManager();
     }
 
     @Override
@@ -121,6 +130,7 @@
         mWorkIcon = findViewById(R.id.work_icon);
         mWorkFAB = findViewById(R.id.work_mode_toggle);
         mSchedulerButton = findViewById(R.id.work_scheduler);
+        mWorkUtilityView = findViewById(R.id.work_utility_view);
         setSelected(true);
         KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
                 new KeyboardInsetAnimationCallback(this);
@@ -319,6 +329,23 @@
             animatorList.add(animateSchedulerScale(isExpanding));
             animatorList.add(animateSchedulerAlpha(isExpanding));
         }
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mStatsLogManager.logger().sendToInteractionJankMonitor(
+                        isExpanding ? LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN
+                                : LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN,
+                        mWorkUtilityView);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStatsLogManager.logger().sendToInteractionJankMonitor(
+                        isExpanding ? LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END
+                                : LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END,
+                        mWorkUtilityView);
+            }
+        });
         animatorSet.playTogether(animatorList);
         animatorSet.start();
     }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index b76e098..fb48a4d 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -1708,7 +1708,7 @@
 
     @Override
     public boolean canInterceptEventsInSystemGestureRegion() {
-        return true;
+        return !mIsEditingName;
     }
 
     /**
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index c9e688b..9edc386 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -86,10 +86,13 @@
 
     private static final String TAG = "GridCustomizationsProvider";
 
+    // KEY_NAME is the name of the grid used internally while the KEY_GRID_TITLE is the translated
+    // string title of the grid.
     private static final String KEY_NAME = "name";
     private static final String KEY_GRID_TITLE = "grid_title";
     private static final String KEY_ROWS = "rows";
     private static final String KEY_COLS = "cols";
+    private static final String KEY_GRID_ICON_ID = "grid_icon_id";
     private static final String KEY_PREVIEW_COUNT = "preview_count";
     // is_default means if a certain option is currently set to the system
     private static final String KEY_IS_DEFAULT = "is_default";
@@ -161,17 +164,18 @@
             case KEY_LIST_OPTIONS: {
                 MatrixCursor cursor = new MatrixCursor(new String[]{
                         KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
-                        KEY_IS_DEFAULT});
+                        KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
                 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
                 for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
                     cursor.newRow()
                             .add(KEY_NAME, gridOption.name)
-                            .add(KEY_GRID_TITLE, gridOption.title)
+                            .add(KEY_GRID_TITLE, gridOption.gridTitle)
                             .add(KEY_ROWS, gridOption.numRows)
                             .add(KEY_COLS, gridOption.numColumns)
                             .add(KEY_PREVIEW_COUNT, 1)
                             .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
-                                    && idp.numRows == gridOption.numRows);
+                                    && idp.numRows == gridOption.numRows)
+                            .add(KEY_GRID_ICON_ID, gridOption.gridIconId);
                 }
                 return cursor;
             }
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
index d59fc19..6f1d98f 100644
--- a/src/com/android/launcher3/graphics/SysUiScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * View scrim which draws behind hotseat and workspace
@@ -94,8 +95,8 @@
 
     public SysUiScrim(View view) {
         mRoot = view;
-        mContainer = StatefulContainer.fromContext(view.getContext());
-        DisplayMetrics dm = mContainer.getContext().getResources().getDisplayMetrics();
+        mContainer = ActivityContext.lookupContext(view.getContext());
+        DisplayMetrics dm = mContainer.asContext().getResources().getDisplayMetrics();
 
         mTopMaskHeight = ResourceUtils.pxFromDp(TOP_MASK_HEIGHT_DP, dm);
         mBottomMaskHeight = ResourceUtils.pxFromDp(BOTTOM_MASK_HEIGHT_DP, dm);
@@ -173,12 +174,12 @@
 
     @Override
     public void onViewAttachedToWindow(View view) {
-        ScreenOnTracker.INSTANCE.get(mContainer.getContext()).addListener(mScreenOnListener);
+        ScreenOnTracker.INSTANCE.get(mContainer.asContext()).addListener(mScreenOnListener);
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        ScreenOnTracker.INSTANCE.get(mContainer.getContext()).removeListener(mScreenOnListener);
+        ScreenOnTracker.INSTANCE.get(mContainer.asContext()).removeListener(mScreenOnListener);
     }
 
     /**
@@ -213,7 +214,7 @@
     }
 
     private Bitmap createDitheredAlphaMask(int height, @ColorInt int[] colors, float[] positions) {
-        DisplayMetrics dm = mContainer.getContext().getResources().getDisplayMetrics();
+        DisplayMetrics dm = mContainer.asContext().getResources().getDisplayMetrics();
         int width = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_WIDTH_DP, dm);
         Bitmap dst = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
         Canvas c = new Canvas(dst);
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 6eb02ab..9a1c874 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -225,9 +225,12 @@
         @UiEvent(doc = "User tapped on desktop icon on a task menu.")
         LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP(1706),
 
-        @UiEvent(doc = "Use tapped on external display icon on a task menu,")
+        @UiEvent(doc = "User tapped on external display icon on a task menu,")
         LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP(1957),
 
+        @UiEvent(doc = "User tapped on close app on a task menu,")
+        LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP(2081),
+
         @UiEvent(doc = "User tapped on pause app system shortcut.")
         LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521),
 
@@ -856,6 +859,18 @@
 
         @UiEvent(doc = "User sets the device in Fixed Landscape")
         FIXED_LANDSCAPE_TOGGLE_DISABLED(2020),
+
+        @UiEvent(doc = "Work utility view expand animation started")
+        LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN(2075),
+
+        @UiEvent(doc = "Work utility view expand animation ended")
+        LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END(2076),
+
+        @UiEvent(doc = "Work utility view shrink animation started")
+        LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN(2077),
+
+        @UiEvent(doc = "Work utility view shrink animation ended")
+        LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END(2078),
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index ddc775d..eab28b7 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.CollectionInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -210,7 +211,7 @@
     }
 
     /**
-     * Updates the deep shortucts state in system to match out internal model, pinning any missing
+     * Updates the deep shortcuts state in system to match out internal model, pinning any missing
      * shortcuts and unpinning any extra shortcuts.
      */
     public void updateShortcutPinnedState(Context context) {
@@ -266,6 +267,8 @@
                     || !systemShortcuts.containsAll(modelShortcuts)) {
                 // Update system state for this package
                 try {
+                    FileLog.d(TAG, "updateShortcutPinnedState:"
+                            + " Pinning Shortcuts: " + entry.getKey() + ": " + modelShortcuts);
                     context.getSystemService(LauncherApps.class).pinShortcuts(
                             entry.getKey(), new ArrayList<>(modelShortcuts), user);
                 } catch (SecurityException | IllegalStateException e) {
@@ -278,6 +281,9 @@
         systemMap.keySet().forEach(packageName -> {
             // Update system state
             try {
+                FileLog.d(TAG, "updateShortcutPinnedState:"
+                        + " Unpinning extra Shortcuts for package: " + packageName
+                        + ": " + systemMap.get(packageName));
                 context.getSystemService(LauncherApps.class).pinShortcuts(
                         packageName, Collections.emptyList(), user);
             } catch (SecurityException | IllegalStateException e) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index d256d1b..b291421 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.Flags.oneGridSpecs;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
@@ -39,6 +38,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
@@ -127,7 +127,7 @@
             return true;
         }
 
-        boolean shouldMigrateToStrictlyTallerGrid = isDestNewDb
+        boolean shouldMigrateToStrictlyTallerGrid = (Flags.oneGridSpecs() || isDestNewDb)
                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
                 && srcDeviceState.getRows() < destDeviceState.getRows();
         if (shouldMigrateToStrictlyTallerGrid) {
@@ -142,7 +142,7 @@
             if (shouldMigrateToStrictlyTallerGrid) {
                 // We want to add the extra row(s) to the top of the screen, so we shift the grid
                 // down.
-                if (oneGridSpecs()) {
+                if (Flags.oneGridSpecs()) {
                     shiftWorkspaceByXCells(
                             target.getWritableDatabase(),
                             (destDeviceState.getRows() - srcDeviceState.getRows()),
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 876919a..9586bf3 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -21,7 +21,6 @@
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags
-import com.android.launcher3.Flags.oneGridSpecs
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.get
 import com.android.launcher3.LauncherPrefs.Companion.getPrefs
@@ -81,7 +80,7 @@
                 // down.
                 if (shouldMigrateToStrtictlyTallerGrid) {
                     Log.d(TAG, "Migrating to strictly taller grid")
-                    if (oneGridSpecs()) {
+                    if (Flags.oneGridSpecs()) {
                         shiftWorkspaceByXCells(
                             target.writableDatabase,
                             (destDeviceState.rows - srcDeviceState.rows),
@@ -374,7 +373,7 @@
         srcDeviceState: DeviceGridState,
         destDeviceState: DeviceGridState,
     ): Boolean {
-        return isDestNewDb &&
+        return (Flags.oneGridSpecs() || isDestNewDb) &&
             srcDeviceState.columns == destDeviceState.columns &&
             srcDeviceState.rows < destDeviceState.rows
     }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 1623881..bd8c36b 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -24,6 +24,7 @@
 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;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -42,6 +43,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -201,7 +203,7 @@
         info.itemType = itemType;
         info.title = getTitle();
         // the fallback icon
-        if (!loadIcon(info)) {
+        if (!loadIconFromDb(info)) {
             info.bitmap = mIconCache.getDefaultIcon(info.user);
         }
 
@@ -213,15 +215,15 @@
     /**
      * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
      */
-    protected boolean loadIcon(WorkspaceItemInfo info) {
-        return createIconRequestInfo(info, false).loadWorkspaceIcon(mContext);
+    protected boolean loadIconFromDb(WorkspaceItemInfo info) {
+        return createIconRequestInfo(info, false).loadIconFromDbBlob(mContext);
     }
 
     public IconRequestInfo<WorkspaceItemInfo> createIconRequestInfo(
             WorkspaceItemInfo wai, boolean useLowResIcon) {
         byte[] iconBlob = itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT || restoreFlag != 0
+                || (wai.isInactiveArchive() && Flags.restoreArchivedAppIconsFromDb())
                 ? getIconBlob() : null;
-
         return new IconRequestInfo<>(wai, mActivityInfo, iconBlob, useLowResIcon);
     }
 
@@ -306,17 +308,17 @@
      * Make an WorkspaceItemInfo object for a restored application or shortcut item that points
      * to a package that is not yet installed on the system.
      */
-    public WorkspaceItemInfo getRestoredItemInfo(Intent intent) {
+    public WorkspaceItemInfo getRestoredItemInfo(Intent intent, boolean isArchived) {
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.user = user;
         info.intent = intent;
 
         // the fallback icon
-        if (!loadIcon(info)) {
+        if (!loadIconFromDb(info)) {
             mIconCache.getTitleAndIcon(info, DEFAULT_LOOKUP_FLAG);
         }
 
-        if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) {
+        if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON) || isArchived) {
             String title = getTitle();
             if (!TextUtils.isEmpty(title)) {
                 info.title = Utilities.trim(title);
@@ -332,6 +334,7 @@
         info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user);
         info.itemType = itemType;
         info.status = restoreFlag;
+        if (isArchived) info.runtimeStatusFlags |= FLAG_ARCHIVED;
         return info;
     }
 
@@ -375,20 +378,11 @@
         info.intent = newIntent;
         UserCache userCache = UserCache.getInstance(mContext);
         UserIconInfo userIconInfo = userCache.getUserInfo(user);
-
-        if (loadIcon) {
-            mIconCache.getTitleAndIcon(info, mActivityInfo,
-                    DEFAULT_LOOKUP_FLAG.withUseLowRes(useLowResIcon));
-            if (mIconCache.isDefaultIcon(info.bitmap, user)) {
-                loadIcon(info);
-            }
-        }
-
         if (mActivityInfo != null) {
             AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo,
                     ApiWrapper.INSTANCE.get(mContext), mPmHelper);
         }
-
+        loadWorkspaceTitleAndIcon(useLowResIcon, loadIcon, info);
         // from the db
         if (TextUtils.isEmpty(info.title)) {
             if (loadIcon) {
@@ -407,6 +401,32 @@
         return info;
     }
 
+    @VisibleForTesting
+    void loadWorkspaceTitleAndIcon(
+            boolean useLowResIcon,
+            boolean loadIconFromCache,
+            WorkspaceItemInfo info
+    ) {
+        boolean isPreArchived = Flags.enableSupportForArchiving()
+                && Flags.restoreArchivedAppIconsFromDb()
+                && info.isInactiveArchive();
+        boolean preArchivedIconNotFound = isPreArchived && !loadIconFromDb(info);
+        if (preArchivedIconNotFound) {
+            Log.d(TAG, "loadIconFromDb failed for pre-archived icon, loading from cache."
+                    + " Component=" + info.getTargetComponent());
+            mIconCache.getTitleAndIcon(info, mActivityInfo,
+                    DEFAULT_LOOKUP_FLAG.withUseLowRes(useLowResIcon));
+        } else if (loadIconFromCache && !info.isInactiveArchive()) {
+            mIconCache.getTitleAndIcon(info, mActivityInfo,
+                    DEFAULT_LOOKUP_FLAG.withUseLowRes(useLowResIcon));
+            if (mIconCache.isDefaultIcon(info.bitmap, user)) {
+                Log.d(TAG, "Default Icon found in cache, trying DB instead. "
+                        + " Component=" + info.getTargetComponent());
+                loadIconFromDb(info);
+            }
+        }
+    }
+
     /**
      * Returns a {@link ContentWriter} which can be used to update the current item.
      */
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index fee9696..3ee029b 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.icons.CacheableShortcutInfo.convertShortcutsToCacheableShortcuts;
+import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
 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_CHANGE_PERMISSION;
@@ -106,11 +107,13 @@
 import com.android.launcher3.widget.WidgetInflater;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 
@@ -154,6 +157,8 @@
     private Map<ShortcutKey, ShortcutInfo> mShortcutKeyToPinnedShortcuts;
     private HashMap<PackageUserKey, SessionInfo> mInstallingPkgsCached;
 
+    private List<IconRequestInfo<WorkspaceItemInfo>> mWorkspaceIconRequestInfos = new ArrayList<>();
+
     private boolean mStopped;
 
     private final Set<PackageUserKey> mPendingPackages = new HashSet<>();
@@ -410,7 +415,7 @@
     protected void loadWorkspace(
             List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
-            LoaderMemoryLogger memoryLogger,
+            @Nullable LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger
     ) {
         Trace.beginSection("LoadWorkspace");
@@ -474,13 +479,12 @@
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
                 queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers);
 
-                List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos = new ArrayList<>();
-
+                mWorkspaceIconRequestInfos = new ArrayList<>();
                 WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger,
                         mUserCache, mUserManagerState, mLauncherApps, mPendingPackages,
                         mShortcutKeyToPinnedShortcuts, mApp, mBgDataModel,
                         mWidgetProvidersMap, installingPkgs, isSdCardReady,
-                        widgetInflater, mPmHelper, iconRequestInfos, unlockedUsers,
+                        widgetInflater, mPmHelper, mWorkspaceIconRequestInfos, unlockedUsers,
                         allDeepShortcuts);
 
                 if (mStopped) {
@@ -490,7 +494,7 @@
                         itemProcessor.processItem();
                     }
                 }
-                tryLoadWorkspaceIconsInBulk(iconRequestInfos);
+                tryLoadWorkspaceIconsInBulk(mWorkspaceIconRequestInfos);
             } finally {
                 IOUtils.closeSilently(c);
             }
@@ -621,7 +625,9 @@
             for (IconRequestInfo<WorkspaceItemInfo> iconRequestInfo : iconRequestInfos) {
                 WorkspaceItemInfo wai = iconRequestInfo.itemInfo;
                 if (mIconCache.isDefaultIcon(wai.bitmap, wai.user)) {
-                    iconRequestInfo.loadWorkspaceIcon(mApp.getContext());
+                    logASplit("tryLoadWorkspaceIconsInBulk: default icon found for "
+                            + wai.getTargetComponent() + ", will attempt to load from iconBlob");
+                    iconRequestInfo.loadIconFromDbBlob(mApp.getContext());
                 }
             }
         } finally {
@@ -702,7 +708,7 @@
         // Clear the list of apps
         mBgAllAppsList.clear();
 
-        List<IconRequestInfo<AppInfo>> iconRequestInfos = new ArrayList<>();
+        List<IconRequestInfo<AppInfo>> allAppsItemRequestInfos = new ArrayList<>();
         boolean isWorkProfileQuiet = false;
         boolean isPrivateProfileQuiet = false;
         for (UserHandle user : profiles) {
@@ -742,15 +748,14 @@
                     }
                 }
 
-                iconRequestInfos.add(new IconRequestInfo<>(
-                        appInfo, app, /* useLowResIcon= */ false));
-                mBgAllAppsList.add(
-                        appInfo, app, false);
+                IconRequestInfo<AppInfo> iconRequestInfo = getAppInfoIconRequestInfo(
+                        appInfo, app, mWorkspaceIconRequestInfos);
+                allAppsItemRequestInfos.add(iconRequestInfo);
+                mBgAllAppsList.add(appInfo, app, false);
             }
             allActivityList.addAll(apps);
         }
 
-
         if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
             // get all active sessions and add them to the all apps list
             for (PackageInstaller.SessionInfo info :
@@ -761,7 +766,7 @@
                         false);
 
                 if (promiseAppInfo != null) {
-                    iconRequestInfos.add(new IconRequestInfo<>(
+                    allAppsItemRequestInfos.add(new IconRequestInfo<>(
                             promiseAppInfo,
                             /* launcherActivityInfo= */ null,
                             promiseAppInfo.getMatchingLookupFlag().useLowRes()));
@@ -770,9 +775,22 @@
         }
 
         Trace.beginSection("LoadAllAppsIconsInBulk");
+
         try {
-            mIconCache.getTitlesAndIconsInBulk(iconRequestInfos);
-            iconRequestInfos.forEach(iconRequestInfo ->
+            mIconCache.getTitlesAndIconsInBulk(allAppsItemRequestInfos);
+            if (Flags.restoreArchivedAppIconsFromDb()) {
+                for (IconRequestInfo<AppInfo> iconRequestInfo : allAppsItemRequestInfos) {
+                    AppInfo appInfo = iconRequestInfo.itemInfo;
+                    if (mIconCache.isDefaultIcon(appInfo.bitmap, appInfo.user)) {
+                        logASplit("LoadAllAppsIconsInBulk: default icon found for "
+                                + appInfo.getTargetComponent()
+                                + ", will attempt to load from iconBlob: "
+                                + Arrays.toString(iconRequestInfo.iconBlob));
+                        iconRequestInfo.loadIconFromDbBlob(mApp.getContext());
+                    }
+                }
+            }
+            allAppsItemRequestInfos.forEach(iconRequestInfo ->
                     mBgAllAppsList.updateSectionName(iconRequestInfo.itemInfo));
         } finally {
             Trace.endSection();
@@ -795,6 +813,51 @@
         return allActivityList;
     }
 
+    @NonNull
+    @VisibleForTesting
+    IconRequestInfo<AppInfo> getAppInfoIconRequestInfo(
+            AppInfo appInfo,
+            LauncherActivityInfo activityInfo,
+            List<IconRequestInfo<WorkspaceItemInfo>> workspaceRequestInfos
+    ) {
+        if (Flags.restoreArchivedAppIconsFromDb()) {
+            Optional<IconRequestInfo<WorkspaceItemInfo>> workspaceIconRequest =
+                    workspaceRequestInfos.stream()
+                            .filter(request -> appInfo.getTargetComponent().equals(
+                                    request.itemInfo.getTargetComponent()))
+                            .findFirst();
+
+            if (workspaceIconRequest.isPresent() && activityInfo.getApplicationInfo().isArchived) {
+                logASplit("getAppInfoIconRequestInfo:"
+                            + " matching archived info found, loading icon blob into icon request."
+                            + " Component=" + appInfo.getTargetComponent());
+                IconRequestInfo<AppInfo> iconRequestInfo = new IconRequestInfo<>(
+                        appInfo,
+                        activityInfo,
+                        workspaceIconRequest.get().iconBlob,
+                        false /* useLowResIcon= */
+                );
+                if (!iconRequestInfo.loadIconFromDbBlob(mApp.getContext())) {
+                    Log.d(TAG, "AppInfo Icon failed to load from blob, using cache.");
+                    mIconCache.getTitleAndIcon(
+                            appInfo,
+                            iconRequestInfo.launcherActivityInfo,
+                            DEFAULT_LOOKUP_FLAG
+                    );
+                }
+                return iconRequestInfo;
+            } else {
+                Log.d(TAG, "App not archived or workspace info not found"
+                        + ", creating IconRequestInfo without icon blob."
+                        + " Component:" + appInfo.getTargetComponent()
+                        + ", isArchived: " + activityInfo.getApplicationInfo().isArchived);
+            }
+        }
+        logASplit("Loading IconRequestInfo without iconBlob for AppInfo: "
+                + appInfo.getTargetComponent());
+        return new IconRequestInfo<>(appInfo, activityInfo, false /* useLowResIcon= */);
+    }
+
     private List<ShortcutInfo> loadDeepShortcuts() {
         List<ShortcutInfo> allShortcuts = new ArrayList<>();
         mBgDataModel.deepShortcutMap.clear();
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 6bef292..d1eceb9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 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;
@@ -39,7 +40,6 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
@@ -238,19 +238,22 @@
                         if (itemInfo.isPromise() && isNewApkAvailable) {
                             boolean isTargetValid = !cn.getClassName().equals(
                                     IconCache.EMPTY_CLASS_NAME);
-                            if (itemInfo.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                            if (itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
                                 List<ShortcutInfo> shortcut =
                                         new ShortcutRequest(context, mUser)
                                                 .forPackage(cn.getPackageName(),
                                                         itemInfo.getDeepShortcutId())
                                                 .query(ShortcutRequest.PINNED);
-                                if (shortcut.isEmpty()) {
+                                if (shortcut.isEmpty()
+                                        && !(Flags.restoreArchivedShortcuts()
+                                            && !itemInfo.isArchived())
+                                ) {
                                     isTargetValid = false;
                                     if (DEBUG) {
                                         Log.d(TAG, "Pinned Shortcut not found for updated"
                                                 + " package=" + itemInfo.getTargetPackage());
                                     }
-                                } else {
+                                } else if (!shortcut.isEmpty()) {
                                     if (DEBUG) {
                                         Log.d(TAG, "Found pinned shortcut for updated"
                                                 + " package=" + itemInfo.getTargetPackage()
@@ -269,7 +272,7 @@
                                     || itemInfo.isArchived())) {
                                 if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
                                     infoUpdated = true;
-                                } else if (itemInfo.hasPromiseIconUi()) {
+                                } else if (shouldRemoveRestoredShortcut(itemInfo)) {
                                     removedShortcuts.add(itemInfo.id);
                                     if (DEBUG) {
                                         FileLog.w(TAG, "Removing restored shortcut promise icon"
@@ -436,7 +439,7 @@
      */
     private boolean updateWorkspaceItemIntent(Context context,
             WorkspaceItemInfo si, String packageName) {
-        if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+        if (si.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
             // Do not update intent for deep shortcuts as they contain additional information
             // about the shortcut.
             return false;
@@ -452,6 +455,15 @@
         return false;
     }
 
+    private boolean shouldRemoveRestoredShortcut(WorkspaceItemInfo itemInfo) {
+        if (itemInfo.hasPromiseIconUi() && !Flags.restoreArchivedShortcuts()) {
+            return true;
+        }
+        return Flags.restoreArchivedShortcuts()
+                && !itemInfo.isArchived()
+                && itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT;
+    }
+
     private String getOpString() {
         return switch (mOp) {
             case OP_NONE -> "NONE";
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.kt b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
index 2e4f75f..56e9e43 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.kt
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
@@ -17,6 +17,7 @@
 
 import android.content.pm.ShortcutInfo
 import android.os.UserHandle
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherModel.ModelUpdateTask
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.icons.CacheableShortcutInfo
@@ -59,8 +60,11 @@
             val infoWrapper = ApplicationInfoWrapper(context, packageName, user)
             if (shortcuts.isEmpty()) {
                 // Verify that the app is indeed installed.
-                if (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) {
-                    // App is not installed or archived, ignoring package events
+                if (
+                    (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) ||
+                        (Flags.restoreArchivedShortcuts() && infoWrapper.isArchived())
+                ) {
+                    // App is not installed or is archived, ignoring package events
                     return
                 }
             }
@@ -75,7 +79,7 @@
             val nonPinnedIds: MutableSet<String> = HashSet(allLauncherKnownIds)
             val updatedWorkspaceItemInfos = ArrayList<WorkspaceItemInfo>()
             for (fullDetails in shortcuts) {
-                if (!fullDetails.isPinned) {
+                if (!fullDetails.isPinned && !Flags.restoreArchivedShortcuts()) {
                     continue
                 }
                 val shortcutId = fullDetails.id
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index de1df2e..3919eb7 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -194,27 +194,36 @@
         if (intent.`package` == null) {
             intent.`package` = targetPkg
         }
+        val isPreArchived = appInfoWrapper.isArchived() && c.restoreFlag != 0
+
         // else if cn == null => can't infer much, leave it
         // else if !validPkg => could be restored icon or missing sd-card
         when {
-            !TextUtils.isEmpty(targetPkg) && !validTarget -> {
+            !TextUtils.isEmpty(targetPkg) && (!validTarget || isPreArchived) -> {
                 // Points to a valid app (superset of cn != null) but the apk
                 // is not available.
                 when {
-                    c.restoreFlag != 0 -> {
+                    c.restoreFlag != 0 || isPreArchived -> {
                         // Package is not yet available but might be
                         // installed later.
-                        FileLog.d(TAG, "package not yet restored: $targetPkg")
+                        FileLog.d(
+                            TAG,
+                            "package not yet restored: $targetPkg, itemType=${c.itemType}" +
+                                "isPreArchived=$isPreArchived, restoreFlag=${c.restoreFlag}",
+                        )
                         tempPackageKey.update(targetPkg, c.user)
                         when {
                             c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
                                 // Restore has started once.
                             }
-                            installingPkgs.containsKey(tempPackageKey) -> {
+                            installingPkgs.containsKey(tempPackageKey) || isPreArchived -> {
                                 // App restore has started. Update the flag
                                 c.restoreFlag =
                                     c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
-                                FileLog.d(TAG, "restore started for installing app: $targetPkg")
+                                FileLog.d(
+                                    TAG,
+                                    "restore started for installing app: $targetPkg, itemType=${c.itemType}",
+                                )
                                 c.updater().put(Favorites.RESTORED, c.restoreFlag).commit()
                             }
                             else -> {
@@ -253,9 +262,18 @@
             }
         }
         if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) {
+            FileLog.d(
+                TAG,
+                "restore flag set AND WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0, setting valid target to false: $targetPkg, itemType=${c.itemType}, restoreFlag=${c.restoreFlag}",
+            )
             validTarget = false
         }
-        if (validTarget) {
+        if (validTarget && !isPreArchived) {
+            FileLog.d(
+                TAG,
+                "valid target true, marking restored: $targetPkg," +
+                    " itemType=${c.itemType}, restoreFlag=${c.restoreFlag}",
+            )
             // The shortcut points to a valid target (either no target
             // or something which is ready to be used)
             c.markRestored()
@@ -265,7 +283,7 @@
         when {
             c.restoreFlag != 0 -> {
                 // Already verified above that user is same as default user
-                info = c.getRestoredItemInfo(intent)
+                info = c.getRestoredItemInfo(intent, isPreArchived)
             }
             c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
                 info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
@@ -286,7 +304,7 @@
                     // If the pinned deep shortcut is no longer published,
                     // use the last saved icon instead of the default.
                     val csi = CacheableShortcutInfo(pinnedShortcut, appInfoWrapper)
-                    iconCache.getShortcutIcon(info, csi, c::loadIcon)
+                    iconCache.getShortcutIcon(info, csi, c::loadIconFromDb)
                     if (appInfoWrapper.isSuspended()) {
                         info.runtimeStatusFlags =
                             info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
diff --git a/src/com/android/launcher3/model/data/IconRequestInfo.java b/src/com/android/launcher3/model/data/IconRequestInfo.java
index e77e527..42af018 100644
--- a/src/com/android/launcher3/model/data/IconRequestInfo.java
+++ b/src/com/android/launcher3/model/data/IconRequestInfo.java
@@ -64,23 +64,25 @@
     }
 
     /**
-     * Loads this request's item info's title. This method should only be used on IconRequestInfos
-     * for WorkspaceItemInfos.
+     * Loads this request's item info's title and icon from given iconBlob from Launcher.db.
+     * This method should only be used on {@link IconRequestInfo} for {@link WorkspaceItemInfo}
+     *  or {@link AppInfo}.
      */
-    public boolean loadWorkspaceIcon(Context context) {
-        if (!(itemInfo instanceof WorkspaceItemInfo)) {
+    public boolean loadIconFromDbBlob(Context context) {
+        if (!(itemInfo instanceof WorkspaceItemInfo) && !(itemInfo instanceof AppInfo)) {
             throw new IllegalStateException(
-                    "loadWorkspaceIcon should only be use for a WorkspaceItemInfos: " + itemInfo);
+                    "loadIconFromDb should only be used for either WorkspaceItemInfo or AppInfo: "
+                            + itemInfo);
         }
 
         try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            WorkspaceItemInfo info = (WorkspaceItemInfo) itemInfo;
-            // Failed to load from resource, try loading from DB.
+            ItemInfoWithIcon info = itemInfo;
             if (iconBlob == null) {
+                Log.d(TAG, "loadIconFromDb: icon blob null, returning. Component="
+                        + info.getTargetComponent());
                 return false;
             }
-            info.bitmap = li.createIconBitmap(decodeByteArray(
-                    iconBlob, 0, iconBlob.length));
+            info.bitmap = li.createIconBitmap(decodeByteArray(iconBlob, 0, iconBlob.length));
             return true;
         } catch (Exception e) {
             Log.e(TAG, "Failed to decode byte array for info " + itemInfo, e);
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 1c9db17..e52ca6d 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -40,7 +40,6 @@
 import androidx.annotation.LayoutRes;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
@@ -579,7 +578,7 @@
     /**
      * Dismisses the popup if it is no longer valid
      */
-    public static void dismissInvalidPopup(BaseDraggingActivity activity) {
+    public static <T extends Context & ActivityContext> void dismissInvalidPopup(T activity) {
         PopupContainerWithArrow popup = getOpen(activity);
         if (popup != null && (!popup.mOriginalIcon.isAttachedToWindow()
                 || !ShortcutUtil.supportsShortcuts((ItemInfo) popup.mOriginalIcon.getTag()))) {
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 9b3292d..df27b54 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.secondarydisplay;
 
+import static com.android.launcher3.util.WallpaperThemeManager.setWallpaperDependentTheme;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.content.Intent;
@@ -29,7 +31,7 @@
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
@@ -59,7 +61,6 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -67,7 +68,7 @@
 /**
  * Launcher activity for secondary displays
  */
-public class SecondaryDisplayLauncher extends BaseDraggingActivity
+public class SecondaryDisplayLauncher extends BaseActivity
         implements BgDataModel.Callbacks, DragController.DragListener {
 
     private LauncherModel mModel;
@@ -77,7 +78,6 @@
     private View mAppsButton;
 
     private PopupDataProvider mPopupDataProvider;
-    private WidgetPickerDataProvider mWidgetPickerDataProvider;
 
     private boolean mAppDrawerShown = false;
 
@@ -90,6 +90,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        setWallpaperDependentTheme(this);
         mModel = LauncherAppState.getInstance(this).getModel();
         mDragController = new SecondaryDragController(this);
         mSecondaryDisplayPredictions = SecondaryDisplayPredictions.newInstance(this);
@@ -202,14 +203,6 @@
     }
 
     @Override
-    public View getRootView() {
-        return mDragLayer;
-    }
-
-    @Override
-    protected void reapplyUi() { }
-
-    @Override
     public BaseDragLayer getDragLayer() {
         return mDragLayer;
     }
@@ -317,11 +310,6 @@
     }
 
     @Override
-    public WidgetPickerDataProvider getWidgetPickerDataProvider() {
-        return mWidgetPickerDataProvider;
-    }
-
-    @Override
     public OnClickListener getItemOnClickListener() {
         return this::onIconClicked;
     }
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index f6b610c..b7dd2bf 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -15,15 +15,13 @@
  */
 package com.android.launcher3.statemanager;
 
-import android.content.Context;
-
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.views.ActivityContext;
 
 /**
  * Interface representing a state of a StatefulContainer
  */
-public interface BaseState<T extends BaseState> {
+public interface BaseState<T> {
 
     // Flag to indicate that Launcher is non-interactive in this state
     int FLAG_NON_INTERACTIVE = 1 << 0;
@@ -37,8 +35,7 @@
     /**
      * @return How long the animation to this state should take (or from this state to NORMAL).
      */
-    <DEVICE_PROFILE_CONTEXT extends Context & ActivityContext>
-    int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState);
+    int getTransitionDuration(ActivityContext context, boolean isToState);
 
     /**
      * Returns the state to go back to from this state
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 763f3ba..a125331 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -27,7 +27,6 @@
 import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
@@ -50,50 +49,49 @@
 /**
  * Class to manage transitions between different states for a StatefulActivity based on different
  * states
- * @param STATE_TYPE Basestate used by the state manager
- * @param STATEFUL_CONTAINER container object used to manage state
+ * @param <S> Basestate used by the state manager
+ * @param <T> container object used to manage state
  */
-public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>,
-        STATEFUL_CONTAINER extends Context & StatefulContainer<STATE_TYPE>> {
+public class StateManager<S extends BaseState<S>, T extends StatefulContainer<S>> {
 
     public static final String TAG = "StateManager";
     // b/279059025, b/325463989
     private static final boolean DEBUG = true;
 
-    private final AnimationState mConfig = new AnimationState();
+    private final AnimationState<S> mConfig = new AnimationState<>();
     private final Handler mUiHandler;
-    private final STATEFUL_CONTAINER mStatefulContainer;
-    private final ArrayList<StateListener<STATE_TYPE>> mListeners = new ArrayList<>();
-    private final STATE_TYPE mBaseState;
+    private final T mContainer;
+    private final ArrayList<StateListener<S>> mListeners = new ArrayList<>();
+    private final S mBaseState;
 
     // Animators which are run on properties also controlled by state animations.
-    private final AtomicAnimationFactory mAtomicAnimationFactory;
+    private final AtomicAnimationFactory<S> mAtomicAnimationFactory;
 
-    private StateHandler<STATE_TYPE>[] mStateHandlers;
-    private STATE_TYPE mState;
+    private StateHandler<S>[] mStateHandlers;
+    private S mState;
 
-    private STATE_TYPE mLastStableState;
-    private STATE_TYPE mCurrentStableState;
+    private S mLastStableState;
+    private S mCurrentStableState;
 
-    private STATE_TYPE mRestState;
+    private S mRestState;
 
-    public StateManager(STATEFUL_CONTAINER container, STATE_TYPE baseState) {
+    public StateManager(T container, S baseState) {
         mUiHandler = new Handler(Looper.getMainLooper());
-        mStatefulContainer = container;
+        mContainer = container;
         mBaseState = baseState;
         mState = mLastStableState = mCurrentStableState = baseState;
         mAtomicAnimationFactory = container.createAtomicAnimationFactory();
     }
 
-    public STATE_TYPE getState() {
+    public S getState() {
         return mState;
     }
 
-    public STATE_TYPE getTargetState() {
-        return (STATE_TYPE) mConfig.targetState;
+    public S getTargetState() {
+        return mConfig.targetState;
     }
 
-    public STATE_TYPE getCurrentStableState() {
+    public S getCurrentStableState() {
         return mCurrentStableState;
     }
 
@@ -115,20 +113,20 @@
         writer.println(prefix + "\tisInTransition:" + isInTransition());
     }
 
-    public StateHandler<STATE_TYPE>[] getStateHandlers() {
+    public StateHandler<S>[] getStateHandlers() {
         if (mStateHandlers == null) {
-            ArrayList<StateHandler<STATE_TYPE>> handlers = new ArrayList<>();
-            mStatefulContainer.collectStateHandlers(handlers);
+            ArrayList<StateHandler<S>> handlers = new ArrayList<>();
+            mContainer.collectStateHandlers(handlers);
             mStateHandlers = handlers.toArray(new StateHandler[handlers.size()]);
         }
         return mStateHandlers;
     }
 
-    public void addStateListener(StateListener listener) {
+    public void addStateListener(StateListener<S> listener) {
         mListeners.add(listener);
     }
 
-    public void removeStateListener(StateListener listener) {
+    public void removeStateListener(StateListener<S> listener) {
         mListeners.remove(listener);
     }
 
@@ -136,14 +134,14 @@
      * Returns true if the state changes should be animated.
      */
     public boolean shouldAnimateStateChange() {
-        return mStatefulContainer.shouldAnimateStateChange();
+        return mContainer.shouldAnimateStateChange();
     }
 
     /**
      * @return {@code true} if the state matches the current state and there is no active
      *         transition to different state.
      */
-    public boolean isInStableState(STATE_TYPE state) {
+    public boolean isInStableState(S state) {
         return mState == state && mCurrentStableState == state
                 && (mConfig.targetState == null || mConfig.targetState == state);
     }
@@ -156,23 +154,23 @@
     }
 
     /**
-     * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
+     * @see #goToState(S, boolean, AnimatorListener)
      */
-    public void goToState(STATE_TYPE state) {
+    public void goToState(S state) {
         goToState(state, shouldAnimateStateChange());
     }
 
     /**
-     * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
+     * @see #goToState(S, boolean, AnimatorListener)
      */
-    public void goToState(STATE_TYPE state, AnimatorListener listener) {
+    public void goToState(S state, AnimatorListener listener) {
         goToState(state, shouldAnimateStateChange(), listener);
     }
 
     /**
-     * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
+     * @see #goToState(S, boolean, AnimatorListener)
      */
-    public void goToState(STATE_TYPE state, boolean animated) {
+    public void goToState(S state, boolean animated) {
         goToState(state, animated, 0, null);
     }
 
@@ -183,21 +181,21 @@
      *                true otherwise
      * @param listener any action to perform at the end of the transition, or null.
      */
-    public void goToState(STATE_TYPE state, boolean animated, AnimatorListener listener) {
+    public void goToState(S state, boolean animated, AnimatorListener listener) {
         goToState(state, animated, 0, listener);
     }
 
     /**
      * Changes the Launcher state to the provided state after the given delay.
      */
-    public void goToState(STATE_TYPE state, long delay, AnimatorListener listener) {
+    public void goToState(S state, long delay, AnimatorListener listener) {
         goToState(state, true, delay, listener);
     }
 
     /**
      * Changes the Launcher state to the provided state after the given delay.
      */
-    public void goToState(STATE_TYPE state, long delay) {
+    public void goToState(S state, long delay) {
         goToState(state, true, delay, null);
     }
 
@@ -219,7 +217,7 @@
             cancelAnimation();
         }
         if (mConfig.currentAnimation == null) {
-            for (StateHandler handler : getStateHandlers()) {
+            for (StateHandler<S> handler : getStateHandlers()) {
                 handler.setState(mState);
             }
             if (wasInAnimation) {
@@ -230,21 +228,21 @@
 
     /** Handles backProgress in predictive back gesture by passing it to state handlers. */
     public void onBackProgressed(
-            STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {
-        for (StateHandler handler : getStateHandlers()) {
+            S toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {
+        for (StateHandler<S> handler : getStateHandlers()) {
             handler.onBackProgressed(toState, backProgress);
         }
     }
 
     /** Handles back cancelled event in predictive back gesture by passing it to state handlers. */
-    public void onBackCancelled(STATE_TYPE toState) {
-        for (StateHandler handler : getStateHandlers()) {
+    public void onBackCancelled(S toState) {
+        for (StateHandler<S> handler : getStateHandlers()) {
             handler.onBackCancelled(toState);
         }
     }
 
     private void goToState(
-            STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
+            S state, boolean animated, long delay, AnimatorListener listener) {
         if (enableStateManagerProtoLog()) {
             StateManagerProtoLogProxy.logGoToState(
                     mState, state, getTrimmedStackTrace("StateManager.goToState"));
@@ -254,7 +252,7 @@
         }
 
         animated &= areAnimatorsEnabled();
-        if (mStatefulContainer.isInState(state)) {
+        if (getState() == state) {
             if (mConfig.currentAnimation == null) {
                 // Run any queued runnable
                 if (listener != null) {
@@ -273,13 +271,13 @@
         }
 
         // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
-        STATE_TYPE fromState = mState;
+        S fromState = mState;
         cancelAnimation();
 
         if (!animated) {
             mAtomicAnimationFactory.cancelAllStateElementAnimation();
             onStateTransitionStart(state);
-            for (StateHandler handler : getStateHandlers()) {
+            for (StateHandler<S> handler : getStateHandlers()) {
                 handler.setState(state);
             }
 
@@ -306,13 +304,13 @@
         }
     }
 
-    private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
+    private void goToStateAnimated(S state, S fromState,
             AnimatorListener listener) {
         // Since state mBaseState can be reached from multiple states, just assume that the
         // transition plays in reverse and use the same duration as previous state.
         mConfig.duration = state == mBaseState
-                ? fromState.getTransitionDuration(mStatefulContainer, false /* isToState */)
-                : state.getTransitionDuration(mStatefulContainer, true /* isToState */);
+                ? fromState.getTransitionDuration(mContainer, false /* isToState */)
+                : state.getTransitionDuration(mContainer, true /* isToState */);
         prepareForAtomicAnimation(fromState, state, mConfig);
         AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
         if (listener != null) {
@@ -326,7 +324,7 @@
      * - Setting interpolators for various animations included in the state transition.
      * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
      */
-    public void prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState,
+    public void prepareForAtomicAnimation(S fromState, S toState,
             StateAnimationConfig config) {
         mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config);
     }
@@ -335,7 +333,7 @@
      * Creates an animation representing atomic transitions between the provided states
      */
     public AnimatorSet createAtomicAnimation(
-            STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
+            S fromState, S toState, StateAnimationConfig config) {
         if (enableStateManagerProtoLog()) {
             StateManagerProtoLogProxy.logCreateAtomicAnimation(
                     mState, toState, getTrimmedStackTrace("StateManager.createAtomicAnimation"));
@@ -348,7 +346,7 @@
         PendingAnimation builder = new PendingAnimation(config.duration);
         prepareForAtomicAnimation(fromState, toState, config);
 
-        for (StateHandler handler : mStatefulContainer.getStateManager().getStateHandlers()) {
+        for (StateHandler<S> handler : getStateHandlers()) {
             handler.setStateWithAnimation(toState, config, builder);
         }
         return builder.buildAnim();
@@ -362,19 +360,19 @@
      *                accuracy.
      */
     public AnimatorPlaybackController createAnimationToNewWorkspace(
-            STATE_TYPE state, long duration) {
+            S state, long duration) {
         return createAnimationToNewWorkspace(state, duration, 0 /* animFlags */);
     }
 
     public AnimatorPlaybackController createAnimationToNewWorkspace(
-            STATE_TYPE state, long duration, @AnimationFlags int animFlags) {
+            S state, long duration, @AnimationFlags int animFlags) {
         StateAnimationConfig config = new StateAnimationConfig();
         config.duration = duration;
         config.animFlags = animFlags;
         return createAnimationToNewWorkspace(state, config);
     }
 
-    public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
+    public AnimatorPlaybackController createAnimationToNewWorkspace(S state,
             StateAnimationConfig config) {
         config.animProps |= StateAnimationConfig.USER_CONTROLLED;
         cancelAnimation();
@@ -384,10 +382,10 @@
         return mConfig.playbackController;
     }
 
-    private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
+    private PendingAnimation createAnimationToNewWorkspaceInternal(final S state) {
         PendingAnimation builder = new PendingAnimation(mConfig.duration);
         if (!mConfig.hasAnimationFlag(SKIP_ALL_ANIMATIONS)) {
-            for (StateHandler handler : getStateHandlers()) {
+            for (StateHandler<S> handler : getStateHandlers()) {
                 handler.setStateWithAnimation(state, mConfig, builder);
             }
         }
@@ -396,7 +394,7 @@
         return builder;
     }
 
-    private AnimatorListener createStateAnimationListener(STATE_TYPE state) {
+    private AnimatorListener createStateAnimationListener(S state) {
         return new AnimationSuccessListener() {
 
             @Override
@@ -412,9 +410,9 @@
         };
     }
 
-    private void onStateTransitionStart(STATE_TYPE state) {
+    private void onStateTransitionStart(S state) {
         mState = state;
-        mStatefulContainer.onStateSetStart(mState);
+        mContainer.onStateSetStart(mState);
 
         if (enableStateManagerProtoLog()) {
             StateManagerProtoLogProxy.logOnStateTransitionStart(state);
@@ -426,14 +424,14 @@
         }
     }
 
-    private void onStateTransitionEnd(STATE_TYPE state) {
+    private void onStateTransitionEnd(S state) {
         // Only change the stable states after the transitions have finished
         if (state != mCurrentStableState) {
             mLastStableState = state.getHistoryForState(mCurrentStableState);
             mCurrentStableState = state;
         }
 
-        mStatefulContainer.onStateSetEnd(state);
+        mContainer.onStateSetEnd(state);
         if (state == mBaseState) {
             setRestState(null);
         }
@@ -448,7 +446,7 @@
         }
     }
 
-    public STATE_TYPE getLastState() {
+    public S getLastState() {
         return mLastStableState;
     }
 
@@ -468,11 +466,11 @@
         }
     }
 
-    public STATE_TYPE getRestState() {
+    public S getRestState() {
         return mRestState == null ? mBaseState : mRestState;
     }
 
-    public void setRestState(STATE_TYPE restState) {
+    public void setRestState(S restState) {
         mRestState = restState;
     }
 
@@ -518,7 +516,7 @@
      * @param anim The custom animation to the given state.
      * @param toState The state we are animating towards.
      */
-    public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
+    public void setCurrentAnimation(AnimatorSet anim, S toState) {
         cancelAnimation();
         setCurrentAnimation(anim);
         anim.addListener(createStateAnimationListener(toState));
@@ -691,10 +689,10 @@
 
         /** Handles backProgress in predictive back gesture for target state. */
         default void onBackProgressed(
-                STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {};
+                STATE_TYPE toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {}
 
         /** Handles back cancelled event in predictive back gesture for target state.  */
-        default void onBackCancelled(STATE_TYPE toState) {};
+        default void onBackCancelled(STATE_TYPE toState) {}
     }
 
     public interface StateListener<STATE_TYPE> {
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index f21e5da..445701d 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -18,10 +18,8 @@
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 
-import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_RECREATE_TO_UPDATE_THEME;
 import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
 
-import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Handler;
@@ -30,9 +28,8 @@
 import android.view.View;
 
 import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
 
-import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
@@ -46,7 +43,7 @@
  * @param <STATE_TYPE> Type of state object
  */
 public abstract class StatefulActivity<STATE_TYPE extends BaseState<STATE_TYPE>>
-        extends BaseDraggingActivity implements StatefulContainer<STATE_TYPE> {
+        extends BaseActivity implements StatefulContainer<STATE_TYPE> {
 
     public final Handler mHandler = new Handler();
     private final Runnable mHandleDeferredResume = this::handleDeferredResume;
@@ -56,7 +53,6 @@
 
     protected Configuration mOldConfig;
     private int mOldRotation;
-    private boolean mRecreateToUpdateTheme = false;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -66,18 +62,6 @@
         mOldRotation = WindowManagerProxy.INSTANCE.get(this).getRotation(this);
     }
 
-    @Override
-    protected void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME, mRecreateToUpdateTheme);
-        super.onSaveInstanceState(outState);
-    }
-
-    @Override
-    protected void recreateToUpdateTheme() {
-        mRecreateToUpdateTheme = true;
-        super.recreateToUpdateTheme();
-    }
-
     /**
      * Create handlers to control the property changes for this activity
      */
@@ -214,11 +198,6 @@
         mOldRotation = rotation;
     }
 
-    @Override
-    public Context getContext() {
-        return this;
-    }
-
     /**
      * Logic for when device configuration changes (rotation, screen size change, multi-window,
      * etc.)
diff --git a/src/com/android/launcher3/statemanager/StatefulContainer.java b/src/com/android/launcher3/statemanager/StatefulContainer.java
index b10af0a..83a2fdc 100644
--- a/src/com/android/launcher3/statemanager/StatefulContainer.java
+++ b/src/com/android/launcher3/statemanager/StatefulContainer.java
@@ -20,8 +20,6 @@
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 
-import android.content.Context;
-import android.content.ContextWrapper;
 import android.content.res.Configuration;
 
 import androidx.annotation.CallSuper;
@@ -40,23 +38,6 @@
         ActivityContext {
 
     /**
-     * Returns an instance of an implementation of StatefulContainer
-     *
-     * @param context will find instance of StatefulContainer from given context.
-     */
-    static <T extends StatefulContainer> T fromContext(Context context) {
-        if (context instanceof StatefulContainer) {
-            return (T) context;
-        } else if (context instanceof ContextWrapper) {
-            return fromContext(((ContextWrapper) context).getBaseContext());
-        } else {
-            throw new IllegalArgumentException("Cannot find StatefulContainer in parent tree");
-        }
-    }
-
-    Context getContext();
-
-    /**
      * Creates a factory for atomic state animations
      */
     default StateManager.AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
diff --git a/src/com/android/launcher3/states/EditModeState.kt b/src/com/android/launcher3/states/EditModeState.kt
index 6ff47ae..268a373 100644
--- a/src/com/android/launcher3/states/EditModeState.kt
+++ b/src/com/android/launcher3/states/EditModeState.kt
@@ -36,11 +36,7 @@
                 FLAG_WORKSPACE_HAS_BACKGROUNDS)
     }
 
-    override fun <T> getTransitionDuration(context: T, isToState: Boolean): Int where
-    T : Context?,
-    T : ActivityContext? {
-        return 150
-    }
+    override fun getTransitionDuration(context: ActivityContext, isToState: Boolean) = 150
 
     override fun <T> getDepthUnchecked(context: T): Float where T : Context?, T : ActivityContext? {
         if (enableScalingRevealHomeAnimation()) {
diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java
index bf2fb30..ed22d39 100644
--- a/src/com/android/launcher3/states/HintState.java
+++ b/src/com/android/launcher3/states/HintState.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * Scale down workspace/hotseat to hint at going to either overview (on pause) or first home screen.
@@ -46,7 +47,7 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         return 80;
     }
 
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 2e57ed8..15e6c61 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -24,6 +24,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * Definition for spring loaded state used during drag and drop.
@@ -41,7 +42,7 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         return 150;
     }
 
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 3817563..4509bae 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.touch;
 
 import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.Flags.enableMouseInteractionChanges;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
@@ -33,6 +34,7 @@
 
 import android.animation.Animator.AnimatorListener;
 import android.animation.ValueAnimator;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 
 import com.android.launcher3.Launcher;
@@ -107,7 +109,9 @@
                 ignoreSlopWhenSettling = true;
             } else {
                 directionsToDetectScroll = getSwipeDirection();
-                if (directionsToDetectScroll == 0) {
+                boolean ignoreMouseScroll = ev.getSource() == InputDevice.SOURCE_MOUSE
+                        && enableMouseInteractionChanges();
+                if (directionsToDetectScroll == 0 || ignoreMouseScroll) {
                     mNoIntercept = true;
                     return false;
                 }
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index b69bc17..d72e6f9 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -21,6 +21,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 
+import static com.android.launcher3.Flags.enableMouseInteractionChanges;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE;
@@ -31,6 +32,7 @@
 import android.graphics.Rect;
 import android.view.GestureDetector;
 import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
@@ -41,7 +43,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.testing.TestLogging;
@@ -193,6 +194,10 @@
 
     @Override
     public void onLongPress(MotionEvent event) {
+        if (event.getSource() == InputDevice.SOURCE_MOUSE && enableMouseInteractionChanges()) {
+            // Stop mouse long press events from showing the menu.
+            return;
+        }
         maybeShowMenu();
     }
 
diff --git a/src/com/android/launcher3/util/WallpaperThemeManager.kt b/src/com/android/launcher3/util/WallpaperThemeManager.kt
new file mode 100644
index 0000000..c48ef07
--- /dev/null
+++ b/src/com/android/launcher3/util/WallpaperThemeManager.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.app.Activity
+import android.content.ComponentCallbacks
+import android.content.res.Configuration
+import android.os.Bundle
+import com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE_RECREATE_TO_UPDATE_THEME
+import com.android.launcher3.R
+
+/** Utility class to manage activity's theme in case it is wallpaper dependent */
+class WallpaperThemeManager private constructor(private val activity: Activity) :
+    OnColorHintListener, ActivityLifecycleCallbacksAdapter, ComponentCallbacks {
+
+    private var themeRes: Int = R.style.AppTheme
+
+    private var recreateToUpdateTheme = false
+
+    init {
+        // Update theme
+        WallpaperColorHints.get(activity).registerOnColorHintsChangedListener(this)
+        val expectedTheme = Themes.getActivityThemeRes(activity)
+        if (expectedTheme != themeRes) {
+            themeRes = expectedTheme
+            activity.setTheme(expectedTheme)
+        }
+
+        activity.registerActivityLifecycleCallbacks(this)
+        activity.registerComponentCallbacks(this)
+    }
+
+    override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) =
+        bundle.putBoolean(RUNTIME_STATE_RECREATE_TO_UPDATE_THEME, recreateToUpdateTheme)
+
+    override fun onActivityDestroyed(unused: Activity) =
+        WallpaperColorHints.get(activity).unregisterOnColorsChangedListener(this)
+
+    override fun onConfigurationChanged(config: Configuration) = updateTheme()
+
+    override fun onLowMemory() {}
+
+    override fun onColorHintsChanged(colorHints: Int) = updateTheme()
+
+    private fun updateTheme() {
+        if (themeRes != Themes.getActivityThemeRes(activity)) {
+            recreateToUpdateTheme = true
+            activity.recreate()
+        }
+    }
+
+    companion object {
+
+        /**
+         * Sets a wallpaper dependent theme on this activity. The activity is automatically
+         * recreated when a wallpaper change can potentially change the theme.
+         */
+        @JvmStatic
+        fun Activity.setWallpaperDependentTheme() {
+            if (!isDestroyed) {
+                WallpaperThemeManager(this)
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index b164b7f..81968fc 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.DropTargetHandler;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -166,6 +167,11 @@
         return false;
     }
 
+    /** Returns the RootView */
+    default View getRootView() {
+        return getDragLayer();
+    }
+
     /**
      * The root view to support drag-and-drop and popup support.
      */
@@ -410,7 +416,8 @@
             View v, Intent intent, @Nullable ItemInfo item) {
         Preconditions.assertUIThread();
         Context context = (Context) this;
-        if (isAppBlockedForSafeMode() && !new ApplicationInfoWrapper(context, intent).isSystem()) {
+        if (LauncherAppState.getInstance(context).isSafeModeEnabled()
+                && !new ApplicationInfoWrapper(context, intent).isSystem()) {
             Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return null;
         }
@@ -456,11 +463,6 @@
         return null;
     }
 
-    /** Returns {@code true} if an app launch is blocked due to safe mode. */
-    default boolean isAppBlockedForSafeMode() {
-        return false;
-    }
-
     /**
      * Creates and logs a new app launch event.
      */
@@ -476,6 +478,7 @@
      * @param v View initiating a launch.
      * @param item Item associated with the view.
      */
+    @NonNull
     default ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
         int left = 0, top = 0;
         int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
@@ -525,6 +528,11 @@
         return false;
     }
 
+    /** Returns the current ActivityContext as context */
+    default Context asContext() {
+        return (Context) this;
+    }
+
     /**
      * Returns the ActivityContext associated with the given Context, or throws an exception if
      * the Context is not associated with any ActivityContext.
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index f499fca..78197e2 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -41,7 +41,6 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.model.data.ItemInfo;
@@ -50,6 +49,7 @@
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
@@ -218,8 +218,7 @@
      * @param widgetId The ID of the widget
      * @param requestCode The request code
      */
-    public void startConfigActivity(@NonNull BaseDraggingActivity activity, int widgetId,
-            int requestCode) {
+    public void startConfigActivity(@NonNull BaseActivity activity, int widgetId, int requestCode) {
         if (!WIDGETS_ENABLED) {
             sendActionCancelled(activity, requestCode);
             return;
@@ -245,7 +244,7 @@
      * the configuration of the {@code widgetId} app widget, or null of options cannot be produced.
      */
     @Nullable
-    protected Bundle getConfigurationActivityOptions(@NonNull BaseDraggingActivity activity,
+    protected Bundle getConfigurationActivityOptions(@NonNull ActivityContext activity,
             int widgetId) {
         LauncherAppWidgetHostView view = mViews.get(widgetId);
         if (view == null) {
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 4811a17..7a27bf4 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -153,17 +153,19 @@
         mWidgetAddButton = findViewById(R.id.widget_add_button);
 
         if (enableWidgetTapToAdd()) {
-
             setAccessibilityDelegate(new AccessibilityDelegate() {
                 @Override
                 public void onInitializeAccessibilityNodeInfo(View host,
                         AccessibilityNodeInfo info) {
                     super.onInitializeAccessibilityNodeInfo(host, info);
-                    String accessibilityLabel = getResources().getString(mWidgetAddButton.isShown()
-                            ? R.string.widget_cell_tap_to_hide_add_button_label
-                            : R.string.widget_cell_tap_to_show_add_button_label);
-                    info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
-                            accessibilityLabel));
+                    if (hasOnClickListeners()) {
+                        String accessibilityLabel = getResources().getString(
+                                mWidgetAddButton.isShown()
+                                        ? R.string.widget_cell_tap_to_hide_add_button_label
+                                        : R.string.widget_cell_tap_to_show_add_button_label);
+                        info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
+                                accessibilityLabel));
+                    }
                 }
             });
             mWidgetAddButton.setVisibility(INVISIBLE);
diff --git a/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java
index 9865516..03a5535 100644
--- a/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -18,8 +18,6 @@
 import static com.android.app.animation.Interpolators.DECELERATE;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 
-import android.content.Context;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -31,8 +29,6 @@
  */
 public class AllAppsState extends LauncherState {
 
-    private static final float PARALLAX_COEFFICIENT = .125f;
-
     private static final int STATE_FLAGS = FLAG_WORKSPACE_INACCESSIBLE;
 
     public AllAppsState(int id) {
@@ -40,8 +36,7 @@
     }
 
     @Override
-    public <DEVICE_PROFILE_CONTEXT extends Context & ActivityContext>
-    int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         return isToState
                 ? context.getDeviceProfile().allAppsOpenDuration
                 : context.getDeviceProfile().allAppsCloseDuration;
diff --git a/src_no_quickstep/com/android/launcher3/uioverrides/states/OverviewState.java b/src_no_quickstep/com/android/launcher3/uioverrides/states/OverviewState.java
index 7a228c4..532a338 100644
--- a/src_no_quickstep/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/src_no_quickstep/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -17,12 +17,11 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 
-import android.content.Context;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * Definition for overview state
@@ -34,7 +33,7 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
+    public int getTransitionDuration(ActivityContext context, boolean isToState) {
         return 250;
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
index 63359ec..11047fb 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.model;
 
+import static android.graphics.BitmapFactory.decodeByteArray;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
 import static androidx.test.InstrumentationRegistry.getContext;
 
 import static com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID;
@@ -40,8 +43,11 @@
 import static com.android.launcher3.LauncherSettings.Favorites.SPANY;
 import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
 import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -52,13 +58,19 @@
 import android.content.Context;
 import android.content.Intent;
 import android.database.MatrixCursor;
+import android.graphics.Bitmap;
 import android.os.Process;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Executors;
@@ -67,6 +79,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -77,6 +90,9 @@
 @RunWith(AndroidJUnit4.class)
 public class LoaderCursorTest {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
     private LauncherModelHelper mModelHelper;
     private LauncherAppState mApp;
     private PackageManagerHelper mPmHelper;
@@ -87,6 +103,12 @@
 
     private LoaderCursor mLoaderCursor;
 
+    private static byte[] sTestBlob = new byte[] {
+            -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1,
+            8, 4, 0, 0, 0, -75, 28, 12, 2, 0, 0, 0, 11, 73, 68, 65, 84, 120, -38, 99, 100, 96, 0,
+            0, 0, 6, 0, 2, 48, -127, -48, 47, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126
+    };
+
     @Before
     public void setup() {
         mModelHelper = new LauncherModelHelper();
@@ -119,7 +141,8 @@
                 .add(PROFILE_ID, 0)
                 .add(ITEM_TYPE, itemType)
                 .add(TITLE, title)
-                .add(CONTAINER, CONTAINER_DESKTOP);
+                .add(CONTAINER, CONTAINER_DESKTOP)
+                .add(ICON, sTestBlob);
     }
 
     @Test
@@ -161,7 +184,12 @@
 
     @Test
     public void loadSimpleShortcut() {
-        initCursor(ITEM_TYPE_DEEP_SHORTCUT, "my-shortcut");
+        mCursor.newRow()
+                .add(_ID, 1)
+                .add(PROFILE_ID, 0)
+                .add(ITEM_TYPE, ITEM_TYPE_DEEP_SHORTCUT)
+                .add(TITLE, "my-shortcut")
+                .add(CONTAINER, CONTAINER_DESKTOP);
         assertTrue(mLoaderCursor.moveToNext());
 
         WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
@@ -223,6 +251,68 @@
                 newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3), true));
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    public void ifArchivedWithFlag_whenloadWorkspaceTitleAndIcon_thenLoadIconFromDb() {
+        // Given
+        initCursor(ITEM_TYPE_APPLICATION, "title");
+        assertTrue(mLoaderCursor.moveToNext());
+        WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+        itemInfo.bitmap = null;
+        itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+        Bitmap expectedBitmap = LauncherIcons.obtain(mContext)
+                .createIconBitmap(decodeByteArray(sTestBlob, 0, sTestBlob.length))
+                .icon;
+        // When
+        mLoaderCursor.loadWorkspaceTitleAndIcon(false, true, itemInfo);
+        // Then
+        assertThat(itemInfo.bitmap.icon).isNotNull();
+        assertThat(itemInfo.bitmap.icon.sameAs(expectedBitmap)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    public void ifArchivedWithFlag_whenLoadIconFromDb_thenLoadIconFromBlob() {
+        // Given
+        initCursor(ITEM_TYPE_APPLICATION, "title");
+        assertTrue(mLoaderCursor.moveToNext());
+        WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+        itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+        // Then
+        assertTrue(mLoaderCursor.loadIconFromDb(itemInfo));
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    public void ifArchivedWithoutFlag_whenLoadWorkspaceTitleAndIcon_thenDoNotLoadFromDb() {
+        // Given
+        initCursor(ITEM_TYPE_APPLICATION, "title");
+        assertTrue(mLoaderCursor.moveToNext());
+        WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+        itemInfo.bitmap = null;
+        itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName("package", "class"));
+        itemInfo.intent = intent;
+        // When
+        mLoaderCursor.loadWorkspaceTitleAndIcon(false, false, itemInfo);
+        // Then
+        assertThat(itemInfo.bitmap).isNull();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    public void ifArchivedWithoutFlag_whenLoadIconFromDb_thenDoNotLoadFromBlob() {
+        // Given
+        initCursor(ITEM_TYPE_APPLICATION, "title");
+        assertTrue(mLoaderCursor.moveToNext());
+        WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+        itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+        // Then
+        assertFalse(mLoaderCursor.loadIconFromDb(itemInfo));
+    }
+
+
     private ItemInfo newItemInfo(int cellX, int cellY, int spanX, int spanY,
             int container, int screenId) {
         ItemInfo info = new ItemInfo();
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
index fb6d038..010159c 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
@@ -24,8 +24,12 @@
 import android.content.pm.ShortcutInfo
 import android.os.Process.myUserHandle
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.icons.BitmapInfo
@@ -42,6 +46,7 @@
 import java.util.function.Predicate
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -55,6 +60,8 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ShortcutsChangedTaskTest {
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
     private lateinit var shortcutsChangedTask: ShortcutsChangedTask
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var context: SandboxModelContext
@@ -192,7 +199,8 @@
     }
 
     @Test
-    fun `When archived pinned shortcut is found then keep in workspace`() {
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When archived pinned shortcut is found with flag off then keep in workspace`() {
         // Given
         shortcuts =
             listOf(
@@ -222,7 +230,8 @@
     }
 
     @Test
-    fun `When archived unpinned shortcut is found then keep in workspace`() {
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When archived unpinned shortcut is found with flag off then keep in workspace`() {
         // Given
         shortcuts =
             listOf(
@@ -310,4 +319,34 @@
         assertThat(modelHelper.bgDataModel.deepShortcutMap).doesNotContainKey(expectedKey)
         verify(mockTaskController, times(0)).bindDeepShortcuts(eq(modelHelper.bgDataModel))
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When restoring archived shortcut with flag on then skip handling`() {
+        // Given
+        shortcuts =
+            listOf(
+                mock<ShortcutInfo>().apply {
+                    whenever(isPinned).thenReturn(true)
+                    whenever(id).thenReturn(expectedShortcutId)
+                }
+            )
+        val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+        items.put(expectedWai.id, expectedWai)
+        doReturn(
+                ApplicationInfo().apply {
+                    enabled = true
+                    flags = flags or FLAG_INSTALLED
+                    isArchived = true
+                }
+            )
+            .whenever(launcherApps)
+            .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+        doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+        // When
+        shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+        // Then
+        verify(mockTaskController, times(0)).deleteAndBindComponentsRemoved(any(), any())
+        verify(mockTaskController, times(0)).bindUpdatedWorkspaceItems(any())
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index da87dfc..7a403e1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -18,16 +18,19 @@
 
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
-import android.content.Context
 import android.content.Intent
+import android.content.pm.ApplicationInfo
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.ShortcutInfo
 import android.os.Process
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.util.LongSparseArray
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
@@ -44,9 +47,14 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_UI_NOT_READY
 import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON
+import com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORE_STARTED
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutKey
 import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.ContentWriter
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.UserIconInfo
@@ -55,6 +63,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -66,6 +75,7 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
@@ -73,20 +83,23 @@
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemProcessorTest {
 
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
     @Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
     @Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
     @Mock private lateinit var mockBgDataModel: BgDataModel
-    @Mock private lateinit var mockContext: Context
     @Mock private lateinit var mockAppState: LauncherAppState
     @Mock private lateinit var mockPmHelper: PackageManagerHelper
-    @Mock private lateinit var mockLauncherApps: LauncherApps
     @Mock private lateinit var mockCursor: LoaderCursor
     @Mock private lateinit var mockUserCache: UserCache
     @Mock private lateinit var mockUserManagerState: UserManagerState
     @Mock private lateinit var mockWidgetInflater: WidgetInflater
 
-    private var intent: Intent = Intent()
-    private var mUserHandle: UserHandle = UserHandle(0)
+    lateinit var mModelHelper: LauncherModelHelper
+    lateinit var mContext: SandboxModelContext
+    lateinit var mLauncherApps: LauncherApps
+    private var mIntent: Intent = Intent()
+    private var mUserHandle: UserHandle = Process.myUserHandle()
     private var mIconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf()
     private var mComponentName: ComponentName = ComponentName("package", "class")
     private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
@@ -101,40 +114,35 @@
 
     @Before
     fun setup() {
-        mUserHandle = UserHandle(0)
+        mModelHelper = LauncherModelHelper()
+        mContext = mModelHelper.sandboxContext
+        mLauncherApps =
+            mContext.spyService(LauncherApps::class.java).apply {
+                doReturn(true).whenever(this).isPackageEnabled("package", mUserHandle)
+                doReturn(true).whenever(this).isActivityEnabled(mComponentName, mUserHandle)
+            }
+        mUserHandle = Process.myUserHandle()
         mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
         mockWorkspaceInfo = mock<WorkspaceItemInfo>()
         mockBgDataModel = mock<BgDataModel>()
         mComponentName = ComponentName("package", "class")
         mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
-        intent =
+        mIntent =
             Intent().apply {
                 component = mComponentName
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
-        mockContext =
-            mock<Context>().apply {
-                whenever(packageManager).thenReturn(mock())
-                whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
-                whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
-                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
-            }
         mockAppState =
             mock<LauncherAppState>().apply {
-                whenever(context).thenReturn(mockContext)
+                whenever(context).thenReturn(mContext)
                 whenever(iconCache).thenReturn(mock())
                 whenever(iconCache.getShortcutIcon(any(), any(), any())).then {}
             }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
-                    .thenReturn(intent)
+                    .thenReturn(mIntent)
             }
         mockCursor =
             mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
@@ -143,9 +151,9 @@
                 id = 1
                 restoreFlag = 1
                 serialNumber = 101
-                whenever(parseIntent()).thenReturn(intent)
+                whenever(parseIntent()).thenReturn(mIntent)
                 whenever(markRestored()).doAnswer { restoreFlag = 0 }
-                whenever(updater().put(Favorites.INTENT, intent.toUri(0)).commit()).thenReturn(1)
+                whenever(updater().put(Favorites.INTENT, mIntent.toUri(0)).commit()).thenReturn(1)
                 whenever(getAppShortcutInfo(any(), any(), any(), any()))
                     .thenReturn(mockWorkspaceInfo)
                 whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
@@ -177,7 +185,7 @@
         memoryLogger: LoaderMemoryLogger? = null,
         userCache: UserCache = mockUserCache,
         userManagerState: UserManagerState = mockUserManagerState,
-        launcherApps: LauncherApps = mockLauncherApps,
+        launcherApps: LauncherApps = mLauncherApps,
         shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
         app: LauncherAppState = mockAppState,
         bgDataModel: BgDataModel = mockBgDataModel,
@@ -244,7 +252,7 @@
     fun `When app has null target package then mark deleted`() {
 
         // Given
-        intent.apply {
+        mIntent.apply {
             component = null
             `package` = null
         }
@@ -264,8 +272,8 @@
 
         // Given
         mComponentName = ComponentName("", "")
-        intent.component = mComponentName
-        intent.`package` = ""
+        mIntent.component = mComponentName
+        mIntent.`package` = ""
 
         // When
         itemProcessorUnderTest = createWorkspaceItemProcessorUnderTest()
@@ -298,15 +306,14 @@
     fun `When fallback Activity found for app then mark restored`() {
 
         // Given
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
-            }
+        mLauncherApps.apply {
+            whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+            whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
+        }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
-                    .thenReturn(intent)
+                    .thenReturn(mIntent)
             }
 
         // When
@@ -317,7 +324,7 @@
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
-        verify(mockCursor.updater().put(Favorites.INTENT, intent.toUri(0))).commit()
+        verify(mockCursor.updater().put(Favorites.INTENT, mIntent.toUri(0))).commit()
         assertThat(mIconRequestInfos).containsExactly(mockIconRequestInfo)
         verify(mockCursor).checkAndAddItem(mockWorkspaceInfo, mockBgDataModel, null)
     }
@@ -326,11 +333,10 @@
     fun `When app with disabled activity and no fallback found then mark deleted`() {
 
         // Given
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
-            }
+        mLauncherApps.apply {
+            whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+            whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
+        }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
@@ -358,11 +364,11 @@
 
     @Test
     fun `When valid Pinned Deep Shortcut then mark restored`() {
-
         // Given
         mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
         val expectedShortcutInfo =
             mock<ShortcutInfo>().apply {
+                whenever(userHandle).thenReturn(mUserHandle)
                 whenever(id).thenReturn("")
                 whenever(`package`).thenReturn("")
                 whenever(activity).thenReturn(mock())
@@ -372,7 +378,7 @@
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
             }
-        val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
+        val shortcutKey = ShortcutKey.fromIntent(mIntent, mockCursor.user)
         mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
         mIconRequestInfos = mutableListOf()
 
@@ -393,6 +399,67 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When Archived Deep Shortcut with flag on then mark restored`() {
+        // Given
+        val mockContentWriter: ContentWriter = mock()
+        val mockAppInfo: ApplicationInfo =
+            mock<ApplicationInfo>().apply {
+                isArchived = true
+                enabled = true
+            }
+        val expectedRestoreFlag = FLAG_RESTORED_ICON or FLAG_RESTORE_STARTED
+        doReturn(mockAppInfo).whenever(mLauncherApps).getApplicationInfo(any(), any(), any())
+        whenever(mockContentWriter.put(Favorites.RESTORED, expectedRestoreFlag))
+            .thenReturn(mockContentWriter)
+        whenever(mockContentWriter.commit()).thenReturn(1)
+        mockCursor.apply {
+            itemType = ITEM_TYPE_DEEP_SHORTCUT
+            restoreFlag = restoreFlag or FLAG_RESTORED_ICON
+            whenever(updater()).thenReturn(mockContentWriter)
+        }
+        mIconRequestInfos = mutableListOf()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        assertThat(mockCursor.restoreFlag and FLAG_RESTORED_ICON).isEqualTo(FLAG_RESTORED_ICON)
+        assertThat(mockCursor.restoreFlag and FLAG_RESTORE_STARTED).isEqualTo(FLAG_RESTORE_STARTED)
+        assertThat(mIconRequestInfos).isNotEmpty()
+        assertThat(mAllDeepShortcuts).isEmpty()
+        verify(mockContentWriter).put(Favorites.RESTORED, expectedRestoreFlag)
+        verify(mockCursor).checkAndAddItem(any(), eq(mockBgDataModel), eq(null))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+    fun `When Archived Deep Shortcut with flag off then remove`() {
+        // Given
+        mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
+        mIconRequestInfos = mutableListOf()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        assertWithMessage("item restoreFlag should be set to 0")
+            .that(mockCursor.restoreFlag)
+            .isEqualTo(0)
+        assertThat(mIconRequestInfos).isEmpty()
+        assertThat(mAllDeepShortcuts).isEmpty()
+        verify(mockCursor)
+            .markDeleted(
+                "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+                "shortcut_not_found",
+            )
+    }
+
+    @Test
     fun `When Pinned Deep Shortcut is not stored in ShortcutManager re-query by Shortcut ID`() {
         // Given
         mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
@@ -406,8 +473,9 @@
                 whenever(disabledMessage).thenReturn("")
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
+                whenever(userHandle).thenReturn(mUserHandle)
             }
-        whenever(mockLauncherApps.getShortcuts(any(), any())).thenReturn(listOf(si))
+        doReturn(listOf(si)).whenever(mLauncherApps).getShortcuts(any(), any())
         mKeyToPinnedShortcutsMap.clear()
         mIconRequestInfos = mutableListOf()
 
@@ -417,12 +485,12 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockLauncherApps).getShortcuts(any(), any())
+        verify(mLauncherApps).getShortcuts(any(), any())
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         verify(mockCursor).markRestored()
-        verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
+        verify(mockCursor).checkAndAddItem(any(), any(), eq(null))
     }
 
     @Test
@@ -469,11 +537,11 @@
             }
         mIconRequestInfos = mutableListOf()
         // Make sure shortcuts map has expected key from expected package
-        intent.`package` = mComponentName.packageName
-        val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
+        mIntent.`package` = mComponentName.packageName
+        val shortcutKey = ShortcutKey.fromIntent(mIntent, mockCursor.user)
         mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
         // set intent package back to null to test scenario
-        intent.`package` = null
+        mIntent.`package` = null
 
         // When
         itemProcessorUnderTest =
@@ -656,7 +724,7 @@
         itemProcessorUnderTest.processItem()
 
         // Then
-        verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
+        verify(mockCursor).checkAndAddItem(any(), eq(mockBgDataModel), eq(null))
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index dba7603..cdb45fc 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -1,7 +1,10 @@
 package com.android.launcher3.model
 
 import android.appwidget.AppWidgetManager
+import android.content.ComponentName
 import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
 import android.os.Process
 import android.os.UserHandle
 import android.platform.test.annotations.DisableFlags
@@ -28,6 +31,9 @@
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.icons.cache.CachingLogic
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.IconRequestInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.provider.RestoreDbTask
 import com.android.launcher3.ui.TestViewHelpers
@@ -38,8 +44,11 @@
 import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.UserIconInfo
 import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
+import java.util.concurrent.CountDownLatch
+import java.util.function.Predicate
 import junit.framework.Assert.assertEquals
 import org.junit.After
 import org.junit.Before
@@ -58,12 +67,11 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
-import java.util.concurrent.CountDownLatch
-import java.util.function.Predicate
 
 private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql"
 
@@ -480,6 +488,157 @@
         verify(spyContext, times(0)).sendBroadcast(any())
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    fun `When flag on then archived AllApps icons found on Workspace loaded from db`() {
+        // Given
+        // Given
+        val activityInfo: LauncherActivityInfo = mock()
+        val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = true }
+        whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+        val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+        val expectedComponent = ComponentName("package", "class")
+        val workspaceIconRequests =
+            listOf(
+                IconRequestInfo<WorkspaceItemInfo>(
+                    WorkspaceItemInfo().apply {
+                        intent = Intent().apply { component = expectedComponent }
+                    },
+                    activityInfo,
+                    expectedIconBlob,
+                    false, /* useLowResIcon */
+                )
+            )
+        val expectedAppInfo = AppInfo().apply { componentName = expectedComponent }
+        // When
+        val loader =
+            LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
+        val actualIconRequest =
+            loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+        // Then
+        assertThat(actualIconRequest.iconBlob).isEqualTo(expectedIconBlob)
+        assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    fun `When flag on then unarchived AllApps icons not loaded from db`() {
+        // Given
+        val activityInfo: LauncherActivityInfo = mock()
+        val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = false }
+        whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+        val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+        val expectedComponent = ComponentName("package", "class")
+        val workspaceIconRequests =
+            listOf(
+                IconRequestInfo<WorkspaceItemInfo>(
+                    WorkspaceItemInfo().apply {
+                        intent = Intent().apply { component = expectedComponent }
+                    },
+                    activityInfo,
+                    expectedIconBlob,
+                    false, /* useLowResIcon */
+                )
+            )
+        val expectedAppInfo = AppInfo().apply { componentName = expectedComponent }
+        // When
+        val loader =
+            LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
+        val actualIconRequest =
+            loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+        // Then
+        assertThat(actualIconRequest.iconBlob).isNull()
+        assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    fun `When flag on then archived AllApps icon not found on Workspace not loaded from db`() {
+        // Given
+        val activityInfo: LauncherActivityInfo = mock()
+        val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = true }
+        whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+        val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+        val expectedComponent = ComponentName("package", "class")
+        val workspaceIconRequests =
+            listOf(
+                IconRequestInfo<WorkspaceItemInfo>(
+                    WorkspaceItemInfo().apply {
+                        intent = Intent().apply { component = expectedComponent }
+                    },
+                    activityInfo,
+                    expectedIconBlob,
+                    false, /* useLowResIcon */
+                )
+            )
+        val expectedAppInfo =
+            AppInfo().apply { componentName = ComponentName("differentPkg", "differentClass") }
+        // When
+        val loader =
+            LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
+        val actualIconRequest =
+            loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+        // Then
+        assertThat(actualIconRequest.iconBlob).isNull()
+        assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+    fun `When flag off then archived AllApps icons not loaded from db`() {
+        // Given
+        val activityInfo: LauncherActivityInfo = mock()
+        val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = true }
+        whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+        val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+        val workspaceIconRequests =
+            listOf(
+                IconRequestInfo<WorkspaceItemInfo>(
+                    WorkspaceItemInfo(),
+                    activityInfo,
+                    expectedIconBlob,
+                    false, /* useLowResIcon */
+                )
+            )
+        val expectedAppInfo = AppInfo()
+        // When
+        val loader =
+            LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
+        val actualIconRequest =
+            loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+        // Then
+        assertThat(actualIconRequest.iconBlob).isNull()
+        assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+    }
+
     @LauncherAppSingleton
     @Component(modules = [AllModulesForTest::class])
     interface TestComponent : LauncherAppComponent {
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
index cb04e13..cab1ebe 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
@@ -116,7 +116,13 @@
      */
     @ScreenRecord // b/381918059
     @Test
-    public void testAddAndDeletePageAndFling() {
+    public void testAddAndDeletePageAndFling() throws Exception {
+        // Set workspace  that includes the chrome Activity app icon on the hotseat.
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder()
+                .atHotseat(0).putApp("com.android.chrome", "com.google.android.apps.chrome.Main");
+        mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, builder);
+        reinitializeLauncherData();
+
         Workspace workspace = mLauncher.getWorkspace();
         // Get the first app from the hotseat
         HomeAppIcon hotSeatIcon = workspace.getHotseatAppIcon(0);
diff --git a/tests/src/com/android/launcher3/util/WallpaperThemeManagerTest.kt b/tests/src/com/android/launcher3/util/WallpaperThemeManagerTest.kt
new file mode 100644
index 0000000..4c8fd8a
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/WallpaperThemeManagerTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.util
+
+import android.app.Activity
+import android.content.ComponentCallbacks
+import android.content.res.Configuration
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.any
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.eq
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.ExtendedMockito.times
+import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.launcher3.util.WallpaperThemeManager.Companion.setWallpaperDependentTheme
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.whenever
+
+/** Tests for WallpaperThemeManager */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WallpaperThemeManagerTest {
+
+    @get:Rule val context = SandboxApplication()
+
+    @Mock lateinit var activity: Activity
+    @Captor lateinit var callbacksCaptor: ArgumentCaptor<ComponentCallbacks>
+
+    private lateinit var mockSession: StaticMockitoSession
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mockSession = mockitoSession().spyStatic(Themes::class.java).startMocking()
+
+        doReturn(1).`when`<Int> { Themes.getActivityThemeRes(any()) }
+        doReturn(context).whenever(activity).applicationContext
+    }
+
+    @After
+    fun tearDown() {
+        mockSession.finishMocking()
+    }
+
+    @Test
+    fun `correct theme set on activity create`() {
+        activity.setWallpaperDependentTheme()
+        verify(activity, times(1)).setTheme(eq(1))
+    }
+
+    @Test
+    fun `ignores update if theme does not change`() {
+        activity.setWallpaperDependentTheme()
+        verify(activity).registerComponentCallbacks(callbacksCaptor.capture())
+        callbacksCaptor.value.onConfigurationChanged(Configuration())
+        verify(activity, never()).recreate()
+    }
+
+    @Test
+    fun `activity recreated if theme changes`() {
+        activity.setWallpaperDependentTheme()
+        verify(activity).registerComponentCallbacks(callbacksCaptor.capture())
+
+        doReturn(3).`when`<Int> { Themes.getActivityThemeRes(any()) }
+        callbacksCaptor.value.onConfigurationChanged(Configuration())
+        verify(activity, times(1)).recreate()
+    }
+}