Add GroupedTaskView for gestures in staged split.

* Currently only works for portrait 50/50 split
* Have gesture swipe animation flow use multiple
TaskViewSimulators, one for each app in split
* Added new APIs in shell to query for actively running
tasks to determine if we're in split screen
* Lots of UI polish needed during gesture
* Launching into staged split after live tile ends
not implemented yet.

Bug: 181704764
Change-Id: Ib90e99e1e10b19121e8709385e1334b9380d6502
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
new file mode 100644
index 0000000..069ff86
--- /dev/null
+++ b/quickstep/res/layout/task_grouped.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2021 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.
+-->
+<!-- NOTE! don't add dimensions for margins / paddings / sizes that change per orientation to this
+     file, they need to be loaded at runtime. -->
+
+<!-- DOUBLE NOTE! Don't deviate IDs from task.xml since this layout acts as a "subclass" (read as
+     "bad code"). How can we use the view pool in RecentsView to use task.xml layout with using
+     GroupedTaskView.java class? Is that possible (while still keeping code in separate class) ? -->
+
+<com.android.quickstep.views.GroupedTaskView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:defaultFocusHighlightEnabled="false"
+    android:focusable="true">
+
+    <com.android.quickstep.views.TaskThumbnailView
+        android:id="@+id/snapshot"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <com.android.quickstep.views.TaskThumbnailView
+        android:id="@+id/bottomright_snapshot"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <com.android.quickstep.views.IconView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/task_thumbnail_icon_size"
+        android:layout_height="@dimen/task_thumbnail_icon_size"
+        android:focusable="false"
+        android:importantForAccessibility="no"/>
+</com.android.quickstep.views.GroupedTaskView>
\ No newline at end of file
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
index 9df9ab1..82e8903 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
@@ -63,7 +63,7 @@
 
         RunningTaskInfo placeholderTask = new RunningTaskInfo();
         placeholderTask.taskId = 22;
-        frv.showCurrentTask(placeholderTask);
+        frv.showCurrentTask(new RunningTaskInfo[]{placeholderTask});
         doLayout(activity);
 
         ThumbnailData thumbnailData = new ThumbnailData();
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 42c89fd..c05ffd5 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -99,6 +99,7 @@
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.InputProxyHandlerFactory;
+import com.android.quickstep.util.LauncherSplitScreenListener;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.RecentsOrientedState;
@@ -106,6 +107,7 @@
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.SwipePipToHomeAnimator;
+import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -222,7 +224,7 @@
     protected final TaskAnimationManager mTaskAnimationManager;
 
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
-    private RunningWindowAnim mRunningWindowAnim;
+    private RunningWindowAnim[] mRunningWindowAnim;
     // Possible second animation running at the same time as mRunningWindowAnim
     private Animator mParallelRunningAnim;
     private boolean mIsMotionPaused;
@@ -253,6 +255,10 @@
 
     private SwipePipToHomeAnimator mSwipePipToHomeAnimator;
     protected boolean mIsSwipingPipToHome;
+    // TODO(b/195473090) no split PIP for now, remove once we have more clarity
+    //  can try to have RectFSpringAnim evaluate multiple rects at once
+    private final SwipePipToHomeAnimator[] mSwipePipToHomeAnimators =
+            new SwipePipToHomeAnimator[2];
 
     // Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold
     private final float mQuickSwitchScaleScrollThreshold;
@@ -426,7 +432,8 @@
         // RecentsView never updates the display rotation until swipe-up, force update
         // RecentsOrientedState before passing to TaskViewSimulator.
         mRecentsView.updateRecentsRotation();
-        mTaskViewSimulator.setOrientationState(mRecentsView.getPagedViewOrientedState());
+        runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator
+                .setOrientationState(mRecentsView.getPagedViewOrientedState()));
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
@@ -519,7 +526,21 @@
     }
 
     protected void notifyGestureAnimationStartToRecents() {
-        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
+        ActivityManager.RunningTaskInfo[] runningTasks;
+        if (mIsSwipeForStagedSplit) {
+            int[] splitTaskIds =
+                    LauncherSplitScreenListener.INSTANCE.getNoCreate().getSplitTaskIds();
+            runningTasks = new ActivityManager.RunningTaskInfo[splitTaskIds.length];
+            for (int i = 0; i < splitTaskIds.length; i++) {
+                int taskId = splitTaskIds[i];
+                ActivityManager.RunningTaskInfo rti = new ActivityManager.RunningTaskInfo();
+                rti.taskId = taskId;
+                runningTasks[i] = rti;
+            }
+        } else {
+            runningTasks = new ActivityManager.RunningTaskInfo[]{mGestureState.getRunningTask()};
+        }
+        mRecentsView.onGestureAnimationStart(runningTasks);
     }
 
     private void launcherFrameDrawn() {
@@ -606,15 +627,15 @@
         if (animate) {
             ValueAnimator reapplyWindowTransformAnim = ValueAnimator.ofFloat(0, 1);
             reapplyWindowTransformAnim.addUpdateListener(anim -> {
-                if (mRunningWindowAnim == null) {
-                    applyWindowTransform();
+                if (mRunningWindowAnim == null || mRunningWindowAnim.length == 0) {
+                    applyScrollAndTransform();
                 }
             });
             reapplyWindowTransformAnim.setDuration(RECENTS_ATTACH_DURATION).start();
             mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED,
                     reapplyWindowTransformAnim::cancel);
         } else {
-            applyWindowTransform();
+            applyScrollAndTransform();
         }
     }
 
@@ -678,7 +699,7 @@
         }
 
         updateSysUiFlags(mCurrentShift.value);
-        applyWindowTransform();
+        applyScrollAndTransform();
 
         updateLauncherTransitionProgress();
     }
@@ -724,24 +745,23 @@
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
-        ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
+        super.onRecentsAnimationStart(controller, targets);
         mRecentsAnimationController = controller;
         mRecentsAnimationTargets = targets;
-        mTransformParams.setTargetSet(mRecentsAnimationTargets);
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
-                mGestureState.getRunningTaskId());
-
-        if (runningTaskTarget != null) {
-            mTaskViewSimulator.setPreview(runningTaskTarget);
-        }
 
         // Only initialize the device profile, if it has not been initialized before, as in some
         // configurations targets.homeContentInsets may not be correct.
         if (mActivity == null) {
-            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
-            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
+            RemoteAnimationTargetCompat primaryTaskTarget = targets.apps[0];
+            // orientation state is independent of which remote target handle we use since both
+            // should be pointing to the same one. Just choose index 0 for now since that works for
+            // both split and non-split
+            RecentsOrientedState orientationState = mRemoteTargetHandles[0].mTaskViewSimulator
+                    .getOrientationState();
+            DeviceProfile dp = orientationState.getLauncherDeviceProfile();
+            if (targets.minimizedHomeBounds != null && primaryTaskTarget != null) {
                 Rect overviewStackBounds = mActivityInterface
-                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
+                        .getOverviewWindowBounds(targets.minimizedHomeBounds, primaryTaskTarget);
                 dp = dp.getMultiWindowProfile(mContext,
                         new WindowBounds(overviewStackBounds, targets.homeContentInsets));
             } else {
@@ -751,7 +771,7 @@
             dp.updateInsets(targets.homeContentInsets);
             dp.updateIsSeascape(mContext);
             initTransitionEndpoints(dp);
-            mTaskViewSimulator.getOrientationState().setMultiWindowMode(dp.isMultiWindowMode);
+            orientationState.setMultiWindowMode(dp.isMultiWindowMode);
         }
 
         // Notify when the animation starts
@@ -869,9 +889,17 @@
     private void endRunningWindowAnim(boolean cancel) {
         if (mRunningWindowAnim != null) {
             if (cancel) {
-                mRunningWindowAnim.cancel();
+                for (RunningWindowAnim r : mRunningWindowAnim) {
+                    if (r != null) {
+                        r.cancel();
+                    }
+                }
             } else {
-                mRunningWindowAnim.end();
+                for (RunningWindowAnim r : mRunningWindowAnim) {
+                    if (r != null) {
+                        r.end();
+                    }
+                }
             }
         }
         if (mParallelRunningAnim != null) {
@@ -1181,15 +1209,17 @@
                     createHomeAnimationFactory(cookies, duration, isTranslucent, appCanEnterPip,
                             runningTaskTarget);
             mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome() && appCanEnterPip;
-            final RectFSpringAnim windowAnim;
+            final RectFSpringAnim[] windowAnim;
             if (mIsSwipingPipToHome) {
                 mSwipePipToHomeAnimator = createWindowAnimationToPip(
                         homeAnimFactory, runningTaskTarget, start);
-                windowAnim = mSwipePipToHomeAnimator;
+                mSwipePipToHomeAnimators[0] = mSwipePipToHomeAnimator;
+                windowAnim = mSwipePipToHomeAnimators;
             } else {
                 mSwipePipToHomeAnimator = null;
                 windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
-                windowAnim.addAnimatorListener(new AnimationSuccessListener() {
+
+                windowAnim[0].addAnimatorListener(new AnimationSuccessListener() {
                     @Override
                     public void onAnimationSuccess(Animator animator) {
                         if (mRecentsAnimationController == null) {
@@ -1203,15 +1233,22 @@
                     }
                 });
             }
-            windowAnim.start(mContext, velocityPxPerMs);
-            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+            mRunningWindowAnim = new RunningWindowAnim[windowAnim.length];
+            for (int i = 0, windowAnimLength = windowAnim.length; i < windowAnimLength; i++) {
+                RectFSpringAnim windowAnimation = windowAnim[i];
+                if (windowAnimation == null) {
+                    continue;
+                }
+                windowAnimation.start(mContext, velocityPxPerMs);
+                mRunningWindowAnim[i] = RunningWindowAnim.wrap(windowAnimation);
+            }
             homeAnimFactory.setSwipeVelocity(velocityPxPerMs.y);
             homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
             mLauncherTransitionController = null;
 
             if (mRecentsView != null) {
                 mRecentsView.onPrepareGestureEndAnimation(null, mGestureState.getEndTarget(),
-                        mTaskViewSimulator);
+                        getRemoteTaskViewSimulators());
             }
         } else {
             AnimatorSet animatorSet = new AnimatorSet();
@@ -1253,11 +1290,12 @@
             animatorSet.play(windowAnim);
             if (mRecentsView != null) {
                 mRecentsView.onPrepareGestureEndAnimation(
-                        animatorSet, mGestureState.getEndTarget(), mTaskViewSimulator);
+                        animatorSet, mGestureState.getEndTarget(),
+                        getRemoteTaskViewSimulators());
             }
             animatorSet.setDuration(duration).setInterpolator(interpolator);
             animatorSet.start();
-            mRunningWindowAnim = RunningWindowAnim.wrap(animatorSet);
+            mRunningWindowAnim = new RunningWindowAnim[]{RunningWindowAnim.wrap(animatorSet)};
         }
     }
 
@@ -1272,16 +1310,21 @@
         }
     }
 
+    /**
+     * TODO(b/195473090) handle multiple task simulators (if needed) for PIP
+     */
     private SwipePipToHomeAnimator createWindowAnimationToPip(HomeAnimationFactory homeAnimFactory,
             RemoteAnimationTargetCompat runningTaskTarget, float startProgress) {
         // Directly animate the app to PiP (picture-in-picture) mode
         final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask();
-        final RecentsOrientedState orientationState = mTaskViewSimulator.getOrientationState();
+        final RecentsOrientedState orientationState = mRemoteTargetHandles[0].mTaskViewSimulator
+                .getOrientationState();
         final int windowRotation = calculateWindowRotation(runningTaskTarget, orientationState);
         final int homeRotation = orientationState.getRecentsActivityRotation();
 
         final Matrix homeToWindowPositionMap = new Matrix();
-        final RectF startRect = updateProgressForStartRect(homeToWindowPositionMap, startProgress);
+        final RectF startRect = updateProgressForStartRect(homeToWindowPositionMap,
+                startProgress)[0];
         // Move the startRect to Launcher space as floatingIconView runs in Launcher
         final Matrix windowToHomePositionMap = new Matrix();
         homeToWindowPositionMap.invert(windowToHomePositionMap);
@@ -1310,7 +1353,7 @@
         // is not ROTATION_0 (which implies the rotation is turned on in launcher settings).
         if (homeRotation == ROTATION_0
                 && (windowRotation == ROTATION_90 || windowRotation == ROTATION_270)) {
-            builder.setFromRotation(mTaskViewSimulator, windowRotation,
+            builder.setFromRotation(mRemoteTargetHandles[0].mTaskViewSimulator, windowRotation,
                     taskInfo.displayCutoutInsets);
         }
         final SwipePipToHomeAnimator swipePipToHomeAnimator = builder.build();
@@ -1340,7 +1383,7 @@
                 mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
             }
         });
