Merge "Add null checks for mRecentsView" into udc-dev
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/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/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/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,