Merge "Use launcher db icon column to load pre-archived app icons on restore, similar to Promise Icons." into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 5186740..72b3c27 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -65,13 +65,6 @@
 }
 
 flag {
-    name: "enable_taskbar_connected_displays"
-    namespace: "launcher"
-    description: "Enables connected displays in taskbar."
-    bug: "362720616"
-}
-
-flag {
     name: "enable_taskbar_customization"
     namespace: "launcher"
     description: "Enables taskbar customization framework."
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/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/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..690dec4 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;
@@ -178,10 +177,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;
@@ -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.
      */
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/util/ActiveTrackpadList.kt b/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt
index 63bd03d..a1ff0ce 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt
+++ b/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt
@@ -30,7 +30,7 @@
 
     init {
         inputManager.registerInputDeviceListener(this, Executors.UI_HELPER_EXECUTOR.handler)
-        inputManager.inputDeviceIds.forEach { deviceId -> onInputDeviceAdded(deviceId) }
+        inputManager.inputDeviceIds.filter(this::isTrackpadDevice).forEach(this::add)
     }
 
     override fun onInputDeviceAdded(deviceId: Int) {
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/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/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..9c8b249 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -213,9 +213,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;
@@ -1952,7 +1954,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 +1968,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);
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 9807b0d..ce0efb6 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
@@ -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/quickstep/util/ActiveTrackpadListTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt
new file mode 100644
index 0000000..b4c236e
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.hardware.input.InputManager
+import android.view.InputDevice
+import android.view.InputDevice.SOURCE_MOUSE
+import android.view.InputDevice.SOURCE_TOUCHPAD
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.TestUtil
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class ActiveTrackpadListTest {
+
+    @get:Rule val context = SandboxApplication()
+
+    private val inputDeviceIds = IntArray()
+    private lateinit var inputManager: InputManager
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        inputManager = context.spyService(InputManager::class.java)
+        doAnswer { inputDeviceIds.toArray() }.whenever(inputManager).inputDeviceIds
+
+        doReturn(null).whenever(inputManager).getInputDevice(eq(1))
+        doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD))
+            .whenever(inputManager)
+            .getInputDevice(eq(2))
+        doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD))
+            .whenever(inputManager)
+            .getInputDevice(eq(3))
+        doReturn(mockDevice(SOURCE_MOUSE)).whenever(inputManager).getInputDevice(eq(4))
+    }
+
+    @Test
+    fun `initialize correct devices`() {
+        inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4))
+
+        val list = ActiveTrackpadList(context) {}
+        assertEquals(2, list.size())
+        assertTrue(list.contains(2))
+        assertTrue(list.contains(3))
+    }
+
+    @Test
+    fun `update callback not called in constructor`() {
+        inputDeviceIds.addAll(IntArray.wrap(2, 3))
+
+        var updateCalled = false
+        val list = ActiveTrackpadList(context) { updateCalled = true }
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+
+        assertEquals(2, list.size())
+        assertFalse(updateCalled)
+    }
+
+    @Test
+    fun `update called on add only once`() {
+        var updateCalled = false
+        val list = ActiveTrackpadList(context) { updateCalled = true }
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+
+        assertFalse(updateCalled)
+        assertEquals(0, list.size())
+
+        list.onInputDeviceAdded(1)
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertFalse(updateCalled)
+        assertEquals(0, list.size())
+
+        list.onInputDeviceAdded(2)
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertTrue(updateCalled)
+        assertEquals(1, list.size())
+
+        updateCalled = false
+        list.onInputDeviceAdded(3)
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertFalse(updateCalled)
+        assertEquals(2, list.size())
+    }
+
+    @Test
+    fun `update called on remove only once`() {
+        var updateCalled = false
+        inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4))
+        val list = ActiveTrackpadList(context) { updateCalled = true }
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertEquals(2, list.size())
+
+        list.onInputDeviceRemoved(2)
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertEquals(1, list.size())
+        assertFalse(updateCalled)
+
+        list.onInputDeviceRemoved(3)
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+        assertEquals(0, list.size())
+        assertTrue(updateCalled)
+    }
+
+    private fun mockDevice(sources: Int) =
+        mock(InputDevice::class.java).apply { doReturn(sources).whenever(this).sources }
+}
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/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/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index b4a24f1..813d8f1 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -25,7 +25,6 @@
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.Utilities.pxFromSp;
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp;
@@ -52,6 +51,7 @@
 
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.DevicePaddings.DevicePadding;
+import com.android.launcher3.folder.ClippedFolderIconLayoutRule;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
@@ -1228,7 +1228,7 @@
     }
 
     private int getIconSizeWithOverlap(int iconSize) {
-        return (int) Math.ceil(iconSize * ICON_OVERLAP_FACTOR);
+        return (int) Math.ceil(iconSize * ClippedFolderIconLayoutRule.getIconOverlapFactor());
     }
 
     /**
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/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
index 8cd91d3..cf5150a 100644
--- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -1,5 +1,7 @@
 package com.android.launcher3.folder;
 
+import com.android.launcher3.Flags;
+
 public class ClippedFolderIconLayoutRule {
 
     public static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
@@ -7,9 +9,12 @@
 
     private static final float MIN_SCALE = 0.44f;
     private static final float MAX_SCALE = 0.51f;
+    // TODO: figure out exact radius for different icons
+    private static final float MAX_RADIUS_DILATION_SHAPES = 0.15f;
     private static final float MAX_RADIUS_DILATION = 0.25f;
     // The max amount of overlap the preview items can go outside of the background bounds.
     public static final float ICON_OVERLAP_FACTOR = 1 + (MAX_RADIUS_DILATION / 2f);
+    public static final float ICON_OVERLAP_FACTOR_SHAPES = 1f;
     private static final float ITEM_RADIUS_SCALE_FACTOR = 1.15f;
 
     public static final int EXIT_INDEX = -2;
@@ -28,7 +33,7 @@
         mRadius = ITEM_RADIUS_SCALE_FACTOR * availableSpace / 2f;
         mIconSize = intrinsicIconSize;
         mIsRtl = rtl;
-        mBaselineIconScale = availableSpace / (intrinsicIconSize * 1f);
+        mBaselineIconScale = availableSpace / intrinsicIconSize;
     }
 
     public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
@@ -84,6 +89,7 @@
         result[1] = top + (row * dy);
     }
 
+    // b/392610664 TODO: Change positioning from circular geometry to square / grid-based.
     private void getPosition(int index, int curNumItems, float[] result) {
         // The case of two items is homomorphic to the case of one.
         curNumItems = Math.max(curNumItems, 2);
@@ -113,8 +119,10 @@
         }
 
         // We bump the radius up between 0 and MAX_RADIUS_DILATION % as the number of items increase
-        float radius = mRadius * (1 + MAX_RADIUS_DILATION * (curNumItems -
-                MIN_NUM_ITEMS_IN_PREVIEW) / (MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
+        float radiusDilation = Flags.enableLauncherIconShapes() ? MAX_RADIUS_DILATION_SHAPES
+                : MAX_RADIUS_DILATION;
+        float radius = mRadius * (1 + radiusDilation * (curNumItems - MIN_NUM_ITEMS_IN_PREVIEW)
+                / (MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
         double theta = theta0 + index * (2 * Math.PI / curNumItems) * direction;
 
         float halfIconSize = (mIconSize * scaleForItem(curNumItems)) / 2;
@@ -130,7 +138,7 @@
     public float scaleForItem(int numItems) {
         // Scale is determined by the number of items in the preview.
         final float scale;
-        if (numItems <= 3) {
+        if (numItems <= 3 && !Flags.enableLauncherIconShapes()) {
             scale = MAX_SCALE;
         } else {
             scale = MIN_SCALE;
@@ -141,4 +149,15 @@
     public float getIconSize() {
         return mIconSize;
     }
+
+    /**
+     * Gets correct constant for icon overlap.
+     */
+    public static float getIconOverlapFactor() {
+        if (Flags.enableLauncherIconShapes()) {
+            return ICON_OVERLAP_FACTOR_SHAPES;
+        } else {
+            return ICON_OVERLAP_FACTOR;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index b7b378e..0ed8787 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.folder;
 
 import static com.android.launcher3.Flags.enableCursorHoverStates;
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
@@ -248,7 +247,8 @@
         mPreviewItemManager.recomputePreviewDrawingParams();
         mBackground.getBounds(outBounds);
         // The preview items go outside of the bounds of the background.
-        Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR);
+        Utilities.scaleRectAboutCenter(outBounds,
+                ClippedFolderIconLayoutRule.getIconOverlapFactor());
     }
 
     public float getBackgroundStrokeWidth() {
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index d9c60db..77fa355 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -18,7 +18,6 @@
 
 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.Animator;
@@ -373,7 +372,7 @@
 
     public Path getClipPath() {
         mPath.reset();
-        float radius = getScaledRadius() * ICON_OVERLAP_FACTOR;
+        float radius = getScaledRadius() * ClippedFolderIconLayoutRule.getIconOverlapFactor();
         // Find the difference in radius so that the clip path remains centered.
         float radiusDifference = radius - getRadius();
         float offsetX = basePreviewOffsetX - radiusDifference;
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 5a2864d..9edc386 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -47,7 +47,7 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.shapes.IconShapeModel;
-import com.android.launcher3.shapes.IconShapesProvider;
+import com.android.launcher3.shapes.ShapesProvider;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
@@ -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";
@@ -145,7 +148,7 @@
                             KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
                     String currentShapePath =
                             ThemeManager.INSTANCE.get(context).getIconState().getIconMask();
-                    for (IconShapeModel shape : IconShapesProvider.INSTANCE.getShapes().values()) {
+                    for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes().values()) {
                         cursor.newRow()
                                 .add(KEY_SHAPE_KEY, shape.getKey())
                                 .add(KEY_SHAPE_TITLE, shape.getTitle())
@@ -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/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index f24c2ab..9f35e4a 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -27,7 +27,7 @@
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.icons.IconThemeController
 import com.android.launcher3.icons.mono.MonoIconThemeController
-import com.android.launcher3.shapes.IconShapesProvider
+import com.android.launcher3.shapes.ShapesProvider
 import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
@@ -94,7 +94,7 @@
     private fun parseIconState(): IconState {
         val shapeModel =
             prefs.get(PREF_ICON_SHAPE).let { shapeOverride ->
-                IconShapesProvider.shapes.values.firstOrNull { it.key == shapeOverride }
+                ShapesProvider.iconShapes.values.firstOrNull { it.key == shapeOverride }
             }
         val iconMask =
             when {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 6eb02ab..2d30466 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),
 
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/shapes/IconShapesProvider.kt b/src/com/android/launcher3/shapes/IconShapesProvider.kt
deleted file mode 100644
index 8608437..0000000
--- a/src/com/android/launcher3/shapes/IconShapesProvider.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shapes
-
-import com.android.launcher3.Flags as LauncherFlags
-import com.android.systemui.shared.Flags
-
-object IconShapesProvider {
-    val shapes =
-        if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
-            mapOf(
-                "arch" to
-                    IconShapeModel(
-                        key = "arch",
-                        title = "arch",
-                        pathString =
-                            "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
-                    ),
-                "4_sided_cookie" to
-                    IconShapeModel(
-                        key = "4_sided_cookie",
-                        title = "4 sided cookie",
-                        pathString =
-                            "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
-                    ),
-                "seven_sided_cookie" to
-                    IconShapeModel(
-                        key = "seven_sided_cookie",
-                        title = "7 sided cookie",
-                        pathString =
-                            "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
-                    ),
-                "sunny" to
-                    IconShapeModel(
-                        key = "sunny",
-                        title = "sunny",
-                        pathString =
-                            "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
-                    ),
-                "circle" to
-                    IconShapeModel(
-                        key = "circle",
-                        title = "circle",
-                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
-                    ),
-                "square" to
-                    IconShapeModel(
-                        key = "square",
-                        title = "square",
-                        pathString =
-                            "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
-                    ),
-            )
-        } else {
-            mapOf(
-                "circle" to
-                    IconShapeModel(
-                        key = "circle",
-                        title = "circle",
-                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
-                    )
-            )
-        }
-}
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
new file mode 100644
index 0000000..dfb7793
--- /dev/null
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.shapes
+
+import com.android.launcher3.Flags as LauncherFlags
+import com.android.systemui.shared.Flags
+
+object ShapesProvider {
+    val folderShapes =
+        if (LauncherFlags.enableLauncherIconShapes()) {
+            mapOf(
+                "clover" to
+                    "M 39.616 4" +
+                        "C 46.224 6.87 53.727 6.87 60.335 4" +
+                        "L 63.884 2.459" +
+                        "C 85.178 -6.789 106.789 14.822 97.541 36.116" +
+                        "L 96 39.665" +
+                        "C 93.13 46.273 93.13 53.776 96 60.384" +
+                        "L 97.541 63.934" +
+                        "C 106.789 85.227 85.178 106.839 63.884 97.591" +
+                        "L 60.335 96.049" +
+                        "C 53.727 93.179 46.224 93.179 39.616 96.049" +
+                        "L 36.066 97.591" +
+                        "C 14.773 106.839 -6.839 85.227 2.409 63.934" +
+                        "L 3.951 60.384" +
+                        "C 6.821 53.776 6.821 46.273 3.951 39.665" +
+                        "L 2.409 36.116" +
+                        "C -6.839 14.822 14.773 -6.789 36.066 2.459" +
+                        "Z",
+                "complexClover" to
+                    "M 49.85 6.764" +
+                        "L 50.013 6.971" +
+                        "L 50.175 6.764" +
+                        "C 53.422 2.635 58.309 0.207 63.538 0.207" +
+                        "C 65.872 0.207 68.175 0.692 70.381 1.648" +
+                        "L 71.79 2.264" +
+                        "L 71.792 2.265" +
+                        "A 3.46 3.46 0 0 0 74.515 2.265" +
+                        "L 74.517 2.264" +
+                        "L 75.926 1.652" +
+                        "A 17.1 17.1 0 0 1 82.769 0.207" +
+                        "C 88.495 0.207 93.824 3.117 97.022 7.989" +
+                        "C 100.21 12.848 100.697 18.712 98.36 24.087" +
+                        "L 97.749 25.496" +
+                        "V 25.497" +
+                        "A 3.45 3.45 0 0 0 97.749 28.222" +
+                        "V 28.223" +
+                        "L 98.36 29.632" +
+                        "C 100.697 35.007 100.207 40.871 97.022 45.73" +
+                        "A 17.5 17.5 0 0 1 93.264 49.838" +
+                        "L 93.06 50" +
+                        "L 93.264 50.162" +
+                        "A 17.5 17.5 0 0 1 97.022 54.27" +
+                        "C 100.21 59.129 100.697 64.993 98.36 70.368" +
+                        "V 71.778" +
+                        "A 3.45 3.45 0 0 0 97.749 74.503" +
+                        "V 74.504" +
+                        "L 98.36 75.913" +
+                        "C 100.697 81.288 100.207 87.152 97.022 92.011" +
+                        "C 93.824 96.883 88.495 99.793 82.769 99.793" +
+                        "C 80.435 99.793 78.132 99.308 75.926 98.348" +
+                        "L 74.517 97.736" +
+                        "H 74.515" +
+                        "A 3.5 3.5 0 0 0 73.153 97.455" +
+                        "C 72.682 97.455 72.225 97.552 71.792 97.736" +
+                        "H 71.79" +
+                        "L 70.381 98.348" +
+                        "A 17.1 17.1 0 0 1 63.538 99.793" +
+                        "C 58.309 99.793 53.422 97.365 50.175 93.236" +
+                        "L 50.013 93.029" +
+                        "L 49.85 93.236" +
+                        "C 46.603 97.365 41.717 99.793 36.488 99.793" +
+                        "C 34.154 99.793 31.851 99.308 29.645 98.348" +
+                        "L 28.236 97.736" +
+                        "H 28.234" +
+                        "A 3.5 3.5 0 0 0 26.872 97.455" +
+                        "C 26.401 97.455 25.944 97.552 25.511 97.736" +
+                        "H 25.509" +
+                        "L 24.1 98.348" +
+                        "A 17.1 17.1 0 0 1 17.257 99.793" +
+                        "C 11.53 99.793 6.202 96.883 3.004 92.011" +
+                        "C -0.181 87.152 -0.671 81.288 1.661 75.913" +
+                        "L 2.277 74.504" +
+                        "V 74.503" +
+                        "A 3.45 3.45 0 0 0 2.277 71.778" +
+                        "V 71.777" +
+                        "L 1.665 70.368" +
+                        "C -0.671 64.993 -0.181 59.129 3.004 54.274" +
+                        "A 17.5 17.5 0 0 1 6.761 50.162" +
+                        "L 6.965 50" +
+                        "L 6.761 49.838" +
+                        "A 17.5 17.5 0 0 1 3.004 45.73" +
+                        "C -0.181 40.871 -0.671 35.007 1.665 29.632" +
+                        "L 2.277 28.223" +
+                        "V 28.222" +
+                        "A 3.45 3.45 0 0 0 2.277 25.497" +
+                        "V 25.496" +
+                        "L 1.665 24.087" +
+                        "C -0.671 18.712 -0.181 12.848 3.004 7.994" +
+                        "V 7.993" +
+                        "C 6.202 3.117 11.53 0.207 17.257 0.207" +
+                        "C 19.591 0.207 21.894 0.692 24.1 1.652" +
+                        "L 25.509 2.264" +
+                        "L 25.511 2.265" +
+                        "A 3.46 3.46 0 0 0 28.234 2.265" +
+                        "L 28.236 2.264" +
+                        "L 29.645 1.652" +
+                        "A 17.1 17.1 0 0 1 36.488 0.207" +
+                        "C 41.717 0.207 46.603 2.635 49.85 6.764" +
+                        "Z",
+                "arch" to
+                    "M 50 0" +
+                        "L 72.5 0" +
+                        "A 27.5 27.5 0 0 1 100 27.5" +
+                        "L 100 86.67" +
+                        "A 13.33 13.33 0 0 1 86.67 100" +
+                        "L 13.33 100" +
+                        "A 13.33 13.33 0 0 1 0 86.67" +
+                        "L 0 27.5" +
+                        "A 27.5 27.5 0 0 1 27.5 0" +
+                        "Z",
+                "square" to
+                    "M 50 0" +
+                        "L 83.4 0" +
+                        "A 16.6 16.6 0 0 1 100 16.6" +
+                        "L 100 83.4" +
+                        "A 16.6 16.6 0 0 1 83.4 100" +
+                        "L 16.6 100" +
+                        "A 16.6 16.6 0 0 1 0 83.4" +
+                        "L 0 16.6" +
+                        "A 16.6 16.6 0 0 1 16.6 0" +
+                        "Z",
+            )
+        } else {
+            mapOf("circle" to "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0")
+        }
+
+    val iconShapes =
+        if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
+            mapOf(
+                "arch" to
+                    IconShapeModel(
+                        key = "arch",
+                        title = "arch",
+                        pathString =
+                            "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
+                        folderPathString = folderShapes["arch"]!!,
+                    ),
+                "four_sided_cookie" to
+                    IconShapeModel(
+                        key = "four_sided_cookie",
+                        title = "4 sided cookie",
+                        pathString =
+                            "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
+                        folderPathString = folderShapes["complexClover"]!!,
+                    ),
+                "seven_sided_cookie" to
+                    IconShapeModel(
+                        key = "seven_sided_cookie",
+                        title = "7 sided cookie",
+                        pathString =
+                            "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
+                        folderPathString = folderShapes["clover"]!!,
+                    ),
+                "sunny" to
+                    IconShapeModel(
+                        key = "sunny",
+                        title = "sunny",
+                        pathString =
+                            "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
+                        folderPathString = folderShapes["clover"]!!,
+                    ),
+                "circle" to
+                    IconShapeModel(
+                        key = "circle",
+                        title = "circle",
+                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+                        folderPathString = folderShapes["clover"]!!,
+                    ),
+                "square" to
+                    IconShapeModel(
+                        key = "square",
+                        title = "square",
+                        pathString =
+                            "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
+                        folderShapes["square"]!!,
+                    ),
+            )
+        } else {
+            mapOf(
+                "default" to
+                    IconShapeModel(
+                        key = "default",
+                        title = "circle",
+                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+                    )
+            )
+        }
+}
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/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
similarity index 63%
rename from tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
index 234e050..2b8896e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -30,14 +30,14 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class IconShapesProviderTest {
+class ShapesProviderTest {
 
     @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path arch`() {
-        IconShapesProvider.shapes["arch"]?.apply {
+        ShapesProvider.iconShapes["arch"]?.apply {
             GenericPathShape(pathString)
             PathParser.createPathFromPathData(pathString)
         }
@@ -46,7 +46,7 @@
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path 4_sided_cookie`() {
-        IconShapesProvider.shapes["4_sided_cookie"]?.apply {
+        ShapesProvider.iconShapes["4_sided_cookie"]?.apply {
             GenericPathShape(pathString)
             PathParser.createPathFromPathData(pathString)
         }
@@ -55,7 +55,7 @@
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path seven_sided_cookie`() {
-        IconShapesProvider.shapes["seven_sided_cookie"]?.apply {
+        ShapesProvider.iconShapes["seven_sided_cookie"]?.apply {
             GenericPathShape(pathString)
             PathParser.createPathFromPathData(pathString)
         }
@@ -64,7 +64,7 @@
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path sunny`() {
-        IconShapesProvider.shapes["sunny"]?.apply {
+        ShapesProvider.iconShapes["sunny"]?.apply {
             GenericPathShape(pathString)
             PathParser.createPathFromPathData(pathString)
         }
@@ -73,7 +73,7 @@
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path circle`() {
-        IconShapesProvider.shapes["circle"]?.apply {
+        ShapesProvider.iconShapes["circle"]?.apply {
             GenericPathShape(pathString)
             PathParser.createPathFromPathData(pathString)
         }
@@ -82,7 +82,43 @@
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path square`() {
-        IconShapesProvider.shapes["square"]?.apply {
+        ShapesProvider.iconShapes["square"]?.apply {
+            GenericPathShape(pathString)
+            PathParser.createPathFromPathData(pathString)
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    fun `verify valid folder path clover`() {
+        ShapesProvider.folderShapes["clover"]?.let { pathString ->
+            GenericPathShape(pathString)
+            PathParser.createPathFromPathData(pathString)
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    fun `verify valid folder path complexClover`() {
+        ShapesProvider.folderShapes["complexClover"]?.let { pathString ->
+            GenericPathShape(pathString)
+            PathParser.createPathFromPathData(pathString)
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    fun `verify valid folder path arch`() {
+        ShapesProvider.folderShapes["arch"]?.let { pathString ->
+            GenericPathShape(pathString)
+            PathParser.createPathFromPathData(pathString)
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+    fun `verify valid folder path square`() {
+        ShapesProvider.folderShapes["square"]?.let { pathString ->
             GenericPathShape(pathString)
             PathParser.createPathFromPathData(pathString)
         }