-        setupWindowAnimation(swipePipToHomeAnimator);
+        setupWindowAnimation(new RectFSpringAnim[]{swipePipToHomeAnimator});
         return swipePipToHomeAnimator;
     }
 
@@ -1367,19 +1410,19 @@
      * @param homeAnimationFactory The home animation factory.
      */
     @Override
-    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+    protected RectFSpringAnim[] createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
-        RectFSpringAnim anim =
+        RectFSpringAnim[] anim =
                 super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
         setupWindowAnimation(anim);
         return anim;
     }
 
-    private void setupWindowAnimation(RectFSpringAnim anim) {
-        anim.addOnUpdateListener((v, r, p) -> {
+    private void setupWindowAnimation(RectFSpringAnim[] anims) {
+        anims[0].addOnUpdateListener((v, r, p) -> {
             updateSysUiFlags(Math.max(p, mCurrentShift.value));
         });
-        anim.addAnimatorListener(new AnimationSuccessListener() {
+        anims[0].addAnimatorListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
                 if (mRecentsView != null) {
@@ -1391,7 +1434,7 @@
             }
         });
         if (mRecentsAnimationTargets != null) {
-            mRecentsAnimationTargets.addReleaseCheck(anim);
+            mRecentsAnimationTargets.addReleaseCheck(anims[0]);
         }
     }
 
@@ -1639,7 +1682,7 @@
      * if applicable. This should happen before {@link #finishRecentsControllerToHome(Runnable)}.
      */
     private void maybeFinishSwipePipToHome() {
-        if (mIsSwipingPipToHome && mSwipePipToHomeAnimator != null) {
+        if (mIsSwipingPipToHome && mSwipePipToHomeAnimators[0] != null) {
             SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
                     mSwipePipToHomeAnimator.getComponentName(),
                     mSwipePipToHomeAnimator.getDestinationBounds(),
@@ -1680,8 +1723,8 @@
      * depend on proper class initialization.
      */
     protected void initAfterSubclassConstructor() {
-        initTransitionEndpoints(
-                mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
+        initTransitionEndpoints(mRemoteTargetHandles[0].mTaskViewSimulator
+                        .getOrientationState().getLauncherDeviceProfile());
     }
 
     protected void performHapticFeedback() {
@@ -1698,7 +1741,8 @@
 
     protected void linkRecentsViewScroll() {
         SurfaceTransactionApplier.create(mRecentsView, applier -> {
-            mTransformParams.setSyncTransactionApplier(applier);
+            runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.mTransformParams
+                            .setSyncTransactionApplier(applier));
             runOnRecentsAnimationStart(() ->
                     mRecentsAnimationTargets.addReleaseCheck(applier));
         });
@@ -1824,19 +1868,25 @@
     /**
      * Applies the transform on the recents animation
      */
-    protected void applyWindowTransform() {
-        if (mWindowTransitionController != null) {
-            mWindowTransitionController.setProgress(
-                    Math.max(mCurrentShift.value, getScaleProgressDueToScroll()),
-                    mDragLengthFactor);
-        }
+    protected void applyScrollAndTransform() {
         // No need to apply any transform if there is ongoing swipe-pip-to-home animator since
         // that animator handles the leash solely.
-        if (mRecentsAnimationTargets != null && !mIsSwipingPipToHome) {
-            if (mRecentsViewScrollLinked && mRecentsView != null) {
-                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+        boolean notSwipingPipToHome = mRecentsAnimationTargets != null && !mIsSwipingPipToHome;
+        boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null;
+        for (RemoteTargetHandle remoteHandle : mRemoteTargetHandles) {
+            AnimatorControllerWithResistance playbackController = remoteHandle.mPlaybackController;
+            if (playbackController != null) {
+                playbackController.setProgress(Math.max(mCurrentShift.value,
+                        getScaleProgressDueToScroll()), mDragLengthFactor);
             }
-            mTaskViewSimulator.apply(mTransformParams);
+
+            if (notSwipingPipToHome) {
+                TaskViewSimulator taskViewSimulator = remoteHandle.mTaskViewSimulator;
+                if (setRecentsScroll) {
+                    taskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+                }
+                taskViewSimulator.apply(remoteHandle.mTransformParams);
+            }
         }
         ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
     }
@@ -1891,7 +1941,6 @@
     }
 
     public interface Factory {
-
         AbsSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 624ade2..76d39a0 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -201,10 +202,36 @@
     }
 
     /**
+     * Sets the task size in {@param outRect} taking split screened windows into account.
+     * We assume combined height of both tasks will be same as one normal task, then we'll modify
+     * the task height/width based on the ratio of task screen space bounds from
+     * {@param splitInfo}
+     *
+     * @param desiredStageBounds whether task size for top/left or bottom/right needs to be computed
+     */
+    public final void calculateStagedSplitTaskSize(Context context, DeviceProfile dp, Rect outRect,
+            SplitConfigurationOptions.StagedSplitBounds splitInfo,
+            @SplitConfigurationOptions.StagePosition int desiredStageBounds) {
+        calculateTaskSize(context, dp, outRect);
+
+        // TODO(b/181705607) Change for landscape vs portrait
+        float totalHeight = splitInfo.mLeftTopBounds.height()
+                + splitInfo.mRightBottomBounds.height()
+                + splitInfo.mDividerBounds.height() / 2f;
+        float topTaskPercent = splitInfo.mLeftTopBounds.height() / totalHeight;
+        if (desiredStageBounds == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) {
+            float diff = outRect.height() * (1f - topTaskPercent);
+            outRect.bottom -= diff;
+        } else {
+            float diff = outRect.height() * topTaskPercent;
+            outRect.top += diff;
+        }
+    }
+
+    /**
      * Calculates the taskView size for the provided device configuration.
      */
-    public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
-            PagedOrientationHandler orientedState) {
+    public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect) {
         Resources res = context.getResources();
         if (dp.overviewShowAsGrid) {
             Rect gridRect = new Rect();
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 7fb8e16..4df1aad 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -57,7 +57,7 @@
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
             PagedOrientationHandler orientationHandler) {
-        calculateTaskSize(context, dp, outRect, orientationHandler);
+        calculateTaskSize(context, dp, outRect);
         if (dp.isVerticalBarLayout()
                 && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
             return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index e2f198c..ec6032d 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -28,6 +28,7 @@
 
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -101,7 +102,9 @@
 
         mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
         if (mRunningOverHome) {
-            mTransformParams.setHomeBuilderProxy(this::updateHomeActivityTransformDuringSwipeUp);
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.mTransformParams.setHomeBuilderProxy(
+                    FallbackSwipeHandler.this::updateHomeActivityTransformDuringSwipeUp));
         }
     }
 
@@ -109,7 +112,8 @@
     protected void initTransitionEndpoints(DeviceProfile dp) {
         super.initTransitionEndpoints(dp);
         if (mRunningOverHome) {
-            mMaxLauncherScale = 1 / mTaskViewSimulator.getFullScreenScale();
+            // Full screen scale should be independent of remote target handle
+            mMaxLauncherScale = 1 / mRemoteTargetHandles[0].mTaskViewSimulator.getFullScreenScale();
         }
     }
 
@@ -174,7 +178,8 @@
     protected void notifyGestureAnimationStartToRecents() {
         if (mRunningOverHome) {
             if (SysUINavigationMode.getMode(mContext).hasGestures) {
-                mRecentsView.onGestureAnimationStartOnHome(mGestureState.getRunningTask());
+                mRecentsView.onGestureAnimationStartOnHome(
+                        new ActivityManager.RunningTaskInfo[]{mGestureState.getRunningTask()});
             }
         } else {
             super.notifyGestureAnimationStartToRecents();
@@ -202,19 +207,24 @@
                 mHomeAlpha = new AnimatedFloat();
                 mHomeAlpha.value = Utilities.boundToRange(1 - mCurrentShift.value, 0, 1);
                 mVerticalShiftForScale.value = mCurrentShift.value;
-                mTransformParams.setHomeBuilderProxy(
-                        this::updateHomeActivityTransformDuringHomeAnim);
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.mTransformParams.setHomeBuilderProxy(
+                                FallbackHomeAnimationFactory.this
+                                        ::updateHomeActivityTransformDuringHomeAnim));
             } else {
                 mHomeAlpha = new AnimatedFloat(this::updateHomeAlpha);
                 mHomeAlpha.value = 0;
-
-                mHomeAlphaParams.setHomeBuilderProxy(
-                        this::updateHomeActivityTransformDuringHomeAnim);
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.mTransformParams.setHomeBuilderProxy(
+                                FallbackHomeAnimationFactory.this
+                                        ::updateHomeActivityTransformDuringHomeAnim));
             }
 
             mRecentsAlpha.value = 1;
-            mTransformParams.setBaseBuilderProxy(
-                    this::updateRecentsActivityTransformDuringHomeAnim);
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.mTransformParams.setHomeBuilderProxy(
+                            FallbackHomeAnimationFactory.this
+                                    ::updateRecentsActivityTransformDuringHomeAnim));
         }
 
         @NonNull
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index d91d5b0..c2613c0 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -71,7 +71,7 @@
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
             PagedOrientationHandler orientationHandler) {
-        calculateTaskSize(context, dp, outRect, orientationHandler);
+        calculateTaskSize(context, dp, outRect);
         if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
             return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
         } else {
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 3239b00..ce3406c 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -110,7 +110,7 @@
             mActivity.setHintUserWillBeActive();
         }
 
-        if (!canUseWorkspaceView || appCanEnterPip) {
+        if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForStagedSplit) {
             return new LauncherHomeAnimationFactory();
         }
         if (workspaceView instanceof LauncherAppWidgetHostView) {
@@ -181,14 +181,16 @@
         final float floatingWidgetAlpha = isTargetTranslucent ? 0 : 1;
         RectF backgroundLocation = new RectF();
         Rect crop = new Rect();
-        mTaskViewSimulator.getCurrentCropRect().roundOut(crop);
+        // We can assume there is only one remote target here because staged split never animates
+        // into the app icon, only into the homescreen
+        mRemoteTargetHandles[0].mTaskViewSimulator.getCurrentCropRect().roundOut(crop);
         Size windowSize = new Size(crop.width(), crop.height());
         int fallbackBackgroundColor =
                 FloatingWidgetView.getDefaultBackgroundColor(mContext, runningTaskTarget);
         FloatingWidgetView floatingWidgetView = FloatingWidgetView.getFloatingWidgetView(mActivity,
                 hostView, backgroundLocation, windowSize,
-                mTaskViewSimulator.getCurrentCornerRadius(), isTargetTranslucent,
-                fallbackBackgroundColor);
+                mRemoteTargetHandles[0].mTaskViewSimulator.getCurrentCornerRadius(),
+                isTargetTranslucent, fallbackBackgroundColor);
 
         return new FloatingViewHomeAnimationFactory(floatingWidgetView) {
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 239233b..e948221 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.graphics.Rect;
 import android.util.ArraySet;
@@ -30,6 +31,7 @@
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
+import java.util.Arrays;
 import java.util.Set;
 
 /**
@@ -93,8 +95,16 @@
             RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets,
             Rect homeContentInsets, Rect minimizedHomeBounds) {
+        // Convert appTargets to type RemoteAnimationTarget for all apps except Home app
+        RemoteAnimationTarget[] nonHomeApps = Arrays.stream(appTargets)
+                .filter(remoteAnimationTarget ->
+                        remoteAnimationTarget.activityType != ACTIVITY_TYPE_HOME)
+                .map(RemoteAnimationTargetCompat::unwrap)
+                .toArray(RemoteAnimationTarget[]::new);
+
         RemoteAnimationTarget[] nonAppTargets =
-                mSystemUiProxy.onGoingToRecentsLegacy(mCancelled);
+                mSystemUiProxy.onGoingToRecentsLegacy(mCancelled, nonHomeApps);
+
         RecentsAnimationTargets targets = new RecentsAnimationTargets(appTargets,
                 wallpaperTargets, RemoteAnimationTargetCompat.wrap(nonAppTargets),
                 homeContentInsets, minimizedHomeBounds);
diff --git a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
index c032889..b20d488 100644
--- a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
@@ -77,8 +77,12 @@
      * Gets the navigation bar remote animation target if exists.
      */
     public RemoteAnimationTargetCompat getNavBarRemoteAnimationTarget() {
+        return getNonAppTargetOfType(TYPE_NAVIGATION_BAR);
+    }
+
+    public RemoteAnimationTargetCompat getNonAppTargetOfType(int type) {
         for (RemoteAnimationTargetCompat target : nonApps) {
-            if (target.windowType == TYPE_NAVIGATION_BAR) {
+            if (target.windowType == type) {
                 return target;
             }
         }
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index 4495455..ec51599 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -15,9 +15,13 @@
  */
 package com.android.quickstep;
 
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_SELECT;
 import static com.android.launcher3.config.FeatureFlags.PROTOTYPE_APP_CLOSE;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -36,8 +40,11 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.AppCloseConfig;
+import com.android.quickstep.util.LauncherSplitScreenListener;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.RectFSpringAnim2;
 import com.android.quickstep.util.TaskViewSimulator;
@@ -46,7 +53,11 @@
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
 
-public abstract class SwipeUpAnimationLogic {
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+public abstract class SwipeUpAnimationLogic implements
+        RecentsAnimationCallbacks.RecentsAnimationListener{
 
     protected static final Rect TEMP_RECT = new Rect();
 
@@ -55,9 +66,9 @@
     protected final Context mContext;
     protected final RecentsAnimationDeviceState mDeviceState;
     protected final GestureState mGestureState;
-    protected final TaskViewSimulator mTaskViewSimulator;
 
-    protected final TransformParams mTransformParams;
+    protected final RemoteTargetHandle[] mRemoteTargetHandles;
+    protected SplitConfigurationOptions.StagedSplitBounds mStagedSplitBounds;
 
     // Shift in the range of [0, 1].
     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
@@ -70,37 +81,56 @@
     // How much further we can drag past recents, as a factor of mTransitionDragLength.
     protected float mDragLengthFactor = 1;
 
-    protected AnimatorControllerWithResistance mWindowTransitionController;
+    protected boolean mIsSwipeForStagedSplit;
 
     public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, TransformParams transformParams) {
         mContext = context;
         mDeviceState = deviceState;
         mGestureState = gestureState;
-        mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
-        mTransformParams = transformParams;
 
-        mTaskViewSimulator.getOrientationState().update(
+        mIsSwipeForStagedSplit = ENABLE_SPLIT_SELECT.get() &&
+                LauncherSplitScreenListener.INSTANCE.getNoCreate().getSplitTaskIds().length > 1;
+
+        TaskViewSimulator primaryTVS = new TaskViewSimulator(context,
+                gestureState.getActivityInterface());
+        primaryTVS.getOrientationState().update(
                 mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
                 mDeviceState.getRotationTouchHelper().getDisplayRotation());
+        mRemoteTargetHandles = new RemoteTargetHandle[mIsSwipeForStagedSplit ? 2 : 1];
+        mRemoteTargetHandles[0] = new RemoteTargetHandle(primaryTVS, transformParams);
+
+        if (mIsSwipeForStagedSplit) {
+            TaskViewSimulator secondaryTVS = new TaskViewSimulator(context,
+                    gestureState.getActivityInterface());
+            secondaryTVS.getOrientationState().update(
+                    mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
+                    mDeviceState.getRotationTouchHelper().getDisplayRotation());
+            mRemoteTargetHandles[1] = new RemoteTargetHandle(secondaryTVS, new TransformParams());
+        }
     }
 
     protected void initTransitionEndpoints(DeviceProfile dp) {
         mDp = dp;
-
-        mTaskViewSimulator.setDp(dp);
         mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength(
-                dp, mContext, TEMP_RECT,
-                mTaskViewSimulator.getOrientationState().getOrientationHandler());
+                dp, mContext, TEMP_RECT, mRemoteTargetHandles[0].mTaskViewSimulator
+                        .getOrientationState().getOrientationHandler());
         mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
 
-        PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
-        mTaskViewSimulator.addAppToOverviewAnim(pa, LINEAR);
-        AnimatorPlaybackController normalController = pa.createPlaybackController();
-        mWindowTransitionController = AnimatorControllerWithResistance.createForRecents(
-                normalController, mContext, mTaskViewSimulator.getOrientationState(),
-                mDp, mTaskViewSimulator.recentsViewScale, AnimatedFloat.VALUE,
-                mTaskViewSimulator.recentsViewSecondaryTranslation, AnimatedFloat.VALUE);
+        for (RemoteTargetHandle remoteHandle : mRemoteTargetHandles) {
+            PendingAnimation pendingAnimation = new PendingAnimation(mTransitionDragLength * 2);
+            TaskViewSimulator taskViewSimulator = remoteHandle.mTaskViewSimulator;
+            taskViewSimulator.setDp(dp);
+            taskViewSimulator.addAppToOverviewAnim(pendingAnimation, LINEAR);
+            AnimatorPlaybackController playbackController =
+                    pendingAnimation.createPlaybackController();
+
+            remoteHandle.mPlaybackController = AnimatorControllerWithResistance.createForRecents(
+                    playbackController, mContext, taskViewSimulator.getOrientationState(),
+                    mDp, taskViewSimulator.recentsViewScale, AnimatedFloat.VALUE,
+                    taskViewSimulator.recentsViewSecondaryTranslation, AnimatedFloat.VALUE
+            );
+        }
     }
 
     @UiThread
@@ -125,7 +155,9 @@
     public abstract void updateFinalShift();
 
     protected PagedOrientationHandler getOrientationHandler() {
-        return mTaskViewSimulator.getOrientationState().getOrientationHandler();
+        // OrientationHandler should be independent of remote target, can directly take one
+        return mRemoteTargetHandles[0].mTaskViewSimulator
+                .getOrientationState().getOrientationHandler();
     }
 
     protected abstract class HomeAnimationFactory {
@@ -207,31 +239,102 @@
      * @param startProgress The progress of {@link #mCurrentShift} to start thw window from.
      * @return {@link RectF} represents the bounds as starting point in window space.
      */
-    protected RectF updateProgressForStartRect(Matrix outMatrix, float startProgress) {
+    protected RectF[] updateProgressForStartRect(Matrix outMatrix, float startProgress) {
         mCurrentShift.updateValue(startProgress);
-        mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
-        RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
+        RectF[] startRects = new RectF[mRemoteTargetHandles.length];
+        for (int i = 0, mRemoteTargetHandlesLength = mRemoteTargetHandles.length;
+                i < mRemoteTargetHandlesLength; i++) {
+            RemoteTargetHandle remoteHandle = mRemoteTargetHandles[i];
+            TaskViewSimulator tvs = remoteHandle.mTaskViewSimulator;
+            tvs.apply(remoteHandle.mTransformParams.setProgress(startProgress));
 
-        mTaskViewSimulator.applyWindowToHomeRotation(outMatrix);
-
-        final RectF startRect = new RectF(cropRectF);
-        mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
-        return startRect;
+            startRects[i] = new RectF(tvs.getCurrentCropRect());
+            tvs.applyWindowToHomeRotation(outMatrix);
+            tvs.getCurrentMatrix().mapRect(startRects[i]);
+        }
+        return startRects;
     }
 
+    /** Helper to avoid writing some for-loops to iterate over {@link #mRemoteTargetHandles} */
+    protected void runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer) {
+        for (RemoteTargetHandle handle : mRemoteTargetHandles) {
+            consumer.accept(handle);
+        }
+    }
+
+    /** @return only the TaskViewSimulators from {@link #mRemoteTargetHandles} */
+    protected TaskViewSimulator[] getRemoteTaskViewSimulators() {
+        return Arrays.stream(mRemoteTargetHandles)
+                .map(remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator)
+                .toArray(TaskViewSimulator[]::new);
+    }
+
+    @Override
+    public void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
+        RemoteAnimationTargetCompat dividerTarget = targets.getNonAppTargetOfType(
+                TYPE_DOCK_DIVIDER);
+        RemoteAnimationTargetCompat primaryTaskTarget;
+        RemoteAnimationTargetCompat secondaryTaskTarget;
+
+        if (!mIsSwipeForStagedSplit) {
+            primaryTaskTarget = targets.findTask(mGestureState.getRunningTaskId());
+            mRemoteTargetHandles[0].mTransformParams.setTargetSet(targets);
+
+            if (primaryTaskTarget != null) {
+                mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(primaryTaskTarget);
+            }
+        } else {
+            // We're in staged split
+            primaryTaskTarget = targets.apps[0];
+            secondaryTaskTarget = targets.apps[1];
+            mStagedSplitBounds = new SplitConfigurationOptions.StagedSplitBounds(
+                    primaryTaskTarget.screenSpaceBounds,
+                    secondaryTaskTarget.screenSpaceBounds, dividerTarget.screenSpaceBounds);
+            mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(primaryTaskTarget,
+                    mStagedSplitBounds);
+            mRemoteTargetHandles[1].mTaskViewSimulator.setPreview(secondaryTaskTarget,
+                    mStagedSplitBounds);
+            mRemoteTargetHandles[0].mTransformParams.setTargetSet(
+                    createRemoteAnimationTargetsForTarget(primaryTaskTarget));
+            mRemoteTargetHandles[1].mTransformParams.setTargetSet(
+                    createRemoteAnimationTargetsForTarget(secondaryTaskTarget));
+        }
+    }
+
+    private RemoteAnimationTargets createRemoteAnimationTargetsForTarget(
+            RemoteAnimationTargetCompat target) {
+        return new RemoteAnimationTargets(new RemoteAnimationTargetCompat[]{target},
+                null, null, MODE_CLOSING);
+    }
     /**
      * Creates an animation that transforms the current app window into the home app.
      * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
      * @param homeAnimationFactory The home animation factory.
      */
-    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+    protected RectFSpringAnim[] createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
+        // TODO(b/195473584) compute separate end targets for different staged split
         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-
+        RectFSpringAnim[] out = new RectFSpringAnim[mRemoteTargetHandles.length];
         Matrix homeToWindowPositionMap = new Matrix();
-        final RectF startRect = updateProgressForStartRect(homeToWindowPositionMap, startProgress);
-        RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
+        RectF[] startRects = updateProgressForStartRect(homeToWindowPositionMap, startProgress);
+        for (int i = 0, mRemoteTargetHandlesLength = mRemoteTargetHandles.length;
+                i < mRemoteTargetHandlesLength; i++) {
+            RemoteTargetHandle remoteHandle = mRemoteTargetHandles[i];
+            out[i] = getWindowAnimationToHomeInternal(homeAnimationFactory,
+                    targetRect, remoteHandle.mTransformParams, remoteHandle.mTaskViewSimulator,
+                    startRects[i], homeToWindowPositionMap);
+        }
+        return out;
+    }
 
+    private RectFSpringAnim getWindowAnimationToHomeInternal(
+            HomeAnimationFactory homeAnimationFactory, RectF targetRect,
+            TransformParams transformParams, TaskViewSimulator taskViewSimulator,
+            RectF startRect, Matrix homeToWindowPositionMap) {
+        RectF cropRectF = new RectF(taskViewSimulator.getCurrentCropRect());
         // Move the startRect to Launcher space as floatingIconView runs in Launcher
         Matrix windowToHomePositionMap = new Matrix();
         homeToWindowPositionMap.invert(windowToHomePositionMap);
@@ -240,7 +343,7 @@
         RectFSpringAnim anim;
         if (PROTOTYPE_APP_CLOSE.get()) {
             anim = new RectFSpringAnim2(startRect, targetRect, mContext,
-                    mTaskViewSimulator.getCurrentCornerRadius(),
+                    taskViewSimulator.getCurrentCornerRadius(),
                     homeAnimationFactory.getEndRadius(cropRectF));
         } else {
             anim = new RectFSpringAnim(startRect, targetRect, mContext);
@@ -248,9 +351,10 @@
         homeAnimationFactory.setAnimation(anim);
 
         SpringAnimationRunner runner = new SpringAnimationRunner(
-                homeAnimationFactory, cropRectF, homeToWindowPositionMap);
-        anim.addOnUpdateListener(runner);
+                homeAnimationFactory, cropRectF, homeToWindowPositionMap,
+                transformParams, taskViewSimulator);
         anim.addAnimatorListener(runner);
+        anim.addOnUpdateListener(runner);
         return anim;
     }
 
@@ -262,6 +366,7 @@
 
         final RectF mWindowCurrentRect = new RectF();
         final Matrix mHomeToWindowPositionMap;
+        private final TransformParams mLocalTransformParams;
         final HomeAnimationFactory mAnimationFactory;
 
         final AnimatorPlaybackController mHomeAnim;
@@ -271,17 +376,19 @@
         final float mEndRadius;
 
         SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
-                Matrix homeToWindowPositionMap) {
+                Matrix homeToWindowPositionMap, TransformParams transformParams,
+                TaskViewSimulator taskViewSimulator) {
             mAnimationFactory = factory;
             mHomeAnim = factory.createActivityAnimationToHome();
             mCropRectF = cropRectF;
             mHomeToWindowPositionMap = homeToWindowPositionMap;
+            mLocalTransformParams = transformParams;
 
             cropRectF.roundOut(mCropRect);
 
             // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
             // rounding at the end of the animation.
-            mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
+            mStartRadius = taskViewSimulator.getCurrentCornerRadius();
             mEndRadius = factory.getEndRadius(cropRectF);
         }
 
@@ -300,10 +407,11 @@
             if (mAnimationFactory.keepWindowOpaque()) {
                 alpha = 1f;
             }
-            mTransformParams
+            mLocalTransformParams
                     .setTargetAlpha(alpha)
                     .setCornerRadius(cornerRadius);
-            mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
+            mLocalTransformParams.applySurfaceParams(mLocalTransformParams
+                    .createSurfaceParams(this));
             mAnimationFactory.update(config, currentRect, progress,
                     mMatrix.mapRadius(cornerRadius));
         }
@@ -332,6 +440,21 @@
         }
     }
 
+    /**
+     * Container to keep together all the associated objects whose properties need to be updated to
+     * animate a single remote app target
+     */
+    public static class RemoteTargetHandle {
+        public TaskViewSimulator mTaskViewSimulator;
+        public TransformParams mTransformParams;
+        public AnimatorControllerWithResistance mPlaybackController;
+        public RemoteTargetHandle(TaskViewSimulator taskViewSimulator,
+                TransformParams transformParams) {
+            mTransformParams = transformParams;
+            mTaskViewSimulator = taskViewSimulator;
+        }
+    }
+
     public interface RunningWindowAnim {
         void end();
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 11ca4b1..7d2d413 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.app.ActivityManager;
 import android.app.PendingIntent;
 import android.app.PictureInPictureParams;
 import android.content.ComponentName;
@@ -632,10 +633,11 @@
      * @param cancel true if recents starting is being cancelled.
      * @return RemoteAnimationTargets of windows that need to animate but only exist in shell.
      */
-    public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel) {
+    public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+            RemoteAnimationTarget[] apps) {
         if (mSplitScreen != null) {
             try {
-                return mSplitScreen.onGoingToRecentsLegacy(cancel);
+                return mSplitScreen.onGoingToRecentsLegacy(cancel, apps);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call onGoingToRecentsLegacy");
             }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index b5da097..5b9e214 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -154,9 +154,10 @@
         boolean isRunningTask = v.isRunningTask();
         TransformParams params = null;
         TaskViewSimulator tsv = null;
+        // TODO(b/195675206) handle two TSVs here
         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
-            params = v.getRecentsView().getLiveTileParams();
-            tsv = v.getRecentsView().getLiveTileTaskViewSimulator();
+            params = v.getRecentsView().getRemoteTargetHandles()[0].mTransformParams;
+            tsv = v.getRecentsView().getRemoteTargetHandles()[0].mTaskViewSimulator;
         }
         createRecentsWindowAnimator(v, skipViewChanges, appTargets, wallpaperTargets, nonAppTargets,
                 depthController, out, params, tsv);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 4979206..20eff34 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -99,6 +99,7 @@
 import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.LauncherSplitScreenListener;
 import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.ProxyScreenStatusProvider;
 import com.android.quickstep.util.SplitScreenBounds;
@@ -364,6 +365,7 @@
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
         mDeviceState.runOnUserUnlocked(mTaskbarManager::onUserUnlocked);
         ProtoTracer.INSTANCE.get(this).add(this);
+        LauncherSplitScreenListener.INSTANCE.get(this).init();
         sConnected = true;
     }
 
@@ -520,6 +522,7 @@
         getSystemService(AccessibilityManager.class)
                 .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
 
+        LauncherSplitScreenListener.INSTANCE.get(this).destroy();
         mTaskbarManager.destroy();
         sConnected = false;
         super.onDestroy();
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 64a428f..765480c 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -79,8 +79,10 @@
      * to the home task. This allows us to handle quick-switch similarly to a quick-switching
      * from a foreground task.
      */
-    public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
-        mHomeTaskInfo = homeTaskInfo;
+    public void onGestureAnimationStartOnHome(RunningTaskInfo[] homeTaskInfo) {
+        // TODO(b/195607777) General fallback love, but this might be correct
+        //  Home task should be defined as the front-most task info I think?
+        mHomeTaskInfo = homeTaskInfo[0];
         onGestureAnimationStart(homeTaskInfo);
     }
 
@@ -92,8 +94,8 @@
     @Override
     public void onPrepareGestureEndAnimation(
             @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
-            TaskViewSimulator taskViewSimulator) {
-        super.onPrepareGestureEndAnimation(animatorSet, endTarget, taskViewSimulator);
+            TaskViewSimulator[] taskViewSimulators) {
+        super.onPrepareGestureEndAnimation(animatorSet, endTarget, taskViewSimulators);
         if (mHomeTaskInfo != null && endTarget == RECENTS && animatorSet != null) {
             TaskView tv = getTaskViewByTaskId(mHomeTaskInfo.taskId);
             if (tv != null) {
@@ -133,7 +135,13 @@
     }
 
     @Override
-    protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
+    protected boolean shouldAddStubTaskView(RunningTaskInfo[] runningTaskInfos) {
+        if (runningTaskInfos.length > 1) {
+            // can't be in split screen w/ home task
+            return super.shouldAddStubTaskView(runningTaskInfos);
+        }
+
+        RunningTaskInfo runningTaskInfo = runningTaskInfos[0];
         if (mHomeTaskInfo != null && runningTaskInfo != null &&
                 mHomeTaskInfo.taskId == runningTaskInfo.taskId
                 && getTaskViewCount() == 0) {
@@ -141,7 +149,7 @@
             // show the empty recents message instead of showing a stub task and later removing it.
             return false;
         }
-        return super.shouldAddStubTaskView(runningTaskInfo);
+        return super.shouldAddStubTaskView(runningTaskInfos);
     }
 
     @Override
@@ -149,6 +157,7 @@
         // When quick-switching on 3p-launcher, we add a "stub" tile corresponding to Launcher
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
         // track the index of the next task appropriately, as if we are switching on any other app.
+        // TODO(b/195607777) Confirm home task info is front-most task and not mixed in with others
         int runningTaskId = getTaskIdsForRunningTaskView()[0];
         if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId && !tasks.isEmpty()) {
             // Check if the task list has running task
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index b2183d6..04b147c 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.OverviewComponentObserver;
@@ -263,14 +264,16 @@
 
         void initDp(DeviceProfile dp) {
             initTransitionEndpoints(dp);
-            mTaskViewSimulator.setPreviewBounds(
+            mRemoteTargetHandles[0].mTaskViewSimulator.setPreviewBounds(
                     new Rect(0, 0, dp.widthPx, dp.heightPx), dp.getInsets());
         }
 
         @Override
         public void updateFinalShift() {
-            mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
-            mTaskViewSimulator.apply(mTransformParams);
+            mRemoteTargetHandles[0].mPlaybackController
+                    .setProgress(mCurrentShift.value, mDragLengthFactor);
+            mRemoteTargetHandles[0].mTaskViewSimulator.apply(
+                    mRemoteTargetHandles[0].mTransformParams);
         }
 
         AnimatedFloat getCurrentShift() {
@@ -326,7 +329,8 @@
                     mFakeIconView.setVisibility(View.INVISIBLE);
                 }
             };
-            RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, homeAnimFactory);
+            RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift,
+                    homeAnimFactory)[0];
             windowAnim.start(mContext, velocityPxPerMs);
             return windowAnim;
         }
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index baca76c..7c83833 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -158,8 +158,7 @@
         Rect startRect = new Rect();
         PagedOrientationHandler orientationHandler = params.recentsOrientedState
                 .getOrientationHandler();
-        LauncherActivityInterface.INSTANCE.calculateTaskSize(params.context, params.dp, startRect,
-                orientationHandler);
+        LauncherActivityInterface.INSTANCE.calculateTaskSize(params.context, params.dp, startRect);
         long distanceToCover = startRect.bottom;
         PendingAnimation resistAnim = params.resistAnim != null
                 ? params.resistAnim
diff --git a/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java b/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java
new file mode 100644
index 0000000..da665d4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/LauncherSplitScreenListener.java
@@ -0,0 +1,104 @@
+package com.android.quickstep.util;
+
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+
+import android.content.Context;
+import android.os.IBinder;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.launcher3.util.SplitConfigurationOptions.StageType;
+import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitTaskPosition;
+import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.splitscreen.ISplitScreenListener;
+
+/**
+ * Listeners for system wide split screen position and stage changes.
+ * Use {@link #getSplitTaskIds()} to determine which tasks, if any, are in staged split.
+ */
+public class LauncherSplitScreenListener extends ISplitScreenListener.Stub {
+
+    public static final MainThreadInitializedObject<LauncherSplitScreenListener> INSTANCE =
+            new MainThreadInitializedObject<>(LauncherSplitScreenListener::new);
+
+    private final StagedSplitTaskPosition mMainStagePosition = new StagedSplitTaskPosition();
+    private final StagedSplitTaskPosition mSideStagePosition = new StagedSplitTaskPosition();
+
+    public LauncherSplitScreenListener(Context context) {
+        mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN;
+        mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
+    }
+
+    /** Also call {@link #destroy()} when done. */
+    public void init() {
+        SystemUiProxy.INSTANCE.getNoCreate().registerSplitScreenListener(this);
+    }
+
+    public void destroy() {
+        SystemUiProxy.INSTANCE.getNoCreate().unregisterSplitScreenListener(this);
+    }
+
+    /**
+     * @return index 0 will be task in left/top position, index 1 in right/bottom position.
+     *         Will return empty array if device is not in staged split
+     */
+    public int[] getSplitTaskIds() {
+        if (mMainStagePosition.taskId == -1 || mSideStagePosition.taskId == -1) {
+            return new int[]{};
+        }
+        int[] out = new int[2];
+        if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+            out[0] = mMainStagePosition.taskId;
+            out[1] = mSideStagePosition.taskId;
+        } else {
+            out[1] = mMainStagePosition.taskId;
+            out[0] = mSideStagePosition.taskId;
+        }
+        return out;
+    }
+
+    @Override
+    public void onStagePositionChanged(@StageType int stage, @StagePosition int position) {
+        if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
+            mMainStagePosition.stagePosition = position;
+        } else {
+            mSideStagePosition.stagePosition = position;
+        }
+    }
+
+    @Override
+    public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
+        // If task is not visible but we are tracking it, stop tracking it
+        if (!visible) {
+            if (mMainStagePosition.taskId == taskId) {
+                resetTaskId(mMainStagePosition);
+            } else if (mSideStagePosition.taskId == taskId) {
+                resetTaskId(mSideStagePosition);
+            } // else it's an un-tracked child
+            return;
+        }
+
+        // If stage has moved to undefined, stop tracking the task
+        if (stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) {
+            resetTaskId(taskId == mMainStagePosition.taskId ?
+                    mMainStagePosition : mSideStagePosition);
+            return;
+        }
+
+        if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
+            mMainStagePosition.taskId = taskId;
+        } else {
+            mSideStagePosition.taskId = taskId;
+        }
+    }
+
+    private void resetTaskId(StagedSplitTaskPosition taskPosition) {
+        taskPosition.taskId = -1;
+    }
+
+    @Override
+    public IBinder asBinder() {
+        return this;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 8834dc2..302526d 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -42,8 +42,7 @@
             PagedOrientationHandler orientationHandler) {
         // Track the bottom of the window.
         Rect taskSize = new Rect();
-        LauncherActivityInterface.INSTANCE.calculateTaskSize(
-                context, dp, taskSize, orientationHandler);
+        LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize);
         return orientationHandler.getDistanceToBottomOfRect(dp, taskSize);
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 7eee415..7b1c62e 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.launcher3.util.SplitConfigurationOptions.*;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
 import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
@@ -29,6 +30,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 
@@ -49,8 +51,13 @@
  */
 public class TaskViewSimulator implements TransformParams.BuilderProxy {
 
+    private final String TAG = "TaskViewSimulator";
+    private final boolean DEBUG = false;
+
     private final Rect mTmpCropRect = new Rect();
     private final RectF mTempRectF = new RectF();
+    // Additional offset for split tasks
+    private final Point mSplitOffset = new Point();
     private final float[] mTempPoint = new float[2];
 
     private final Context mContext;
@@ -63,6 +70,8 @@
     private final Rect mTaskRect = new Rect();
     private final PointF mPivot = new PointF();
     private DeviceProfile mDp;
+    @StagePosition
+    private int mStagePosition = STAGE_POSITION_UNDEFINED;
 
     private final Matrix mMatrix = new Matrix();
     private final Matrix mMatrixTmp = new Matrix();
@@ -89,6 +98,7 @@
     // Cached calculations
     private boolean mLayoutValid = false;
     private int mOrientationStateId;
+    private StagedSplitBounds mStagedSplitBounds;
 
     public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
         mContext = context;
@@ -128,9 +138,19 @@
         if (mDp == null) {
             return 1;
         }
-        mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect,
-                mOrientationState.getOrientationHandler());
-        return mOrientationState.getFullScreenScaleAndPivot(mTaskRect, mDp, mPivot);
+        Rect fullTaskSize = new Rect();
+        mSizeStrategy.calculateTaskSize(mContext, mDp, fullTaskSize);
+
+        if (mStagedSplitBounds != null) {
+            // The task rect changes according to the staged split task sizes, but recents
+            // fullscreen scale and pivot remains the same since the task fits into the existing
+            // sized task space bounds
+            mSizeStrategy.calculateStagedSplitTaskSize(mContext, mDp, mTaskRect, mStagedSplitBounds,
+                    mStagePosition);
+        } else {
+            mTaskRect.set(fullTaskSize);
+        }
+        return mOrientationState.getFullScreenScaleAndPivot(fullTaskSize, mDp, mPivot);
     }
 
     /**
@@ -143,6 +163,24 @@
     }
 
     /**
+     * Sets the targets which the simulator will control specifically for targets to animate when
+     * in split screen
+     *
+     * @param splitInfo set to {@code null} when not in staged split mode
+     */
+    public void setPreview(RemoteAnimationTargetCompat runningTarget, StagedSplitBounds splitInfo) {
+        setPreview(runningTarget);
+        mStagedSplitBounds = splitInfo;
+        if (mStagedSplitBounds == null) {
+            mStagePosition = STAGE_POSITION_UNDEFINED;
+            return;
+        }
+        mStagePosition = mThumbnailPosition.equals(splitInfo.mLeftTopBounds) ?
+                STAGE_POSITION_TOP_OR_LEFT :
+                STAGE_POSITION_BOTTOM_OR_RIGHT;
+    }
+
+    /**
      * Sets the targets which the simulator will control
      */
     public void setPreviewBounds(Rect bounds, Rect insets) {
@@ -239,6 +277,15 @@
             getFullScreenScale();
             mThumbnailData.rotation = mOrientationState.getDisplayRotation();
 
+            // TODO(b/195145340) handle non 50-50 split scenarios
+            if (mStagedSplitBounds != null) {
+                if (mStagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+                    // The preview set is for the bottom/right, inset by top/left task
+                    mSplitOffset.y = mStagedSplitBounds.mLeftTopBounds.height() +
+                        mStagedSplitBounds.mDividerBounds.height() / 2;
+                }
+            }
+
             // mIsRecentsRtl is the inverse of TaskView RTL.
             boolean isRtlEnabled = !mIsRecentsRtl;
             mPositionHelper.updateThumbnailMatrix(
@@ -246,6 +293,9 @@
                     mTaskRect.width(), mTaskRect.height(),
                     mDp, mOrientationState.getRecentsActivityRotation(), isRtlEnabled);
             mPositionHelper.getMatrix().invert(mInversePositionMatrix);
+            if (DEBUG) {
+                Log.d(TAG, " taskRect: " + mTaskRect + " splitOffset: " + mSplitOffset);
+            }
         }
 
         float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
@@ -280,6 +330,9 @@
                 recentsViewPrimaryTranslation.value);
         applyWindowToHomeRotation(mMatrix);
 
+        // Move lower/right split window into correct position
+        mMatrix.postTranslate(0, mSplitOffset.y);
+
         // Crop rect is the inverse of thumbnail matrix
         mTempRectF.set(-insets.left, -insets.top,
                 taskWidth + insets.right, taskHeight + insets.bottom);
@@ -287,6 +340,25 @@
         mTempRectF.roundOut(mTmpCropRect);
 
         params.applySurfaceParams(params.createSurfaceParams(this));
+
+        if (!DEBUG) {
+            return;
+        }
+        Log.d(TAG, "progress: " + fullScreenProgress
+                + " scale: " + scale
+                + " recentsViewScale: " + recentsViewScale.value
+                + " crop: " + mTmpCropRect
+                + " radius: " + getCurrentCornerRadius()
+                + " translate: " + mSplitOffset
+                + " taskW: " + taskWidth + " H: " + taskHeight
+                + " taskRect: " + mTaskRect
+                + " taskPrimaryT: " + taskPrimaryTranslation.value
+                + " recentsPrimaryT: " + recentsViewPrimaryTranslation.value
+                + " recentsSecondaryT: " + recentsViewSecondaryTranslation.value
+                + " taskSecondaryT: " + taskSecondaryTranslation.value
+                + " recentsScroll: " + recentsViewScroll.value
+                + " pivot: " + mPivot
+        );
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
new file mode 100644
index 0000000..cd20f4b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -0,0 +1,137 @@
+package com.android.quickstep.views;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.TaskThumbnailCache;
+import com.android.quickstep.util.CancellableTask;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
+ *
+ * That's right. If you call within the next 5 minutes we'll go ahead and double your order and
+ * send you !! TWO !! Tasks along with their TaskThumbnailViews complimentary. On. The. House.
+ * And not only that, we'll even clean up your thumbnail request if you don't like it.
+ * All the benefits of one TaskView, except DOUBLED!
+ *
+ * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
+ */
+public class GroupedTaskView extends TaskView {
+
+    private Task mSecondaryTask;
+    private TaskThumbnailView mSnapshotView2;
+    private CancellableTask mThumbnailLoadRequest2;
+
+    public GroupedTaskView(Context context) {
+        super(context);
+    }
+
+    public GroupedTaskView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSnapshotView2 = findViewById(R.id.bottomright_snapshot);
+    }
+
+    public void bind(Task primary, Task secondary, RecentsOrientedState orientedState) {
+        super.bind(primary, orientedState);
+        mSecondaryTask = secondary;
+        mTaskIdContainer[1] = secondary.key.id;
+        mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2);
+        mSnapshotView2.bind(secondary);
+        adjustThumbnailBoundsForSplit();
+    }
+
+    @Override
+    public void onTaskListVisibilityChanged(boolean visible, int changes) {
+        super.onTaskListVisibilityChanged(visible, changes);
+        if (visible) {
+            RecentsModel model = RecentsModel.INSTANCE.get(getContext());
+            TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
+
+            if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
+                mThumbnailLoadRequest2 = thumbnailCache.updateThumbnailInBackground(mSecondaryTask,
+                        thumbnailData -> mSnapshotView2.setThumbnail(
+                                mSecondaryTask, thumbnailData
+                        ));
+            }
+
+            if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+                // TODO What's the Icon for this going to look like? :o
+            }
+        } else {
+            if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
+                mSnapshotView2.setThumbnail(null, null);
+                // Reset the task thumbnail reference as well (it will be fetched from the cache or
+                // reloaded next time we need it)
+                mSecondaryTask.thumbnail = null;
+            }
+            if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+                // TODO
+            }
+        }
+    }
+
+    @Override
+    protected void cancelPendingLoadTasks() {
+        super.cancelPendingLoadTasks();
+        if (mThumbnailLoadRequest2 != null) {
+            mThumbnailLoadRequest2.cancel();
+            mThumbnailLoadRequest2 = null;
+        }
+    }
+
+    @Override
+    public void onRecycle() {
+        super.onRecycle();
+        mSnapshotView2.setThumbnail(mSecondaryTask, null);
+    }
+
+    @Override
+    public void setOverlayEnabled(boolean overlayEnabled) {
+        super.setOverlayEnabled(overlayEnabled);
+        mSnapshotView2.setOverlayEnabled(overlayEnabled);
+    }
+
+    private void adjustThumbnailBoundsForSplit() {
+        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+        ViewGroup.LayoutParams primaryLp = mSnapshotView.getLayoutParams();
+        primaryLp.width = mSecondaryTask == null ?
+                MATCH_PARENT :
+                getWidth();
+        int spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx;
+        // TODO get divider height
+        int dividerBar = 20;
+        primaryLp.height = mSecondaryTask == null ?
+                MATCH_PARENT :
+                (getHeight() - spaceAboveSnapshot - dividerBar) / 2;
+        mSnapshotView.setLayoutParams(primaryLp);
+
+        if (mSecondaryTask == null) {
+            mSnapshotView2.setVisibility(GONE);
+            return;
+        }
+
+        mSnapshotView2.setVisibility(VISIBLE);
+        ViewGroup.LayoutParams secondaryLp = mSnapshotView2.getLayoutParams();
+        secondaryLp.width = getWidth();
+        secondaryLp.height = primaryLp.height;
+        mSnapshotView2.setLayoutParams(secondaryLp);
+        mSnapshotView2.setTranslationY(primaryLp.height + spaceAboveSnapshot + dividerBar);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 538e61e..03827d2 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -19,6 +19,7 @@
 import static android.view.Surface.ROTATION_0;
 import static android.view.View.MeasureSpec.EXACTLY;
 import static android.view.View.MeasureSpec.makeMeasureSpec;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
@@ -51,6 +52,7 @@
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -143,11 +145,13 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
 import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.SwipeUpAnimationLogic.RemoteTargetHandle;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.ViewUtils;
+import com.android.quickstep.util.LauncherSplitScreenListener;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitScreenBounds;
@@ -317,7 +321,13 @@
                     view.setScaleY(scale);
                     view.mLastComputedTaskStartPushOutDistance = null;
                     view.mLastComputedTaskEndPushOutDistance = null;
-                    view.mLiveTileTaskViewSimulator.recentsViewScale.value = scale;
+                    view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() {
+                        @Override
+                        public void accept(RemoteTargetHandle remoteTargetHandle) {
+                            remoteTargetHandle.mTaskViewSimulator.recentsViewScale.value =
+                                    scale;
+                        }
+                    });
                     view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation);
                     view.updatePageOffsets();
                 }
