Merge "Fix ConcurrentModificationException in QuickstepWidgetHolder" into udc-dev
diff --git a/OWNERS b/OWNERS
index 76644b3..b684460 100644
--- a/OWNERS
+++ b/OWNERS
@@ -12,6 +12,7 @@
 vadimt@google.com
 winsonc@google.com
 jonmiranda@google.com
+alexchau@google.com
 
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 9db03f5..7e0530b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -674,18 +674,6 @@
     }
 
     /**
-     * Notify system to inset the rounded corner frame based on the task bar insets.
-     */
-    public void updateInsetRoundedCornerFrame(boolean shouldInsetsRoundedCorner) {
-        if (!mDragLayer.isAttachedToWindow()
-                || mWindowLayoutParams.insetsRoundedCornerFrame == shouldInsetsRoundedCorner) {
-            return;
-        }
-        mWindowLayoutParams.insetsRoundedCornerFrame = shouldInsetsRoundedCorner;
-        mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
-    }
-
-    /**
      * Updates the TaskbarContainer height (pass {@link #getDefaultTaskbarWindowHeight()} to reset).
      */
     public void setTaskbarWindowHeight(int height) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index c029097..19b9a18 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -71,7 +71,6 @@
     fun init(controllers: TaskbarControllers) {
         this.controllers = controllers
         windowLayoutParams = context.windowLayoutParams
-        windowLayoutParams.insetsRoundedCornerFrame = true
         onTaskbarWindowHeightOrInsetsChanged()
 
         context.addOnDeviceProfileChangeListener(deviceProfileChangeListener)
@@ -86,23 +85,23 @@
     fun onTaskbarWindowHeightOrInsetsChanged() {
         if (context.isGestureNav) {
             windowLayoutParams.providedInsets =
-                    arrayOf(
-                            InsetsFrameProvider(insetsOwner, 0, navigationBars())
-                                    .setFlags(FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM),
-                            InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                            InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
-                            InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
-                                    .setSource(SOURCE_DISPLAY),
-                            InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
-                                    .setSource(SOURCE_DISPLAY)
-                    )
+                arrayOf(
+                    InsetsFrameProvider(insetsOwner, 0, navigationBars())
+                        .setFlags(FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM),
+                    InsetsFrameProvider(insetsOwner, 0, tappableElement()),
+                    InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
+                    InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
+                        .setSource(SOURCE_DISPLAY),
+                    InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
+                        .setSource(SOURCE_DISPLAY)
+                )
         } else {
             windowLayoutParams.providedInsets =
-                    arrayOf(
-                            InsetsFrameProvider(insetsOwner, 0, navigationBars()),
-                            InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                            InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures())
-                    )
+                arrayOf(
+                    InsetsFrameProvider(insetsOwner, 0, navigationBars()),
+                    InsetsFrameProvider(insetsOwner, 0, tappableElement()),
+                    InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures())
+                )
         }
 
         val touchableHeight = controllers.taskbarStashController.touchableHeight
@@ -162,6 +161,11 @@
                 provider.insetsSizeOverrides = insetsSizeOverride
             }
         }
+
+        // We only report tappableElement height for unstashed, persistent taskbar,
+        // which is also when we draw the rounded corners above taskbar.
+        windowLayoutParams.insetsRoundedCornerFrame = tappableHeight > 0
+
         context.notifyUpdateLayoutParams()
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index e334d05..b2f9378 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -106,9 +106,6 @@
             | FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS
             | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO;
 
-    private static final int FLAGS_STASHED_IN_APP_IGNORING_IME =
-            FLAGS_STASHED_IN_APP & ~FLAG_STASHED_IN_APP_IME;
-
     // If any of these flags are enabled, inset apps by our stashed height instead of our unstashed
     // height. This way the reported insets are consistent even during transitions out of the app.
     // Currently any flag that causes us to stash in an app is included, except for IME or All Apps
@@ -414,13 +411,6 @@
     }
 
     /**
-     * Returns whether the taskbar should be stashed in apps regardless of the IME visibility.
-     */
-    public boolean isStashedInAppIgnoringIme() {
-        return hasAnyFlag(FLAGS_STASHED_IN_APP_IGNORING_IME);
-    }
-
-    /**
      * Returns whether the taskbar should be stashed in the current LauncherState.
      */
     public boolean isInStashedLauncherState() {
@@ -1059,11 +1049,6 @@
     private void notifyStashChange(boolean visible, boolean stashed) {
         mSystemUiProxy.notifyTaskbarStatus(visible, stashed);
         setUpTaskbarSystemAction(visible);
-        // If stashing taskbar is caused by IME visibility, we could just skip updating rounded
-        // corner insets since the rounded corners will be covered by IME during IME is showing and
-        // taskbar will be restored back to unstashed when IME is hidden.
-        mControllers.taskbarActivityContext.updateInsetRoundedCornerFrame(
-                    visible && !isStashedInAppIgnoringIme());
         mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed);
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index d67dbae..549c50b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1317,5 +1317,8 @@
         writer.println("\nQuickstepLauncher:");
         writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" :
                 recentsView.getPagedViewOrientedState()));