@@ -365,8 +375,7 @@
     // mTaskGridVerticalDiff and mTopBottomRowHeightDiff summed together provides the top
     // position for bottom row of grid tasks.
 
-    protected final TransformParams mLiveTileParams = new TransformParams();
-    protected final TaskViewSimulator mLiveTileTaskViewSimulator;
+    protected RemoteTargetHandle[] mRemoteTargetHandles;
     protected final Rect mLastComputedTaskSize = new Rect();
     protected final Rect mLastComputedGridSize = new Rect();
     protected final Rect mLastComputedGridTaskSize = new Rect();
@@ -402,9 +411,10 @@
     private final InvariantDeviceProfile mIdp;
 
     /**
-     * Getting views should be done via {@link #getTaskViewFromPool()}
+     * Getting views should be done via {@link #getTaskViewFromPool(boolean)}
      */
     private final ViewPool<TaskView> mTaskViewPool;
+    private final ViewPool<GroupedTaskView> mGroupedTaskViewPool;
 
     private final TaskOverlayFactory mTaskOverlayFactory;
 
@@ -504,13 +514,13 @@
     // Only valid until the launcher state changes to NORMAL
     /**
      * ID for the current running TaskView view, unique amongst TaskView instances. ID's are set
-     * through {@link #getTaskViewFromPool()} and incremented by {@link #mTaskViewIdCount}
+     * through {@link #getTaskViewFromPool(boolean)} and incremented by {@link #mTaskViewIdCount}
      */
     protected int mRunningTaskViewId = -1;
     private int mTaskViewIdCount;
     private final int[] INVALID_TASK_IDS = new int[]{-1, -1};
     protected boolean mRunningTaskTileHidden;
-    private Task mTmpRunningTask;
+    private Task[] mTmpRunningTasks;
     protected int mFocusedTaskViewId = -1;
 
     private boolean mTaskIconScaledDown = false;
@@ -626,6 +636,9 @@
         mClearAllButton.setOnClickListener(this::dismissAllTasks);
         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
                 10 /* initial size */);
+        // There's only one pair of grouped tasks we can envision at the moment
+        mGroupedTaskViewPool = new ViewPool<>(context, this,
+                R.layout.task_grouped, 2 /* max size */, 1 /* initial size */);
 
         mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
@@ -656,10 +669,6 @@
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
 
-        mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy());
-        mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
-        mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
-
         mTintingColor = getForegroundScrimDimColor(context);
     }
 
@@ -707,7 +716,7 @@
             super.dispatchDraw(canvas);
         }
         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
-                && mLiveTileParams.getTargetSet() != null) {
+                && mRemoteTargetHandles != null) {
             redrawLiveTile();
         }
     }
@@ -749,9 +758,13 @@
         if (mHandleTaskStackChanges) {
             TaskView taskView = getTaskViewByTaskId(taskId);
             if (taskView != null) {
-                Task task = taskView.getTask();
-                taskView.getThumbnail().setThumbnail(task, thumbnailData);
-                return task;
+                for (TaskView.TaskIdAttributeContainer container :
+                        taskView.getTaskIdAttributeContainers()) {
+                    if (container == null || taskId != container.getTask().key.id) {
+                        continue;
+                    }
+                    container.getThumbnailView().setThumbnail(container.getTask(), thumbnailData);
+                }
             }
         }
         return null;