+        if (recentsView != null) {
+            recentsView.getSplitSelectController().dump(prefix, writer);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index b7a29e0..5333cbe 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -177,7 +177,7 @@
     protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected T mActivity;
-    protected Q mRecentsView;
+    protected @Nullable Q mRecentsView;
     protected Runnable mGestureEndCallback;
     protected MultiStateCallback mStateCallback;
     protected boolean mCanceled;
@@ -1895,7 +1895,9 @@
     private void invalidateHandlerWithLauncher() {
         endLauncherTransitionController();
 
-        mRecentsView.onGestureAnimationEnd();
+        if (mRecentsView != null) {
+            mRecentsView.onGestureAnimationEnd();
+        }
         resetLauncherListeners();
     }
 
@@ -1922,7 +1924,9 @@
     private void resetLauncherListeners() {
         mActivity.getRootView().setOnApplyWindowInsetsListener(null);
 
-        mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
+        if (mRecentsView != null) {
+            mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
+        }
     }
 
     private void resetStateForAnimationCancel() {
@@ -1981,8 +1985,10 @@
     private boolean updateThumbnail(int runningTaskId, boolean refreshView) {
         boolean finishTransitionPosted = false;
         final TaskView taskView;
-        if (mGestureState.getEndTarget() == HOME || mGestureState.getEndTarget() == NEW_TASK
-                || mGestureState.getEndTarget() == ALL_APPS) {
+        if (mGestureState.getEndTarget() == HOME
+                || mGestureState.getEndTarget() == NEW_TASK
+                || mGestureState.getEndTarget() == ALL_APPS
+                || mRecentsView == null) {
             // Capture the screenshot before finishing the transition to home or quickswitching to
             // ensure it's taken in the correct orientation, but no need to update the thumbnail.
             taskView = null;
@@ -2135,7 +2141,7 @@
     protected void startNewTask(Consumer<Boolean> resultCallback) {
         // Launch the task user scrolled to (mRecentsView.getNextPage()).
         if (!mCanceled) {
-            TaskView nextTask = mRecentsView.getNextPageTaskView();
+            TaskView nextTask = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
             if (nextTask != null) {
                 Task.TaskKey nextTaskKey = nextTask.getTask().key;
                 int taskId = nextTaskKey.id;
@@ -2237,7 +2243,8 @@
                     return;
                 }
                 RemoteAnimationTarget taskTarget = taskTargetOptional.get();
-                TaskView taskView = mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
+                TaskView taskView = mRecentsView == null
+                        ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
                 if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) {
                     finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
                     return;
@@ -2296,9 +2303,11 @@
      * resume if we finish the controller.
      */
     protected int getLastAppearedTaskIndex() {
-        return mGestureState.getLastAppearedTaskId() != -1
-                ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
-                : mRecentsView.getRunningTaskIndex();
+        return mRecentsView == null
+                ? -1
+                : mGestureState.getLastAppearedTaskId() != -1
+                        ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
+                        : mRecentsView.getRunningTaskIndex();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 4e892e2..ab3ae9f 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -149,6 +149,11 @@
             case TestProtocol.REQUEST_DISABLE_TRANSIENT_TASKBAR:
                 enableTransientTaskbar(false);
                 return response;
+
+            case TestProtocol.REQUEST_SHELL_DRAG_READY:
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        SystemUiProxy.INSTANCE.get(mContext).isDragAndDropReady());
+                return response;
         }
 
         return super.call(method, arg, extras);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 616ddef..89f06f6 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -70,6 +70,7 @@
 import com.android.systemui.unfold.progress.IUnfoldTransitionListener;
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.desktopmode.IDesktopMode;
+import com.android.wm.shell.draganddrop.IDragAndDrop;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
 import com.android.wm.shell.pip.IPipAnimationListener;
@@ -128,6 +129,7 @@
     private IBinder mOriginalTransactionToken = null;
     private IOnBackInvokedCallback mBackToLauncherCallback;
     private IRemoteAnimationRunner mBackToLauncherRunner;
+    private IDragAndDrop mDragAndDrop;
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -203,7 +205,7 @@
             IStartingWindow startingWindow, IRecentTasks recentTasks,
             ISysuiUnlockAnimationController sysuiUnlockAnimationController,
             IBackAnimation backAnimation, IDesktopMode desktopMode,
-            IUnfoldAnimation unfoldAnimation) {
+            IUnfoldAnimation unfoldAnimation, IDragAndDrop dragAndDrop) {
         unlinkToDeath();
         mSystemUiProxy = proxy;
         mPip = pip;
@@ -216,6 +218,7 @@
         mBackAnimation = backAnimation;
         mDesktopMode = desktopMode;
         mUnfoldAnimation = unfoldAnimation;
+        mDragAndDrop = dragAndDrop;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
         setPipAnimationListener(mPipAnimationListener);
@@ -230,7 +233,7 @@
     }
 
     public void clearProxy() {
-        setProxy(null, null, null, null, null, null, null, null, null, null, null);
+        setProxy(null, null, null, null, null, null, null, null, null, null, null, null);
     }
 
     // TODO(141886704): Find a way to remove this
@@ -1099,6 +1102,11 @@
             Log.e(TAG, "Failed call setUnfoldAnimationListener", e);
         }
     }
+
+    //
+    // Recents
+    //
+
     /**
      * Starts the recents activity. The caller should manage the thread on which this is called.
      */
@@ -1131,10 +1139,30 @@
         try {
             mRecentTasks.startRecentsTransition(mRecentsPendingIntent, intent, optsBundle,
                     mContext.getIApplicationThread(), runner);
+            return true;
         } catch (RemoteException e) {
             Log.e(TAG, "Error starting recents via shell", e);
             return false;
         }
-        return true;
+    }
+
+    //
+    // Drag and drop
+    //
+
+    /**
+     * For testing purposes.  Returns `true` only if the shell drop target has shown and
+     * drawn and is ready to handle drag events and the subsequent drop.
+     */
+    public boolean isDragAndDropReady() {
+        if (mDragAndDrop == null) {
+            return false;
+        }
+        try {
+            return mDragAndDrop.isReadyToHandleDrag();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error querying drag state", e);
+            return false;
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 038c674..6ea171e 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -43,6 +43,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
@@ -125,6 +126,7 @@
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.desktopmode.IDesktopMode;
+import com.android.wm.shell.draganddrop.IDragAndDrop;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
 import com.android.wm.shell.recents.IRecentTasks;
@@ -185,11 +187,13 @@
                     bundle.getBinder(KEY_EXTRA_SHELL_DESKTOP_MODE));
             IUnfoldAnimation unfoldTransition = IUnfoldAnimation.Stub.asInterface(
                     bundle.getBinder(KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER));
+            IDragAndDrop dragAndDrop = IDragAndDrop.Stub.asInterface(
+                    bundle.getBinder(KEY_EXTRA_SHELL_DRAG_AND_DROP));
             MAIN_EXECUTOR.execute(() -> {
                 SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
                         splitscreen, onehanded, shellTransitions, startingWindow,
                         recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode,
-                        unfoldTransition);
+                        unfoldTransition, dragAndDrop);
                 TouchInteractionService.this.initInputMonitor("TISBinder#onInitialize()");
                 preloadOverview(true /* fromInit */);
             });
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
new file mode 100644
index 0000000..ebea58c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
@@ -0,0 +1,364 @@
+/*
+ *  Copyright (C) 2023 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.annotation.IntDef
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
+import com.android.launcher3.logging.StatsLogManager.EventEnum
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
+import com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition
+import com.android.quickstep.util.SplitSelectDataHolder.Companion.SplitLaunchType
+import java.io.PrintWriter
+
+/**
+ * Holds/transforms/signs/seals/delivers information for the transient state of the user
+ * selecting a first app to start split with and then choosing a second app.
+ * This class DOES NOT associate itself with drag-and-drop split screen starts because they come
+ * from the bad part of town.
+ *
+ * After setting the correct fields for initial/second.* variables, this converts them into the
+ * correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary
+ * data back via [getSplitLaunchData].
+ * [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
+ * state
+ */
+class SplitSelectDataHolder(
+        val context: Context
+) {
+    val TAG = SplitSelectDataHolder::class.simpleName
+
+    /**
+     * Order of the constant indicates the order of which task/app was selected.
+     * Ex. SPLIT_TASK_SHORTCUT means primary split app identified by task, secondary is shortcut
+     * SPLIT_SHORTCUT_TASK means primary split app is determined by shortcut, secondary is task
+     */
+    companion object {
+        @IntDef(SPLIT_TASK_TASK, SPLIT_TASK_PENDINGINTENT, SPLIT_TASK_SHORTCUT,
+                SPLIT_PENDINGINTENT_TASK, SPLIT_PENDINGINTENT_PENDINGINTENT, SPLIT_SHORTCUT_TASK)
+        @Retention(AnnotationRetention.SOURCE)
+        annotation class SplitLaunchType
+
+        const val SPLIT_TASK_TASK = 0
+        const val SPLIT_TASK_PENDINGINTENT = 1
+        const val SPLIT_TASK_SHORTCUT = 2
+        const val SPLIT_PENDINGINTENT_TASK = 3
+        const val SPLIT_SHORTCUT_TASK = 4
+        const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5
+    }
+
+
+    @StagePosition
+    private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
+    private var initialTaskId: Int = INVALID_TASK_ID
+    private var secondTaskId: Int = INVALID_TASK_ID
+    private var initialUser: UserHandle? = null
+    private var secondUser: UserHandle? = null
+    private var initialIntent: Intent? = null
+    private var secondIntent: Intent? = null
+    private var secondPendingIntent: PendingIntent? = null
+    private var itemInfo: ItemInfo? = null
+    private var splitEvent: EventEnum? = null
+    private var initialShortcut: ShortcutInfo? = null
+    private var secondShortcut: ShortcutInfo? = null
+    private var initialPendingIntent: PendingIntent? = null
+
+    /**
+     * @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID]
+     * then @param intent will be used to launch the initial task
+     * @param intent will be ignored if @param alreadyRunningTask is set
+     */
+    fun setInitialTaskSelect(intent: Intent?, @StagePosition stagePosition: Int,
+                             itemInfo: ItemInfo?, splitEvent: EventEnum?,
+                             alreadyRunningTask: Int) {
+        if (alreadyRunningTask != INVALID_TASK_ID) {
+            initialTaskId = alreadyRunningTask
+        } else {
+            initialIntent = intent!!
+            initialUser = itemInfo!!.user
+        }
+        setInitialData(stagePosition, splitEvent, itemInfo)
+    }
+
+    /**
+     * To be called after first task selected from using a split shortcut from the fullscreen
+     * running app.
+     */
+    fun setInitialTaskSelect(info: RunningTaskInfo,
+                             @StagePosition stagePosition: Int, itemInfo: ItemInfo?,
+                             splitEvent: EventEnum?) {
+        initialTaskId = info.taskId
+        setInitialData(stagePosition, splitEvent, itemInfo)
+    }
+
+    private fun setInitialData(@StagePosition stagePosition: Int,
+                               event: EventEnum?, item: ItemInfo?) {
+        itemInfo = item
+        initialStagePosition = stagePosition
+        splitEvent = event
+    }
+
+    /**
+     * To be called as soon as user selects the second task (even if animations aren't complete)
+     * @param taskId The second task that will be launched.
+     */
+    fun setSecondTask(taskId: Int) {
+        secondTaskId = taskId
+    }
+
+    /**
+     * To be called as soon as user selects the second app (even if animations aren't complete)
+     * @param intent The second intent that will be launched.
+     * @param user The user of that intent.
+     */
+    fun setSecondTask(intent: Intent, user: UserHandle) {
+        secondIntent = intent
+        secondUser = user
+    }
+
+    /**
+     * To be called as soon as user selects the second app (even if animations aren't complete)
+     * Sets [secondUser] from that of the pendingIntent
+     * @param pendingIntent The second PendingIntent that will be launched.
+     */
+    fun setSecondTask(pendingIntent: PendingIntent) {
+        secondPendingIntent = pendingIntent
+        secondUser = pendingIntent.creatorUserHandle!!
+    }
+
+    private fun getShortcutInfo(intent: Intent?, user: UserHandle?): ShortcutInfo? {
+        if (intent?.getPackage() == null) {
+            return null
+        }
+        val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID)
+                ?: return null
+        try {
+            val context: Context = context.createPackageContextAsUser(
+                    intent.getPackage(), 0 /* flags */, user)
+            return ShortcutInfo.Builder(context, shortcutId).build()
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage())
+        }
+        return null
+    }
+
+    /**
+     * Converts intents to pendingIntents, associating the [user] with the intent if provided
+     */
+    private fun getPendingIntent(intent: Intent?, user: UserHandle?): PendingIntent? {
+        if (intent != initialIntent && intent != secondIntent) {
+            throw IllegalStateException("Invalid intent to convert to PendingIntent")
+        }
+
+        return if (intent == null) {
+            null
+        } else if (user != null) {
+            PendingIntent.getActivityAsUser(context, 0, intent,
+                    PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
+                    null /* options */, user)
+        } else {
+            PendingIntent.getActivity(context, 0, intent,
+                    PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT)
+        }
+    }
+
+    /**
+     * @return [SplitLaunchData] with the necessary fields populated as determined by
+     *   [SplitLaunchData.splitLaunchType]
+     */
+    fun getSplitLaunchData() : SplitLaunchData {
+        // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
+        convertIntentsToFinalTypes()
+        val splitLaunchType = getSplitLaunchType()
+        if (splitLaunchType == SPLIT_TASK_PENDINGINTENT || splitLaunchType == SPLIT_TASK_SHORTCUT) {
+            // need to get opposite stage position
+            initialStagePosition = getOppositeStagePosition(initialStagePosition)
+        }
+
+        return SplitLaunchData(
+                splitLaunchType,
+                initialTaskId,
+                secondTaskId,
+                initialPendingIntent,
+                secondPendingIntent,
+                initialShortcut,
+                secondShortcut,
+                itemInfo,
+                splitEvent,
+                initialStagePosition)
+    }
+
+    /**
+     * Converts our [initialIntent] and [secondIntent] into shortcuts and pendingIntents, if
+     * possible.
+     *
+     * Note that both [initialIntent] and [secondIntent] will be nullified on method return
+     *
+     * One caveat is that if [secondPendingIntent] is set, we will use that and *not* attempt to
+     * convert [secondIntent]
+     */
+    private fun convertIntentsToFinalTypes() {
+        initialShortcut = getShortcutInfo(initialIntent, initialUser)
+        initialPendingIntent = getPendingIntent(initialIntent, initialUser)
+        initialIntent = null
+
+        // Only one of the two is currently allowed (secondPendingIntent directly set for widgets)
+        if (secondIntent != null && secondPendingIntent != null) {
+            throw IllegalStateException("Both secondIntent and secondPendingIntent non-null")
+        }
+        // If secondPendingIntent already set, no need to convert. Prioritize using that
+        if (secondPendingIntent != null) {
+            secondIntent = null
+            return
+        }
+
+        secondShortcut = getShortcutInfo(secondIntent, secondUser)
+        secondPendingIntent = getPendingIntent(secondIntent, secondUser)
+        secondIntent = null
+    }
+
+    /**
+     * Only valid data fields at this point should be tasks, shortcuts, or pendingIntents
+     * Intents need to be converted in [convertIntentsToFinalTypes] prior to calling this method
+     */
+    @VisibleForTesting
+    @SplitLaunchType
+    fun getSplitLaunchType(): Int {
+        if (initialIntent != null || secondIntent != null) {
+            throw IllegalStateException("Intents need to be converted")
+        }
+
+        // Prioritize task launches first
+        if (initialTaskId != INVALID_TASK_ID) {
+            if (secondTaskId != INVALID_TASK_ID) {
+                return SPLIT_TASK_TASK
+            }
+            if (secondShortcut != null) {
+                return SPLIT_TASK_SHORTCUT
+            }
+            if (secondPendingIntent != null) {
+                return SPLIT_TASK_PENDINGINTENT
+            }
+        }
+
+        if (secondTaskId != INVALID_TASK_ID) {
+            if (initialShortcut != null) {
+                return SPLIT_SHORTCUT_TASK
+            }
+            if (initialPendingIntent != null) {
+                return SPLIT_PENDINGINTENT_TASK
+            }
+        }
+
+        // All task+shortcut combinations are handled above, only launch left is with multiple
+        // intents (and respective shortcut infos, if necessary)
+        if (initialPendingIntent != null && secondPendingIntent != null) {
+            return SPLIT_PENDINGINTENT_PENDINGINTENT
+        }
+        throw IllegalStateException("Unidentified split launch type")
+    }
+
+    data class SplitLaunchData(
+            @SplitLaunchType
+            val splitLaunchType: Int,
+            var initialTaskId: Int = INVALID_TASK_ID,
+            var secondTaskId: Int = INVALID_TASK_ID,
+            var initialPendingIntent: PendingIntent? = null,
+            var secondPendingIntent: PendingIntent? = null,
+            var initialShortcut: ShortcutInfo? = null,
+            var secondShortcut: ShortcutInfo? = null,
+            var itemInfo: ItemInfo? = null,
+            var splitEvent: EventEnum? = null,
+            val initialStagePosition: Int = STAGE_POSITION_UNDEFINED
+    )
+
+    /**
+     * @return `true` if first task has been selected and waiting for the second task to be
+     * chosen
+     */
+    fun isSplitSelectActive(): Boolean {
+        return isInitialTaskIntentSet() && !isSecondTaskIntentSet()
+    }
+
+    /**
+     * @return `true` if the first and second task have been chosen and split is waiting to
+     * be launched
+     */
+    fun isBothSplitAppsConfirmed(): Boolean {
+        return isInitialTaskIntentSet() && isSecondTaskIntentSet()
+    }
+
+    private fun isInitialTaskIntentSet(): Boolean {
+        return initialTaskId != INVALID_TASK_ID || initialIntent != null
+    }
+
+    fun getInitialTaskId(): Int {
+        return initialTaskId
+    }
+
+    fun getSecondTaskId(): Int {
+        return secondTaskId
+    }
+
+    private fun isSecondTaskIntentSet(): Boolean {
+        return secondTaskId != INVALID_TASK_ID || secondIntent != null
+                || secondPendingIntent != null
+    }
+
+    fun resetState() {
+        initialStagePosition = STAGE_POSITION_UNDEFINED
+        initialTaskId = INVALID_TASK_ID
+        secondTaskId = INVALID_TASK_ID
+        initialUser = null
+        secondUser = null
+        initialIntent = null
+        secondIntent = null
+        secondPendingIntent = null
+        itemInfo = null
+        splitEvent = null
+        initialShortcut = null
+        secondShortcut = null
+    }
+
+    fun dump(prefix: String, writer: PrintWriter) {
+        writer.println("$prefix ${javaClass.simpleName}")
+        writer.println("$prefix\tinitialStagePosition= $initialStagePosition")
+        writer.println("$prefix\tinitialTaskId= $initialTaskId")
+        writer.println("$prefix\tsecondTaskId= $secondTaskId")
+        writer.println("$prefix\tinitialUser= $initialUser")
+        writer.println("$prefix\tsecondUser= $secondUser")
+        writer.println("$prefix\tinitialIntent= $initialIntent")
+        writer.println("$prefix\tsecondIntent= $secondIntent")
+        writer.println("$prefix\tsecondPendingIntent= $secondPendingIntent")
+        writer.println("$prefix\titemInfo= $itemInfo")
+        writer.println("$prefix\tsplitEvent= $splitEvent")
+        writer.println("$prefix\tinitialShortcut= $initialShortcut")
+        writer.println("$prefix\tsecondShortcut= $secondShortcut")
+    }
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 8b21115..acc3ba1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -24,6 +24,12 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
 import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -34,6 +40,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -51,6 +58,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceId;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