@@ -812,7 +825,8 @@
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
         mSyncTransactionApplier = new SurfaceTransactionApplier(this);
-        mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
+        runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.mTransformParams
+                .setSyncTransactionApplier(mSyncTransactionApplier));
         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
         mIPipAnimationListener.setActivityAndRecentsView(mActivity, this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
@@ -830,7 +844,8 @@
         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
         mSyncTransactionApplier = null;
-        mLiveTileParams.setSyncTransactionApplier(null);
+        runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.mTransformParams
+                .setSyncTransactionApplier(null));
         executeSideTaskLaunchCallback();
         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
@@ -851,9 +866,15 @@
         if (child instanceof TaskView && child != mSplitHiddenTaskView
                 && child != mMovingTaskView) {
             TaskView taskView = (TaskView) child;
-            mHasVisibleTaskData.delete(taskView.getTaskIds()[0]);
+            for (int i : taskView.getTaskIds()) {
+                mHasVisibleTaskData.delete(i);
+            }
+            if (child instanceof GroupedTaskView) {
+                mGroupedTaskViewPool.recycle((GroupedTaskView)taskView);
+            } else {
+                mTaskViewPool.recycle(taskView);
+            }
             taskView.setTaskViewId(-1);
-            mTaskViewPool.recycle(taskView);
             mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
         }
         updateTaskStartIndex(child);
@@ -906,10 +927,15 @@
         }
     }
 
+    /**
+     * TODO(b/195675206) Check both taskIDs from runningTaskViewId
+     *  and launch if either of them is {@param taskId}
+     */
     public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
         int runningTaskViewId = getTaskViewIdFromTaskId(taskId);
         if (mRunningTaskViewId != -1 && mRunningTaskViewId == runningTaskViewId) {
-            RemoteAnimationTargets targets = getLiveTileParams().getTargetSet();
+            TransformParams params = mRemoteTargetHandles[0].mTransformParams;
+            RemoteAnimationTargets targets = params.getTargetSet();
             if (targets != null && targets.findTask(taskId) != null) {
                 launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers,
                         targets.nonApps);
@@ -1015,10 +1041,21 @@
         if (!enabled) {
             // Reset the running task when leaving overview since it can still have a reference to
             // its thumbnail
-            mTmpRunningTask = null;
+            mTmpRunningTasks = null;
             if (mSplitSelectStateController.isSplitSelectActive()) {
                 cancelSplitSelect(false);
             }
+            // Remove grouped tasks and recycle once we exit overview
+            int taskCount = getTaskViewCount();
+            for (int i = 0; i < taskCount; i++) {
+                View v = getTaskViewAt(i);
+                if (!(v instanceof GroupedTaskView)) {
+                    return;
+                }
+                GroupedTaskView gtv = (GroupedTaskView) v;
+                gtv.onTaskListVisibilityChanged(false);
+                removeView(gtv);
+            }
         }
         updateLocusId();
     }
@@ -1220,18 +1257,32 @@
         TaskView ignoreResetTaskView =
                 mIgnoreResetTaskId == -1 ? null : getTaskViewByTaskId(mIgnoreResetTaskId);
 
-        final int requiredTaskCount = tasks.size();
-        if (getTaskViewCount() != requiredTaskCount) {
+        int[] splitTaskIds =
+                LauncherSplitScreenListener.INSTANCE.getNoCreate().getSplitTaskIds();
+        int requiredGroupTaskViews = splitTaskIds.length / 2;
+
+        // Subtract half the number of split tasks and not total number because we've already
+        // added a GroupedTaskView when swipe up gesture happens.
+        // This will need to change if we start showing GroupedTaskViews during swipe up from home
+        int requiredTaskViewCount = tasks.size() - requiredGroupTaskViews;
+
+        if (getTaskViewCount() != requiredTaskViewCount) {
             if (indexOfChild(mClearAllButton) != -1) {
                 removeView(mClearAllButton);
             }
-            for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
-                addView(getTaskViewFromPool());
+
+            for (int i = getTaskViewCount(); i < requiredTaskViewCount; i++) {
+                addView(getTaskViewFromPool(false));
             }
-            while (getTaskViewCount() > requiredTaskCount) {
+            while (getTaskViewCount() > requiredTaskViewCount) {
                 removeView(getChildAt(getChildCount() - 1));
             }
-            if (requiredTaskCount > 0) {
+            while (requiredGroupTaskViews > 0) {
+                // Add to front of list
+                addView(getTaskViewFromPool(true), 0);
+                requiredGroupTaskViews--;
+            }
+            if (requiredTaskViewCount > 0) {
                 addView(mClearAllButton);
             }
         }
@@ -1245,12 +1296,28 @@
                 + " runningTaskViewId: " + mRunningTaskViewId
                 + " forTaskView: " + getTaskViewFromTaskViewId(mRunningTaskViewId));
 
-        // Rebind and reset all task views
-        for (int i = requiredTaskCount - 1; i >= 0; i--) {
-            final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
-            final Task task = tasks.get(i);
+        for (int taskViewIndex = requiredTaskViewCount - 1, taskDataIndex = tasks.size() - 1;
+                taskViewIndex >= 0;
+                taskViewIndex--, taskDataIndex--) {
+            final int pageIndex = requiredTaskViewCount - taskViewIndex - 1 + mTaskViewStartIndex;
+            final Task task = tasks.get(taskDataIndex);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
-            taskView.bind(task, mOrientationState);
+            if (taskView instanceof GroupedTaskView) {
+                Task leftTop;
+                Task rightBottom;
+                if (task.key.id == splitTaskIds[0]) {
+                    leftTop = task;
+                    taskDataIndex--;
+                    rightBottom = tasks.get(taskDataIndex);
+                } else {
+                    rightBottom = task;
+                    taskDataIndex--;
+                    leftTop = tasks.get(taskDataIndex);
+                }
+                ((GroupedTaskView) taskView).bind(leftTop, rightBottom, mOrientationState);
+            } else {
+                taskView.bind(task, mOrientationState);
+            }
         }
 
         // Keep same previous focused task
@@ -1270,8 +1337,8 @@
             newRunningTaskView = getTaskViewByTaskId(runningTaskId);
             if (newRunningTaskView == null) {
                 StringBuilder sb = new StringBuilder();
-                for (int i = requiredTaskCount - 1; i >= 0; i--) {
-                    final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
+                for (int i = requiredTaskViewCount - 1; i >= 0; i--) {
+                    final int pageIndex = requiredTaskViewCount - i - 1 + mTaskViewStartIndex;
                     final TaskView taskView = (TaskView) getChildAt(pageIndex);
                     int taskViewId = taskView.getTaskViewId();
                     sb.append(" taskViewId: " + taskViewId
@@ -1354,12 +1421,12 @@
             // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
             // to reset the params after it settles in Overview from swipe up so that we don't
             // render with obsolete param values.
-            mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = 0;
-            mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0;
-            mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
-            mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
-
-            mLiveTileParams.setTargetAlpha(1);
+            runActionOnRemoteHandles(remoteTargetHandle -> {
+                remoteTargetHandle.mTaskViewSimulator.taskPrimaryTranslation.value = 0;
+                remoteTargetHandle.mTaskViewSimulator.taskSecondaryTranslation.value = 0;
+                remoteTargetHandle.mTaskViewSimulator.fullScreenProgress.value = 0;
+                remoteTargetHandle.mTaskViewSimulator.recentsViewScale.value = 1;
+            });
 
             // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
             // null.
@@ -1413,7 +1480,8 @@
         setPageSpacing(dp.overviewPageSpacing);
 
         // Propagate DeviceProfile change event.
-        mLiveTileTaskViewSimulator.setDp(dp);
+        runActionOnRemoteHandles(
+                remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator.setDp(dp));
         mActionsView.setDp(dp);
         mOrientationState.setDeviceProfile(dp);
 
@@ -1524,8 +1592,7 @@
     }
 
     public void getTaskSize(Rect outRect) {
-        mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
-                mOrientationHandler);
+        mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
         mLastComputedTaskSize.set(outRect);
     }
 
@@ -1533,8 +1600,7 @@
      * Returns the size of task selected to enter modal state.
      */
     public Point getSelectedTaskSize() {
-        mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), mTempRect,
-                mOrientationHandler);
+        mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), mTempRect);
         return new Point(mTempRect.width(), mTempRect.height());
     }
 
@@ -1667,8 +1733,17 @@
                 visible = lower <= index && index <= upper;
             }
             if (visible) {
-                if (task == mTmpRunningTask) {
-                    // Skip loading if this is the task that we are animating into
+                boolean skipLoadingTask = false;
+                if (mTmpRunningTasks != null) {
+                    for (Task t : mTmpRunningTasks) {
+                        if (task == t) {
+                            // Skip loading if this is the task that we are animating into
+                            skipLoadingTask = true;
+                            break;
+                        }
+                    }
+                }
+                if (skipLoadingTask) {
                     continue;
                 }
                 if (!mHasVisibleTaskData.get(task.key.id)) {
@@ -1743,7 +1818,8 @@
             }
         }
         setEnableDrawingLiveTile(false);
-        mLiveTileParams.setTargetSet(null);
+        runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.mTransformParams
+                .setTargetSet(null));
 
         // These are relatively expensive and don't need to be done this frame (RecentsView isn't
         // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
@@ -1810,8 +1886,10 @@
      * Handle the edge case where Recents could increment task count very high over long
      * period of device usage. Probably will never happen, but meh.
      */