@@ -71,6 +79,7 @@
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
 
+import java.io.PrintWriter;
 import java.util.function.Consumer;
 
 /**
@@ -85,6 +94,7 @@
     private final RecentsModel mRecentTasksModel;
     private final SplitAnimationController mSplitAnimationController;
     private final AppPairsController mAppPairsController;
+    private final SplitSelectDataHolder mSplitSelectDataHolder;
     private StatsLogManager mStatsLogManager;
     private final SystemUiProxy mSystemUiProxy;
     private final StateManager mStateManager;
@@ -137,6 +147,7 @@
         mRecentTasksModel = recentsModel;
         mSplitAnimationController = new SplitAnimationController(this);
         mAppPairsController = new AppPairsController(context, this);
+        mSplitSelectDataHolder = new SplitSelectDataHolder(mContext);
     }
 
     /**
@@ -155,6 +166,11 @@
         }
 
         setInitialData(stagePosition, splitEvent, itemInfo);
+
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
+                    alreadyRunningTask);
+        }
     }
 
     /**
@@ -166,6 +182,10 @@
             StatsLogManager.EventEnum splitEvent) {
         mInitialTaskId = info.taskId;
         setInitialData(stagePosition, splitEvent, itemInfo);
+
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
+        }
     }
 
     private void setInitialData(@StagePosition int stagePosition,
@@ -243,6 +263,10 @@
      */
     public void setSecondTask(Task task) {
         mSecondTaskId = task.key.id;
+
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.setSecondTask(task.key.id);
+        }
     }
 
     /**
@@ -253,6 +277,10 @@
     public void setSecondTask(Intent intent, UserHandle user) {
         mSecondTaskIntent = intent;
         mSecondUser = user;
+
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.setSecondTask(intent, user);
+        }
     }
 
     /**
@@ -263,6 +291,10 @@
     public void setSecondTask(PendingIntent pendingIntent) {
         mSecondPendingIntent = pendingIntent;
         mSecondUser = pendingIntent.getCreatorUserHandle();
+
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.setSecondTask(pendingIntent);
+        }
     }
 
     /**
@@ -285,6 +317,12 @@
      */
     public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition,
             Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.setInitialTaskSelect(null /*intent*/,
+                    stagePosition, null /*itemInfo*/, null /*splitEvent*/,
+                    taskId1);
+            mSplitSelectDataHolder.setSecondTask(taskId2);
+        }
         launchTasks(taskId1, null /* intent1 */, taskId2, null /* intent2 */, stagePosition,
                 callback, freezeTaskList, splitRatio, null);
     }