-    private TaskView getTaskViewFromPool() {
-        TaskView taskView = mTaskViewPool.getView();
+    private <T extends TaskView> T getTaskViewFromPool(boolean isGrouped) {
+        T taskView = isGrouped ?
+                (T) mGroupedTaskViewPool.getView() :
+                (T) mTaskViewPool.getView();
         taskView.setTaskViewId(mTaskViewIdCount);
         if (mTaskViewIdCount == Integer.MAX_VALUE) {
             mTaskViewIdCount = 0;
@@ -1847,7 +1925,7 @@
     /**
      * Called when a gesture from an app is starting.
      */
-    public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
+    public void onGestureAnimationStart(RunningTaskInfo[] runningTaskInfo) {
         mGestureActive = true;
         // This needs to be called before the other states are set since it can create the task view
         if (mOrientationState.setGestureActive(true)) {
@@ -1914,7 +1992,7 @@
      */
     public void onPrepareGestureEndAnimation(
             @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
-            TaskViewSimulator taskViewSimulator) {
+            TaskViewSimulator[] taskViewSimulators) {
         mCurrentGestureEndTarget = endTarget;
         if (endTarget == GestureState.GestureEndTarget.RECENTS) {
             setEnableFreeScroll(true);
@@ -1931,13 +2009,16 @@
                         runningTaskView.getGridTranslationX(),
                         runningTaskView.getGridTranslationY());
             }
-            if (animatorSet == null) {
-                setGridProgress(1);
-                taskViewSimulator.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
-            } else {
-                animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
-                animatorSet.play(taskViewSimulator.taskPrimaryTranslation.animateToValue(
-                        runningTaskPrimaryGridTranslation));
+            for (TaskViewSimulator tvs : taskViewSimulators) {
+                if (animatorSet == null) {
+                    setGridProgress(1);
+                    tvs.taskPrimaryTranslation.value =
+                            runningTaskPrimaryGridTranslation;
+                } else {
+                    animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
+                    animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
+                            runningTaskPrimaryGridTranslation));
+                }
             }
         }
     }
@@ -1966,7 +2047,21 @@
     /**
      * Returns true if we should add a stub taskView for the running task id
      */
-    protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
+    protected boolean shouldAddStubTaskView(RunningTaskInfo[] runningTaskInfos) {
+        if (runningTaskInfos.length > 1) {
+            // * Always create new view for GroupedTaskView
+            // * Remove existing associated taskViews for tasks currently in split
+            for (RunningTaskInfo rti : runningTaskInfos) {
+                TaskView taskView = getTaskViewByTaskId(rti.taskId);
+                if (taskView == null) {
+                    continue;
+                }
+                taskView.onTaskListVisibilityChanged(false);
+                removeView(taskView);
+            }
+            return true;
+        }
+        RunningTaskInfo runningTaskInfo = runningTaskInfos[0];
         return runningTaskInfo != null && getTaskViewByTaskId(runningTaskInfo.taskId) == null;
     }
 
@@ -1976,29 +2071,44 @@
      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
      * is called.  Also scrolls the view to this task.
      */
-    public void showCurrentTask(RunningTaskInfo runningTaskInfo) {
+    public void showCurrentTask(RunningTaskInfo[] runningTaskInfo) {
         int runningTaskViewId = -1;
+        boolean needGroupTaskView = runningTaskInfo.length > 1;
+        RunningTaskInfo taskInfo = runningTaskInfo[0];
         if (shouldAddStubTaskView(runningTaskInfo)) {
             boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
-            final TaskView taskView = getTaskViewFromPool();
+            final TaskView taskView;
+            if (needGroupTaskView) {
+                taskView = getTaskViewFromPool(true);
+                RunningTaskInfo secondaryTaskInfo = runningTaskInfo[1];
+                mTmpRunningTasks = new Task[]{
+                        Task.from(new TaskKey(taskInfo), taskInfo, false),
+                        Task.from(new TaskKey(secondaryTaskInfo), secondaryTaskInfo, false)
+                };
+                addView(taskView, mTaskViewStartIndex);
+                ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
+                        mOrientationState);
+            } else {
+                taskView = getTaskViewFromPool(false);
+                addView(taskView, mTaskViewStartIndex);
+                // The temporary running task is only used for the duration between the start of the
+                // gesture and the task list is loaded and applied
+                mTmpRunningTasks = new Task[]{Task.from(new TaskKey(taskInfo), taskInfo, false)};
+                taskView.bind(mTmpRunningTasks[0], mOrientationState);
+            }
             runningTaskViewId = taskView.getTaskViewId();
-            addView(taskView, mTaskViewStartIndex);
             if (wasEmpty) {
                 addView(mClearAllButton);
             }
-            // The temporary running task is only used for the duration between the start of the
-            // gesture and the task list is loaded and applied
-            mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
-            taskView.bind(mTmpRunningTask, mOrientationState);
 
             // Measure and layout immediately so that the scroll values is updated instantly
             // as the user might be quick-switching
             measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
                     makeMeasureSpec(getMeasuredHeight(), EXACTLY));
             layout(getLeft(), getTop(), getRight(), getBottom());
-        } else if (getTaskViewByTaskId(runningTaskInfo.taskId) != null) {
-            runningTaskViewId = getTaskViewByTaskId(runningTaskInfo.taskId).getTaskViewId();
+        } else if (!needGroupTaskView && getTaskViewByTaskId(taskInfo.taskId) != null) {
+            runningTaskViewId = getTaskViewByTaskId(taskInfo.taskId).getTaskViewId();
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -2413,8 +2523,11 @@
         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
         // alpha is set to 0 so that it can be recycled in the view pool properly
         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask()) {
-            anim.setFloat(mLiveTileParams, TransformParams.TARGET_ALPHA, 0,
-                    clampToProgress(ACCEL, 0, 0.5f));
+            runActionOnRemoteHandles(remoteTargetHandle -> {
+                TransformParams params = remoteTargetHandle.mTransformParams;
+                anim.setFloat(params, TransformParams.TARGET_ALPHA, 0,
+                        clampToProgress(ACCEL, 0, 0.5f));
+            });
         }
         anim.setFloat(taskView, VIEW_ALPHA, 0, clampToProgress(ACCEL, 0, 0.5f));
         FloatProperty<TaskView> secondaryViewTranslate =
@@ -2433,10 +2546,12 @@
         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
                 && taskView.isRunningTask()) {
             anim.addOnFrameCallback(() -> {
-                mLiveTileTaskViewSimulator.taskSecondaryTranslation.value =
-                        mOrientationHandler.getSecondaryValue(
-                                taskView.getTranslationX(),
-                                taskView.getTranslationY());
+                runActionOnRemoteHandles(
+                        remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator
+                                .taskSecondaryTranslation.value = mOrientationHandler
+                                .getSecondaryValue(taskView.getTranslationX(),
+                                        taskView.getTranslationY()
+                                ));
                 redrawLiveTile();
             });
         }
@@ -2487,7 +2602,6 @@
         boolean showAsGrid = showAsGrid();
         int taskCount = getTaskViewCount();
         int dismissedIndex = indexOfChild(dismissedTaskView);
-        int dismissedTaskId = dismissedTaskView.getTaskIds()[0];
         int dismissedTaskViewId = dismissedTaskView.getTaskViewId();
 
         // Grid specific properties.
@@ -2585,9 +2699,14 @@
                             && child instanceof TaskView
                             && ((TaskView) child).isRunningTask()) {
                         anim.addOnFrameCallback(() -> {
-                            mLiveTileTaskViewSimulator.taskPrimaryTranslation.value =
-                                    mOrientationHandler.getPrimaryValue(child.getTranslationX(),
-                                            child.getTranslationY());
+                            runActionOnRemoteHandles(
+                                    remoteTargetHandle ->
+                                            remoteTargetHandle.mTaskViewSimulator
+                                                    .taskPrimaryTranslation.value =
+                                                    mOrientationHandler.getPrimaryValue(
+                                                            child.getTranslationX(),
+                                                            child.getTranslationY()
+                                                    ));
                             redrawLiveTile();
                         });
                     }
@@ -2669,9 +2788,9 @@
                             if (ENABLE_QUICKSTEP_LIVE_TILE.get()
                                     && dismissedTaskView.isRunningTask()) {
                                 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
-                                        () -> removeTaskInternal(dismissedTaskId));
+                                        () -> removeTaskInternal(dismissedTaskViewId));
                             } else {
-                                removeTaskInternal(dismissedTaskId);
+                                removeTaskInternal(dismissedTaskViewId);
                             }
                             mActivity.getStatsLogManager().logger()
                                     .withItemInfo(dismissedTaskView.getItemInfo())
@@ -2801,9 +2920,17 @@
         return lastVisibleIndex;
     }
 
-    private void removeTaskInternal(int dismissedTaskId) {
+    private void removeTaskInternal(int dismissedTaskViewId) {
+        int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
+        int primaryTaskId = taskIds[0];
+        int secondaryTaskId = taskIds[1];
         UI_HELPER_EXECUTOR.getHandler().postDelayed(
-                () -> ActivityManagerWrapper.getInstance().removeTask(dismissedTaskId),
+                () -> {
+                    ActivityManagerWrapper.getInstance().removeTask(primaryTaskId);
+                    if (secondaryTaskId != -1) {
+                        ActivityManagerWrapper.getInstance().removeTask(secondaryTaskId);
+                    }
+                },
                 REMOVE_TASK_WAIT_FOR_APP_STOP_MS);
     }
 
@@ -3136,7 +3263,9 @@
         mLastComputedTaskStartPushOutDistance = null;
         mLastComputedTaskEndPushOutDistance = null;
         updatePageOffsets();
-        mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
+        runActionOnRemoteHandles(
+                remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator
+                        .setScroll(getScrollOffset()));
         setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
     }
@@ -3200,7 +3329,9 @@
             translationProperty.set(child, totalTranslation);
             if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
                     && i == getRunningTaskIndex()) {
-                mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = totalTranslation;
+                runActionOnRemoteHandles(
+                        remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator
+                                .taskPrimaryTranslation.value = totalTranslation);
                 redrawLiveTile();
             }
         }
@@ -3303,7 +3434,9 @@
             TaskView task = getTaskViewAt(i);
             task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
         }