@@ -305,6 +343,11 @@
             @Nullable InstanceId shellInstanceId) {
         TestLogging.recordEvent(
                 TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            launchTasksRefactored(callback, freezeTaskList, splitRatio, shellInstanceId);
+            return;
+        }
+
         final ActivityOptions options1 = ActivityOptions.makeBasic();
         if (freezeTaskList) {
             options1.setFreezeRecentTasksReordering();
@@ -367,6 +410,101 @@
         }
     }
 
+    private void launchTasksRefactored(Consumer<Boolean> callback, boolean freezeTaskList,
+            float splitRatio, @Nullable InstanceId shellInstanceId) {
+        final ActivityOptions options1 = ActivityOptions.makeBasic();
+        if (freezeTaskList) {
+            options1.setFreezeRecentTasksReordering();
+        }
+
+        SplitSelectDataHolder.SplitLaunchData launchData =
+                mSplitSelectDataHolder.getSplitLaunchData();
+        int firstTaskId = launchData.getInitialTaskId();
+        int secondTaskId = launchData.getSecondTaskId();
+        ShortcutInfo firstShortcut = launchData.getInitialShortcut();
+        ShortcutInfo secondShortcut = launchData.getSecondShortcut();
+        PendingIntent firstPI = launchData.getInitialPendingIntent();
+        PendingIntent secondPI = launchData.getSecondPendingIntent();
+        int initialStagePosition = launchData.getInitialStagePosition();
+        Bundle optionsBundle = options1.toBundle();
+
+        if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
+            final RemoteSplitLaunchTransitionRunner animationRunner =
+                    new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
+            final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
+                    ActivityThread.currentActivityThread().getApplicationThread(),
+                    "LaunchSplitPair");
+            switch (launchData.getSplitLaunchType()) {
+                case SPLIT_TASK_TASK ->
+                        mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
+                                null /* options2 */, initialStagePosition, splitRatio,
+                                remoteTransition, shellInstanceId);
+
+                case SPLIT_TASK_PENDINGINTENT ->
+                        mSystemUiProxy.startIntentAndTask(secondPI, optionsBundle, firstTaskId,
+                                null /*options2*/, initialStagePosition, splitRatio,
+                                remoteTransition, shellInstanceId);
+
+                case SPLIT_TASK_SHORTCUT ->
+                        mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
+                                firstTaskId, null /*options2*/, initialStagePosition, splitRatio,
+                                remoteTransition, shellInstanceId);
+
+                case SPLIT_PENDINGINTENT_TASK ->
+                        mSystemUiProxy.startIntentAndTask(firstPI, optionsBundle, secondTaskId,
+                                null /*options2*/, initialStagePosition, splitRatio,
+                                remoteTransition, shellInstanceId);
+
+                case SPLIT_PENDINGINTENT_PENDINGINTENT ->
+                        mSystemUiProxy.startIntents(firstPI, firstShortcut, optionsBundle, secondPI,
+                                secondShortcut, null /*options2*/, initialStagePosition, splitRatio,
+                                remoteTransition, shellInstanceId);
+
+                case SPLIT_SHORTCUT_TASK ->
+                        mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
+                                secondTaskId, null /*options2*/, initialStagePosition, splitRatio,
+                                remoteTransition, shellInstanceId);
+            }
+        } else {
+            final RemoteSplitLaunchAnimationRunner animationRunner =
+                    new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
+            final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                    animationRunner, 300, 150,
+                    ActivityThread.currentActivityThread().getApplicationThread());
+            switch (launchData.getSplitLaunchType()) {
+                case SPLIT_TASK_TASK ->
+                        mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle,
+                                secondTaskId, null /* options2 */, initialStagePosition,
+                                splitRatio, adapter, shellInstanceId);
+
+                case SPLIT_TASK_PENDINGINTENT ->
+                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI,
+                                optionsBundle, firstTaskId, null /*options2*/, initialStagePosition,
+                                splitRatio, adapter, shellInstanceId);
+
+                case SPLIT_TASK_SHORTCUT ->
+                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut,
+                                optionsBundle, firstTaskId, null /*options2*/, initialStagePosition,
+                                splitRatio, adapter, shellInstanceId);
+
+                case SPLIT_PENDINGINTENT_TASK ->
+                        mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI,
+                                optionsBundle, secondTaskId, null /*options2*/,
+                                initialStagePosition, splitRatio, adapter, shellInstanceId);
+
+                case SPLIT_PENDINGINTENT_PENDINGINTENT ->
+                        mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstShortcut,
+                                optionsBundle, secondPI, secondShortcut, null /*options2*/,
+                                initialStagePosition, splitRatio, adapter, shellInstanceId);
+
+                case SPLIT_SHORTCUT_TASK ->
+                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut,
+                                optionsBundle, secondTaskId, null /*options2*/,
+                                initialStagePosition, splitRatio, adapter, shellInstanceId);
+            }
+        }
+    }
+
     private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1,
             int taskId, @StagePosition int stagePosition, float splitRatio,
             RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) {
@@ -572,6 +710,9 @@
      * To be called if split select was cancelled
      */
     public void resetState() {
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            mSplitSelectDataHolder.resetState();
+        }
         mInitialTaskId = INVALID_TASK_ID;
         mInitialTaskIntent = null;
         mSecondTaskId = INVALID_TASK_ID;
@@ -593,7 +734,11 @@
      *         chosen
      */
     public boolean isSplitSelectActive() {
-        return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            return mSplitSelectDataHolder.isSplitSelectActive();
+        } else {
+            return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
+        }
     }
 
     /**
@@ -601,7 +746,11 @@
      *          be launched
      */
     public boolean isBothSplitAppsConfirmed() {
-        return isInitialTaskIntentSet() && isSecondTaskIntentSet();
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            return mSplitSelectDataHolder.isBothSplitAppsConfirmed();
+        } else {
+            return isInitialTaskIntentSet() && isSecondTaskIntentSet();
+        }
     }
 
     private boolean isInitialTaskIntentSet() {
@@ -609,11 +758,19 @@
     }
 
     public int getInitialTaskId() {
-        return mInitialTaskId;
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            return mSplitSelectDataHolder.getInitialTaskId();
+        } else {
+            return mInitialTaskId;
+        }
     }
 
     public int getSecondTaskId() {
-        return mSecondTaskId;
+        if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) {
+            return mSplitSelectDataHolder.getSecondTaskId();
+        } else {
+            return mSecondTaskId;
+        }
     }
 
     private boolean isSecondTaskIntentSet() {
@@ -632,4 +789,10 @@
     public AppPairsController getAppPairsController() {
         return mAppPairsController;
     }
+
+    public void dump(String prefix, PrintWriter writer) {
+        if (mSplitSelectDataHolder != null) {
+            mSplitSelectDataHolder.dump(prefix, writer);
+        }
+    }
 }
diff --git a/res/drawable/ic_note_taking_widget_category.xml b/res/drawable/ic_note_taking_widget_category.xml
index 2b59157..96cc523 100644
--- a/res/drawable/ic_note_taking_widget_category.xml
+++ b/res/drawable/ic_note_taking_widget_category.xml
@@ -20,14 +20,9 @@
     android:viewportHeight="48">
 
     <path
-        android:fillColor="#F5F5F5"
-        android:pathData="M 0 0 H 48 V 48 H 0 V 0 Z" />
-    <group>
-        <path
-            android:fillColor="#0B57D0"
-            android:pathData="M48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.2548 10.7452 48 24 48C37.2548 48 48 37.2548 48 24Z" />
-        <path
-            android:fillColor="#ffffff"
-            android:pathData="M32.892 16.8L31.2 15.108C30.756 14.652 30.144 14.4 29.508 14.4C28.872 14.4 28.26 14.652 27.816 15.108L18.468 24.456L15.708 27.216L14.448 32.28C14.412 32.352 14.4 32.448 14.4 32.532C14.4 33.12 14.88 33.6 15.468 33.6C15.552 33.6 15.648 33.588 15.732 33.564L20.796 32.304L23.556 29.544L32.904 20.196C33.348 19.74 33.6 19.128 33.6 18.492C33.6 17.856 33.348 17.244 32.892 16.8ZM21.852 27.852L20.652 29.052L18.96 27.36L20.16 26.16L29.508 16.8L31.2 18.492L21.852 27.852Z" />
-    </group>
+        android:fillColor="#0B57D0"
+        android:pathData="M48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.2548 10.7452 48 24 48C37.2548 48 48 37.2548 48 24Z" />
+    <path
+        android:fillColor="#ffffff"
+        android:pathData="M32.892 16.8L31.2 15.108C30.756 14.652 30.144 14.4 29.508 14.4C28.872 14.4 28.26 14.652 27.816 15.108L18.468 24.456L15.708 27.216L14.448 32.28C14.412 32.352 14.4 32.448 14.4 32.532C14.4 33.12 14.88 33.6 15.468 33.6C15.552 33.6 15.648 33.588 15.732 33.564L20.796 32.304L23.556 29.544L32.904 20.196C33.348 19.74 33.6 19.128 33.6 18.492C33.6 17.856 33.348 17.244 32.892 16.8ZM21.852 27.852L20.652 29.052L18.96 27.36L20.16 26.16L29.508 16.8L31.2 18.492L21.852 27.852Z" />
 </vector>