-        mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation;
+        runActionOnRemoteHandles(
+                remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator
+                        .recentsViewSecondaryTranslation.value = translation);
     }
 
     protected void setTaskViewsPrimarySplitTranslation(float translation) {
@@ -3505,8 +3638,6 @@
         resetTaskVisuals();
         mSplitHiddenTaskView.setVisibility(VISIBLE);
         mSplitHiddenTaskView = null;
-        mSecondSplitHiddenTaskView.setVisibility(VISIBLE);
-        mSecondSplitHiddenTaskView = null;
         mSplitHiddenTaskViewIndex = -1;
         if (mFirstFloatingTaskView != null) {
             mActivity.getRootView().removeView(mFirstFloatingTaskView);
@@ -3515,6 +3646,8 @@
         if (mSecondFloatingTaskView != null) {
             mActivity.getRootView().removeView(mSecondFloatingTaskView);
             mSecondFloatingTaskView = null;
+            mSecondSplitHiddenTaskView.setVisibility(VISIBLE);
+            mSecondSplitHiddenTaskView = null;
         }
     }
 
@@ -3610,10 +3743,12 @@
             int runningTaskIndex = recentsView.getRunningTaskIndex();
             if (ENABLE_QUICKSTEP_LIVE_TILE.get() && runningTaskIndex != -1
                     && runningTaskIndex != taskIndex) {
-                anim.play(ObjectAnimator.ofFloat(
-                        recentsView.getLiveTileTaskViewSimulator().taskPrimaryTranslation,
-                        AnimatedFloat.VALUE,
-                        primaryTranslation));
+                for (RemoteTargetHandle remoteHandle : recentsView.getRemoteTargetHandles()) {
+                    anim.play(ObjectAnimator.ofFloat(
+                            remoteHandle.mTaskViewSimulator.taskPrimaryTranslation,
+                            AnimatedFloat.VALUE,
+                            primaryTranslation));
+                }
             }
 
             int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
@@ -3693,7 +3828,9 @@
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
+            runActionOnRemoteHandles(
+                    remoteTargetHandle -> remoteTargetHandle.mTaskViewSimulator
+                            .addOverviewToAppAnim(mPendingAnimation, interpolator));
             mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
         }
         mPendingAnimation.addEndListener(isSuccess -> {
@@ -3784,31 +3921,87 @@
     }
 
     public void redrawLiveTile() {
-        if (mLiveTileParams.getTargetSet() != null) {
-            mLiveTileTaskViewSimulator.apply(mLiveTileParams);
-        }
+        runActionOnRemoteHandles(remoteTargetHandle -> {
+            TransformParams params = remoteTargetHandle.mTransformParams;
+            if (params.getTargetSet() != null) {
+                remoteTargetHandle.mTaskViewSimulator.apply(params);
+            }
+        });
     }
 
-    public TaskViewSimulator getLiveTileTaskViewSimulator() {
-        return mLiveTileTaskViewSimulator;
-    }
-
-    public TransformParams getLiveTileParams() {
-        return mLiveTileParams;
+    public RemoteTargetHandle[] getRemoteTargetHandles() {
+        return mRemoteTargetHandles;
     }
 
     // TODO: To be removed in a follow up CL
     public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
             RecentsAnimationTargets recentsAnimationTargets) {
         mRecentsAnimationController = recentsAnimationController;
-        if (recentsAnimationTargets != null && recentsAnimationTargets.apps.length > 0) {
-            if (mSyncTransactionApplier != null) {
-                recentsAnimationTargets.addReleaseCheck(mSyncTransactionApplier);
-            }
-            mLiveTileTaskViewSimulator.setPreview(
-                    recentsAnimationTargets.apps[recentsAnimationTargets.apps.length - 1]);
-            mLiveTileParams.setTargetSet(recentsAnimationTargets);
+        if (recentsAnimationTargets == null || recentsAnimationTargets.apps.length == 0) {
+            return;
         }
+
+        if (mSyncTransactionApplier != null) {
+            recentsAnimationTargets.addReleaseCheck(mSyncTransactionApplier);
+        }
+
+        // TODO Consolidate this shared code with SwipeUpAnimationLogic (or mabe just reuse
+        //  what that class has and pass it into here
+        mRemoteTargetHandles = new RemoteTargetHandle[recentsAnimationTargets.apps.length];
+        TaskViewSimulator primaryTvs = createTaskViewSimulator();
+        mRemoteTargetHandles[0] = new RemoteTargetHandle(primaryTvs, new TransformParams());
+        if (recentsAnimationTargets.apps.length == 1) {
+            mRemoteTargetHandles[0].mTaskViewSimulator
+                    .setPreview(recentsAnimationTargets.apps[0], null);
+            mRemoteTargetHandles[0].mTransformParams.setTargetSet(recentsAnimationTargets);
+        } else {
+            TaskViewSimulator secondaryTvs = createTaskViewSimulator();
+            secondaryTvs.setOrientationState(mOrientationState);
+            secondaryTvs.recentsViewScale.value = 1;
+
+            mRemoteTargetHandles[1] = new RemoteTargetHandle(secondaryTvs, new TransformParams());
+            RemoteAnimationTargetCompat dividerTarget =
+                    recentsAnimationTargets.getNonAppTargetOfType(TYPE_DOCK_DIVIDER);
+            RemoteAnimationTargetCompat primaryTaskTarget = recentsAnimationTargets.apps[0];
+            RemoteAnimationTargetCompat secondaryTaskTarget = recentsAnimationTargets.apps[1];
+            SplitConfigurationOptions.StagedSplitBounds
+                    info = new SplitConfigurationOptions.StagedSplitBounds(
+                    primaryTaskTarget.screenSpaceBounds,
+                    secondaryTaskTarget.screenSpaceBounds, dividerTarget.screenSpaceBounds);
+            mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(primaryTaskTarget, info);
+            mRemoteTargetHandles[1].mTaskViewSimulator.setPreview(secondaryTaskTarget, info);
+            RemoteAnimationTargets rats = new RemoteAnimationTargets(
+                    new RemoteAnimationTargetCompat[]{primaryTaskTarget},
+                    recentsAnimationTargets.wallpapers, recentsAnimationTargets.nonApps,
+                    MODE_CLOSING
+            );
+            RemoteAnimationTargets splitRats = new RemoteAnimationTargets(
+                    new RemoteAnimationTargetCompat[]{secondaryTaskTarget},
+                    recentsAnimationTargets.wallpapers, recentsAnimationTargets.nonApps,
+                    MODE_CLOSING
+            );
+            mRemoteTargetHandles[0].mTransformParams.setTargetSet(rats);
+            mRemoteTargetHandles[1].mTransformParams.setTargetSet(splitRats);
+        }
+    }
+
+    /** Helper to avoid writing some for-loops to iterate over {@link #mRemoteTargetHandles} */
+    private void runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer) {
+        if (mRemoteTargetHandles == null) {
+            return;
+        }
+
+        for (RemoteTargetHandle handle : mRemoteTargetHandles) {
+            consumer.accept(handle);
+        }
+    }
+
+    private TaskViewSimulator createTaskViewSimulator() {
+        TaskViewSimulator tvs = new TaskViewSimulator(getContext(), getSizeStrategy());
+        tvs.setOrientationState(mOrientationState);
+        tvs.setDp(mActivity.getDeviceProfile());
+        tvs.recentsViewScale.value = 1;
+        return tvs;
     }
 
     public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
@@ -4114,14 +4307,42 @@
             }
             return;
         }
-        int runningTaskId = getTaskIdsForRunningTaskView()[0];
-        switchToScreenshot(mRunningTaskViewId == -1 ? null
-                : mRecentsAnimationController.screenshotTask(runningTaskId), onFinishRunnable);
+
+        switchToScreenshotInternal(onFinishRunnable);
+    }
+
+    private void switchToScreenshotInternal(Runnable onFinishRunnable) {
+        TaskView taskView = getRunningTaskView();
+        if (taskView == null) {
+            onFinishRunnable.run();
+            return;
+        }
+
+        taskView.setShowScreenshot(true);
+        for (TaskView.TaskIdAttributeContainer container :
+                taskView.getTaskIdAttributeContainers()) {
+            if (container == null) {
+                continue;
+            }
+
+            ThumbnailData td =
+                    mRecentsAnimationController.screenshotTask(container.getTask().key.id);
+            TaskThumbnailView thumbnailView = container.getThumbnailView();
+            if (td != null) {
+                thumbnailView.setThumbnail(container.getTask(), td);
+            } else {
+                thumbnailView.refresh();
+            }
+        }
+        ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
     }
 
     /**
      * Switch the current running task view to static snapshot mode, using the
      * provided thumbnail data as the snapshot.
+     * TODO(b/195609063) Consolidate this method w/ the one above, except this thumbnail data comes
+     *  from gesture state, which is a larger change of it having to keep track of multiple tasks.
+     *  OR. Maybe it doesn't need to pass in a thumbnail and we can use the exact same flow as above
      */
     public void switchToScreenshot(ThumbnailData thumbnailData, Runnable onFinishRunnable) {
         TaskView taskView = getRunningTaskView();
@@ -4274,7 +4495,8 @@
     }
 
     private void dispatchScrollChanged() {
-        mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
+        runActionOnRemoteHandles(remoteTargetHandle ->
+                remoteTargetHandle.mTaskViewSimulator.setScroll(getScrollOffset()));
         for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
             mScrollListeners.get(i).onScrollChanged();
         }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 2c33b6d..edd09db 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -91,6 +91,7 @@
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.SwipeUpAnimationLogic.RemoteTargetHandle;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
@@ -100,16 +101,20 @@
 import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.lang.annotation.Retention;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.stream.Stream;
 
 /**
  * A task in the Recents view.
@@ -329,8 +334,8 @@
 
     private final TaskOutlineProvider mOutlineProvider;
 
-    private Task mTask;
-    private TaskThumbnailView mSnapshotView;
+    protected Task mTask;
+    protected TaskThumbnailView mSnapshotView;
     private IconView mIconView;
     private final DigitalWellBeingToast mDigitalWellBeingToast;
     private float mFullscreenProgress;
@@ -338,7 +343,7 @@
     private float mNonGridScale = 1;
     private float mDismissScale = 1;
     private final FullscreenDrawParams mCurrentFullscreenParams;
-    private final StatefulActivity mActivity;
+    protected final StatefulActivity mActivity;
 
     // Various causes of changing primary translation, which we aggregate to setTranslationX/Y().
     private float mDismissTranslationX;
@@ -367,7 +372,12 @@
     private float mStableAlpha = 1;
 
     private int mTaskViewId = -1;
-    private final int[] mTaskIdContainer = new int[]{-1, -1};
+    /**
+     * Index 0 will contain taskID of left/top task, index 1 will contain taskId of bottom/right
+     */
+    protected final int[] mTaskIdContainer = new int[]{-1, -1};
+    protected final TaskIdAttributeContainer[] mTaskIdAttributeContainer =
+            new TaskIdAttributeContainer[2];
 
     private boolean mShowScreenshot;
 
@@ -518,10 +528,15 @@
         cancelPendingLoadTasks();
         mTask = task;
         mTaskIdContainer[0] = mTask.key.id;
+        mTaskIdAttributeContainer[0] = new TaskIdAttributeContainer(task, mSnapshotView);
         mSnapshotView.bind(task);
         setOrientationState(orientedState);
     }
 
+    public TaskIdAttributeContainer[] getTaskIdAttributeContainers() {
+        return mTaskIdAttributeContainer;
+    }
+
     public Task getTask() {
         return mTask;
     }
@@ -563,7 +578,29 @@
 
             mIsClickableAsLiveTile = false;
             RecentsView recentsView = getRecentsView();
-            final RemoteAnimationTargets targets = recentsView.getLiveTileParams().getTargetSet();
+            RemoteAnimationTargets targets;
+            RemoteTargetHandle[] remoteTargetHandles =
+                    recentsView.mRemoteTargetHandles;
+            if (remoteTargetHandles.length == 1) {
+                targets = remoteTargetHandles[0].mTransformParams.getTargetSet();
+            } else {
+                TransformParams topLeftParams = remoteTargetHandles[0].mTransformParams;
+                TransformParams rightBottomParams = remoteTargetHandles[1].mTransformParams;
+                RemoteAnimationTargetCompat[] apps = Stream.concat(
+                        Arrays.stream(topLeftParams.getTargetSet().apps),
+                        Arrays.stream(rightBottomParams.getTargetSet().apps))
+                        .toArray(RemoteAnimationTargetCompat[]::new);
+                RemoteAnimationTargetCompat[] wallpapers = Stream.concat(
+                        Arrays.stream(topLeftParams.getTargetSet().wallpapers),
+                        Arrays.stream(rightBottomParams.getTargetSet().wallpapers))
+                        .toArray(RemoteAnimationTargetCompat[]::new);
+                RemoteAnimationTargetCompat[] nonApps = Stream.concat(
+                        Arrays.stream(topLeftParams.getTargetSet().nonApps),
+                        Arrays.stream(rightBottomParams.getTargetSet().nonApps))
+                        .toArray(RemoteAnimationTargetCompat[]::new);
+                targets = new RemoteAnimationTargets(apps, wallpapers, nonApps,
+                        topLeftParams.getTargetSet().targetMode);
+            }
             if (targets == null) {
                 // If the recents animation is cancelled somehow between the parent if block and
                 // here, try to launch the task as a non live tile task.
@@ -723,11 +760,11 @@
         }
     }
 
-    private boolean needsUpdate(@TaskDataChanges int dataChange, @TaskDataChanges int flag) {
+    protected boolean needsUpdate(@TaskDataChanges int dataChange, @TaskDataChanges int flag) {
         return (dataChange & flag) == flag;
     }
 
-    private void cancelPendingLoadTasks() {
+    protected void cancelPendingLoadTasks() {
         if (mThumbnailLoadRequest != null) {
             mThumbnailLoadRequest.cancel();
             mThumbnailLoadRequest = null;
@@ -1509,4 +1546,22 @@
         }
 
     }
+
+    public class TaskIdAttributeContainer {
+        private final TaskThumbnailView thumbnailView;
+        private final Task task;
+
+        public TaskIdAttributeContainer(Task task, TaskThumbnailView thumbnailView) {
+            this.task = task;
+            this.thumbnailView = thumbnailView;
+        }
+
+        public TaskThumbnailView getThumbnailView() {
+            return thumbnailView;
+        }
+
+        public Task getTask() {
+            return task;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index eb058e8..3f6e7cd 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -221,6 +221,7 @@
     // DragController
     public int flingToDeleteThresholdVelocity;
 
+    /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
     DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds,
             boolean isMultiWindowMode, boolean transposeLayoutWithOrientation,
             boolean useTwoPanels) {
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 573c8bd..1f1db9d 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -18,6 +18,8 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.graphics.Rect;
+
 import androidx.annotation.IntDef;
 
 import java.lang.annotation.Retention;
@@ -82,4 +84,25 @@
             mStageType = stageType;
         }
     }
+
+    public static class StagedSplitBounds {
+        public final Rect mLeftTopBounds;
+        public final Rect mRightBottomBounds;
+        public final Rect mDividerBounds;
+
+
+        public StagedSplitBounds(Rect leftTopBounds, Rect rightBottomBounds, Rect dividerBounds) {
+            mLeftTopBounds = leftTopBounds;
+            mRightBottomBounds = rightBottomBounds;
+            mDividerBounds = dividerBounds;
+        }
+    }
+
+    public static class StagedSplitTaskPosition {
+        public int taskId = -1;
+        @StagePosition
+        public int stagePosition = STAGE_POSITION_UNDEFINED;
+        @StageType
+        public int stageType = STAGE_TYPE_UNDEFINED;
+    }
 }