\ No newline at end of file
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 6f3e948..45b03c2 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
 
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
@@ -76,8 +77,8 @@
 
         // Update theme
         if (Utilities.ATLEAST_P) {
-            getSystemService(WallpaperManager.class)
-                    .addOnColorsChangedListener(this, MAIN_EXECUTOR.getHandler());
+            THREAD_POOL_EXECUTOR.execute(() -> getSystemService(WallpaperManager.class)
+                    .addOnColorsChangedListener(this, MAIN_EXECUTOR.getHandler()));
         }
         int themeRes = Themes.getActivityThemeRes(this);
         if (themeRes != mThemeRes) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 2a3ad9a..331ae5d 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -407,7 +407,12 @@
             "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", DISABLED,
             "Use local overrides for search request timeout");
 
-    // TODO(Block 31): Empty block
+    // TODO(Block 31)
+    public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325,
+            "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", DISABLED,
+            "Use refactored split launching code path");
+
+    // TODO(Block 32): Empty block
 
     public static class BooleanFlag {
 
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index c9fe745..3c31b7a 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -733,10 +733,11 @@
         }
 
         mPageIndicator.stopAllAnimations();
-        startAnimation(anim);
+
         // Because t=0 has the folder match the folder icon, we can skip the
         // first frame and have the same movement one frame earlier.
         anim.setCurrentPlayTime(Math.min(getSingleFrameMs(getContext()), anim.getTotalDuration()));
+        startAnimation(anim);
 
         // Make sure the folder picks up the last drag move even if the finger doesn't move.
         if (mDragController.isDragging()) {
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 36255b4..b472cdb 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -111,6 +111,7 @@
     public static final String REQUEST_IS_TABLET = "is-tablet";
     public static final String REQUEST_IS_TWO_PANELS = "is-two-panel";
     public static final String REQUEST_START_DRAG_THRESHOLD = "start-drag-threshold";
+    public static final String REQUEST_SHELL_DRAG_READY = "shell-drag-ready";
     public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT =
             "get-activities-created-count";
     public static final String REQUEST_GET_ACTIVITIES = "get-activities";
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 4a3507e..58d5a36 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -16,11 +16,14 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL;
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING;
+import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_SHELL_DRAG_READY;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_HEIGHT;
 
 import android.graphics.Point;
@@ -146,6 +149,12 @@
 
             try (LauncherInstrumentation.Closable c2 = launcher.addContextLayer(
                     "started item drag")) {
+                launcher.assertTrue("Shell drag not marked as ready", launcher.waitAndGet(() -> {
+                    LauncherInstrumentation.log("Checking shell drag ready");
+                    return launcher.getTestInfo(REQUEST_SHELL_DRAG_READY)
+                            .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, false);
+                }, WAIT_TIME_MS, DEFAULT_POLL_INTERVAL));
+
                 launcher.movePointer(
                         dragStart,
                         endPoint,