Merge "Unifying swipe handling for fallback launcher" into ub-launcher3-rvc-dev
diff --git a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
index ffe906c..7b3e378 100644
--- a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
+++ b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
@@ -18,6 +18,7 @@
     android:id="@+id/drag_layer"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:clipChildren="false"
     android:fitsSystemWindows="true">
 
     <com.android.quickstep.fallback.FallbackRecentsView
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 11593a1..94c7771 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -49,64 +49,62 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.views.RecentsView;
 
 /**
  * Animation factory for quickstep specific transitions
  */
-public class QuickstepAtomicAnimationFactory extends AtomicAnimationFactory<LauncherState> {
+public class QuickstepAtomicAnimationFactory extends
+        RecentsAtomicAnimationFactory<Launcher, LauncherState> {
 
     // Scale recents takes before animating in
     private static final float RECENTS_PREPARE_SCALE = 1.33f;
 
-    public static final int INDEX_SHELF_ANIM = 0;
-    public static final int INDEX_RECENTS_FADE_ANIM = 1;
-    public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = 2;
-    public static final int INDEX_PAUSE_TO_OVERVIEW_ANIM = 3;
-    private static final int ANIM_COUNT = 4;
+    public static final int INDEX_SHELF_ANIM = RecentsAtomicAnimationFactory.NEXT_INDEX + 0;
+    public static final int INDEX_PAUSE_TO_OVERVIEW_ANIM =
+            RecentsAtomicAnimationFactory.NEXT_INDEX + 1;
+
+    private static final int MY_ANIM_COUNT = 2;
+    protected static final int NEXT_INDEX = RecentsAtomicAnimationFactory.NEXT_INDEX
+            + MY_ANIM_COUNT;
 
     public static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
 
-    private final QuickstepLauncher mLauncher;
-
-    public QuickstepAtomicAnimationFactory(QuickstepLauncher launcher) {
-        super(ANIM_COUNT);
-        mLauncher = launcher;
+    public QuickstepAtomicAnimationFactory(QuickstepLauncher activity) {
+        super(activity, MY_ANIM_COUNT);
     }
 
     @Override
     public Animator createStateElementAnimation(int index, float... values) {
         switch (index) {
             case INDEX_SHELF_ANIM: {
-                AllAppsTransitionController aatc = mLauncher.getAllAppsController();
+                AllAppsTransitionController aatc = mActivity.getAllAppsController();
                 Animator springAnim = aatc.createSpringAnimation(values);
 
-                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+                if ((OVERVIEW.getVisibleElements(mActivity) & HOTSEAT_ICONS) != 0) {
                     // Translate hotseat with the shelf until reaching overview.
-                    float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
-                    ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mLauncher);
+                    float overviewProgress = OVERVIEW.getVerticalProgress(mActivity);
+                    ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mActivity);
                     float shiftRange = aatc.getShiftRange();
                     if (values.length == 1) {
                         values = new float[] {aatc.getProgress(), values[0]};
@@ -114,9 +112,9 @@
                     ValueAnimator hotseatAnim = ValueAnimator.ofFloat(values);
                     hotseatAnim.addUpdateListener(anim -> {
                         float progress = (Float) anim.getAnimatedValue();
-                        if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
+                        if (progress >= overviewProgress || mActivity.isInState(BACKGROUND_APP)) {
                             float hotseatShift = (progress - overviewProgress) * shiftRange;
-                            mLauncher.getHotseat().setTranslationY(hotseatShift + sat.translationY);
+                            mActivity.getHotseat().setTranslationY(hotseatShift + sat.translationY);
                         }
                     });
                     hotseatAnim.setInterpolator(LINEAR);
@@ -130,34 +128,21 @@
 
                 return springAnim;
             }
-            case INDEX_RECENTS_FADE_ANIM:
-                return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
-                        RecentsView.CONTENT_ALPHA, values);
-            case INDEX_RECENTS_TRANSLATE_X_ANIM: {
-                RecentsView rv = mLauncher.getOverviewPanel();
-                return new SpringAnimationBuilder(mLauncher)
-                        .setMinimumVisibleChange(1f / rv.getPageOffsetScale())
-                        .setDampingRatio(0.8f)
-                        .setStiffness(250)
-                        .setValues(values)
-                        .build(rv, ADJACENT_PAGE_OFFSET);
-            }
             case INDEX_PAUSE_TO_OVERVIEW_ANIM: {
                 StateAnimationConfig config = new StateAnimationConfig();
                 config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
 
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
                 config.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_3);
-                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+                if ((OVERVIEW.getVisibleElements(mActivity) & HOTSEAT_ICONS) != 0) {
                     config.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
                     config.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
                 }
 
-                StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+                StateManager<LauncherState> stateManager = mActivity.getStateManager();
                 return stateManager.createAtomicAnimation(
                         stateManager.getCurrentStableState(), OVERVIEW, config);
             }
-
             default:
                 return super.createStateElementAnimation(index, values);
         }
@@ -172,7 +157,7 @@
             config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL);
             config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
-            Workspace workspace = mLauncher.getWorkspace();
+            Workspace workspace = mActivity.getWorkspace();
 
             // Start from a higher workspace scale, but only if we're invisible so we don't jump.
             boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
@@ -186,13 +171,13 @@
                 workspace.setScaleX(0.92f);
                 workspace.setScaleY(0.92f);
             }
-            Hotseat hotseat = mLauncher.getHotseat();
+            Hotseat hotseat = mActivity.getHotseat();
             boolean isHotseatVisible = hotseat.getVisibility() == VISIBLE && hotseat.getAlpha() > 0;
             if (!isHotseatVisible) {
                 hotseat.setScaleX(0.92f);
                 hotseat.setScaleY(0.92f);
                 if (ENABLE_OVERVIEW_ACTIONS.get()) {
-                    AllAppsContainerView qsbContainer = mLauncher.getAppsView();
+                    AllAppsContainerView qsbContainer = mActivity.getAppsView();
                     View qsb = qsbContainer.getSearchView();
                     boolean qsbVisible = qsb.getVisibility() == VISIBLE && qsb.getAlpha() > 0;
                     if (!qsbVisible) {
@@ -209,7 +194,7 @@
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
             config.setInterpolator(ANIM_OVERVIEW_SCRIM_FADE, FAST_OUT_SLOW_IN);
         } else if ((fromState == NORMAL || fromState == HINT_STATE) && toState == OVERVIEW) {
-            if (SysUINavigationMode.getMode(mLauncher) == NO_BUTTON) {
+            if (SysUINavigationMode.getMode(mActivity) == NO_BUTTON) {
                 config.setInterpolator(ANIM_WORKSPACE_SCALE,
                         fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
                 config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
@@ -217,7 +202,7 @@
                 config.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
 
                 // Scale up the recents, if it is not coming from the side
-                RecentsView overview = mLauncher.getOverviewPanel();
+                RecentsView overview = mActivity.getOverviewPanel();
                 if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
                     SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
                 }
@@ -225,7 +210,7 @@
             config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
             config.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
             Interpolator translationInterpolator = ENABLE_OVERVIEW_ACTIONS.get()
-                    && removeShelfFromOverview(mLauncher)
+                    && removeShelfFromOverview(mActivity)
                     ? OVERSHOOT_1_2
                     : OVERSHOOT_1_7;
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index dc8fb9e..f4d1629 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -77,7 +77,6 @@
                     controller.dispatchOnStart();
                     controller.getAnimationPlayer().end();
                 });
-        factory.onRemoteAnimationReceived(null);
         factory.createActivityInterface(RECENTS_LAUNCH_DURATION);
         factory.setRecentsAttachedToAppWindow(true, false);
         mActivity = activity;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 66fefbe..d957418 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -55,7 +55,6 @@
 import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
@@ -511,8 +510,8 @@
 
     public interface Factory {
 
-        BaseSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs,
-                boolean continuingLastGesture, boolean isLikelyToStartNewTask);
+        BaseSwipeUpHandler newHandler(
+                GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
     }
 
     protected interface RunningWindowAnim {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
similarity index 91%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
index a8fa630..343f28a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
@@ -38,29 +37,24 @@
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
-import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
-import android.graphics.RectF;
 import android.os.Build;
 import android.os.SystemClock;
-import android.os.UserHandle;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.WindowInsets;
 import android.view.animation.Interpolator;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -72,7 +66,6 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.BaseActivityInterface.AnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
@@ -80,7 +73,6 @@
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.util.TransformParams.TargetAlphaProvider;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
@@ -92,11 +84,12 @@
 
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
+ * TODO: Merge this with BaseSwipeUpHandler
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class LauncherSwipeHandler extends BaseSwipeUpHandler<Launcher, RecentsView>
-        implements OnApplyWindowInsetsListener {
-    private static final String TAG = LauncherSwipeHandler.class.getSimpleName();
+public abstract class BaseSwipeUpHandlerV2<T extends StatefulActivity<?>, Q extends RecentsView>
+        extends BaseSwipeUpHandler<T, Q> implements OnApplyWindowInsetsListener {
+    private static final String TAG = BaseSwipeUpHandlerV2.class.getSimpleName();
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
@@ -108,9 +101,11 @@
     }
 
     // Launcher UI related states
-    private static final int STATE_LAUNCHER_PRESENT = getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
-    private static final int STATE_LAUNCHER_STARTED = getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
-    private static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
+    protected static final int STATE_LAUNCHER_PRESENT =
+            getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
+    protected static final int STATE_LAUNCHER_STARTED =
+            getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
+    protected static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
 
     // Internal initialization states
     private static final int STATE_APP_CONTROLLER_RECEIVED =
@@ -122,7 +117,7 @@
     private static final int STATE_SCALED_CONTROLLER_RECENTS =
             getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
 
-    private static final int STATE_HANDLER_INVALIDATED =
+    protected static final int STATE_HANDLER_INVALIDATED =
             getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
     private static final int STATE_GESTURE_STARTED =
             getFlagForIndex(7, "STATE_GESTURE_STARTED");
@@ -163,7 +158,7 @@
      */
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
-    private final TaskAnimationManager mTaskAnimationManager;
+    protected final TaskAnimationManager mTaskAnimationManager;
 
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim mRunningWindowAnim;
@@ -193,7 +188,7 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
-    public LauncherSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+    public BaseSwipeUpHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
@@ -222,9 +217,6 @@
                         | STATE_GESTURE_CANCELLED,
                 this::resetStateForAnimationCancel);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
-                this::sendRemoteAnimationsToAnimationFactory);
-
         mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
         mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
@@ -272,7 +264,7 @@
     @Override
     protected boolean onActivityInit(Boolean alreadyOnHome) {
         super.onActivityInit(alreadyOnHome);
-        final Launcher activity = mActivityInterface.getCreatedActivity();
+        final T activity = mActivityInterface.getCreatedActivity();
         if (mActivity == activity) {
             return true;
         }
@@ -323,7 +315,7 @@
     }
 
     private void onLauncherStart() {
-        final Launcher activity = mActivityInterface.getCreatedActivity();
+        final T activity = mActivityInterface.getCreatedActivity();
         if (mActivity != activity) {
             return;
         }
@@ -411,6 +403,10 @@
             updateSysUiFlags(mCurrentShift.value);
             return;
         }
+        notifyGestureAnimationStartToRecents();
+    }
+
+    protected void notifyGestureAnimationStartToRecents() {
         mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
     }
 
@@ -418,10 +414,6 @@
         mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
     }
 
-    private void sendRemoteAnimationsToAnimationFactory() {
-        mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationTargets);
-    }
-
     private void initializeLauncherAnimationController() {
         buildAnimationController();
 
@@ -633,7 +625,7 @@
      */
     @UiThread
     private void notifyGestureStartedAsync() {
-        final Launcher curActivity = mActivity;
+        final T curActivity = mActivity;
         if (curActivity != null) {
             // Once the gesture starts, we can no longer transition home through the button, so
             // reset the force override of the activity visibility
@@ -681,7 +673,7 @@
         endLauncherTransitionController();
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // Hide the task view, if not already hidden
-            setTargetAlphaProvider(LauncherSwipeHandler::getHiddenTargetAlpha);
+            setTargetAlphaProvider(BaseSwipeUpHandlerV2::getHiddenTargetAlpha);
         }
 
         StatefulActivity activity = mActivityInterface.getCreatedActivity();
@@ -911,6 +903,8 @@
                 interpolator, target, velocityPxPerMs));
     }
 
+    protected abstract HomeAnimationFactory createHomeAnimationFactory(long duration);
+
     @UiThread
     private void animateToProgressInternal(float start, float end, long duration,
             Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
@@ -919,67 +913,7 @@
         maybeUpdateRecentsAttachedState();
 
         if (mGestureState.getEndTarget() == HOME) {
-            HomeAnimationFactory homeAnimFactory;
-            if (mActivity != null) {
-                final TaskView runningTaskView = mRecentsView.getRunningTaskView();
-                final View workspaceView;
-                if (runningTaskView != null
-                        && runningTaskView.getTask().key.getComponent() != null) {
-                    workspaceView = mActivity.getWorkspace().getFirstMatchForAppClose(
-                            runningTaskView.getTask().key.getComponent().getPackageName(),
-                            UserHandle.of(runningTaskView.getTask().key.userId));
-                } else {
-                    workspaceView = null;
-                }
-                final RectF iconLocation = new RectF();
-                boolean canUseWorkspaceView =
-                        workspaceView != null && workspaceView.isAttachedToWindow();
-                FloatingIconView floatingIconView = canUseWorkspaceView
-                        ? FloatingIconView.getFloatingIconView(mActivity, workspaceView,
-                        true /* hideOriginal */, iconLocation, false /* isOpening */)
-                        : null;
-
-                mActivity.getRootView().setForceHideBackArrow(true);
-                mActivityInterface.setHintUserWillBeActive();
-
-                homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
-
-                    @Override
-                    public RectF getWindowTargetRect() {
-                        if (canUseWorkspaceView) {
-                            return iconLocation;
-                        } else {
-                            return super.getWindowTargetRect();
-                        }
-                    }
-
-                    @NonNull
-                    @Override
-                    public AnimatorPlaybackController createActivityAnimationToHome() {
-                        // Return an empty APC here since we have an non-user controlled animation
-                        // to home.
-                        long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
-                        return mActivity.getStateManager().createAnimationToNewWorkspace(
-                                NORMAL, accuracy, 0 /* animComponents */);
-                    }
-
-                    @Override
-                    public void playAtomicAnimation(float velocity) {
-                        new StaggeredWorkspaceAnim(mActivity, velocity,
-                                true /* animateOverviewScrim */).start();
-                    }
-                };
-
-            } else {
-                homeAnimFactory = new HomeAnimationFactory(null) {
-                    @Override
-                    public AnimatorPlaybackController createActivityAnimationToHome() {
-                        return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
-                    }
-                };
-                mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
-                        isPresent -> mRecentsView.startHome());
-            }
+            HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
             RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
             windowAnim.addAnimatorListener(new AnimationSuccessListener() {
                 @Override
@@ -1303,14 +1237,15 @@
             // If there are no targets or the animation not started, then there is nothing to finish
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
         } else {
-            mRecentsAnimationController.finish(true /* toRecents */,
-                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED),
-                    true /* sendUserLeaveHint */);
+            finishRecentsControllerToHome(
+                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
         }
         ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
         doLogGesture(HOME);
     }
 
+    protected abstract void finishRecentsControllerToHome(Runnable callback);
+
     private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
         endLauncherTransitionController();
         mActivityInterface.onSwipeUpToRecentsComplete();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 8dfe75e..7d08fac 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -15,27 +15,21 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
-import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 
 import android.content.Context;
-import android.graphics.PointF;
 import android.graphics.Rect;
+import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.views.RecentsView;
@@ -55,9 +49,10 @@
     public static final FallbackActivityInterface INSTANCE = new FallbackActivityInterface();
 
     private FallbackActivityInterface() {
-        super(false);
+        super(false, DEFAULT, BACKGROUND_APP);
     }
 
+    /** 2 */
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
             PagedOrientationHandler orientationHandler) {
@@ -72,6 +67,13 @@
         }
     }
 
+    /** 4 */
+    @Override
+    public void onSwipeUpToHomeComplete() {
+        onSwipeUpToRecentsComplete();
+    }
+
+    /** 5 */
     @Override
     public void onAssistantVisibilityChanged(float visibility) {
         // This class becomes active when the screen is locked.
@@ -79,51 +81,13 @@
         // set to zero prior to this class becoming active.
     }
 
+    /** 6 */
     @Override
     public AnimationFactory prepareRecentsUI(
             boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity == null) {
-            return (transitionLength) -> { };
-        }
-
-        activity.getStateManager().goToState(BACKGROUND_APP);
-        FallbackRecentsView rv = activity.getOverviewPanel();
-        rv.setContentAlpha(0);
-
-        return new AnimationFactory() {
-
-            boolean isAnimatingToRecents = false;
-
-            @Override
-            public void onRemoteAnimationReceived(RemoteAnimationTargets targets) {
-                isAnimatingToRecents = targets != null && targets.isAnimatingHome();
-                if (!isAnimatingToRecents) {
-                    rv.setContentAlpha(1);
-                }
-                createActivityInterface(getSwipeUpDestinationAndLength(
-                        activity.getDeviceProfile(), activity, new Rect(),
-                        rv.getPagedOrientationHandler()));
-            }
-
-            @Override
-            public void createActivityInterface(long transitionLength) {
-                PendingAnimation pa = new PendingAnimation(transitionLength * 2);
-
-                if (isAnimatingToRecents) {
-                    pa.addFloat(rv, CONTENT_ALPHA, 0, 1, LINEAR);
-                }
-
-                pa.addFloat(rv, SCALE_PROPERTY, rv.getMaxScaleForFullScreen(), 1, LINEAR);
-                pa.addFloat(rv, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
-                AnimatorPlaybackController controller = pa.createPlaybackController();
-
-                // Since we are changing the start position of the UI, reapply the state, at the end
-                controller.setEndAction(() -> activity.getStateManager().goToState(
-                        controller.getInterpolatedProgress() > 0.5 ? DEFAULT : BACKGROUND_APP));
-                callback.accept(controller);
-            }
-        };
+        DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
+        factory.initUI();
+        return factory;
     }
 
     @Override
@@ -167,6 +131,15 @@
     }
 
     @Override
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        // In non-gesture mode, user might be clicking on the home button which would directly
+        // start the home activity instead of going through recents. In that case, defer starting
+        // recents until we are sure it is a gesture.
+        return !deviceState.isFullyGesturalNavMode()
+                || super.deferStartingActivity(deviceState, ev);
+    }
+
+    @Override
     public int getContainerType() {
         RecentsActivity activity = getCreatedActivity();
         boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
@@ -191,11 +164,6 @@
     }
 
     @Override
-    public void getMultiWindowSize(Context context, DeviceProfile dp, PointF out) {
-        out.set(dp.widthPx, dp.heightPx);
-    }
-
-    @Override
     protected float getExtraSpace(Context context, DeviceProfile dp,
             PagedOrientationHandler orientationHandler) {
         return showOverviewActions(context)
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index db41bd6..7b614c2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -15,537 +15,117 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
-import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
-import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
-import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
-import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
-import android.animation.Animator;
-import android.animation.AnimatorSet;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.PointF;
-import android.os.Bundle;
-import android.util.ArrayMap;
-import android.view.MotionEvent;
 
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.util.ObjectWrapper;
-import com.android.quickstep.BaseActivityInterface.AnimationFactory;
-import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.quickstep.util.TransformParams;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
 
 /**
  * Handles the navigation gestures when a 3rd party launcher is the default home activity.
  */
-public class FallbackSwipeHandler extends BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
+public class FallbackSwipeHandler extends
+        BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
 
-    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[5] : null;
-
-    private static int getFlagForIndex(int index, String name) {
-        if (DEBUG_STATES) {
-            STATE_NAMES[index] = name;
-        }
-        return 1 << index;
-    }
-
-    private static final int STATE_RECENTS_PRESENT =
-            getFlagForIndex(0, "STATE_RECENTS_PRESENT");
-    private static final int STATE_HANDLER_INVALIDATED =
-            getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
-
-    private static final int STATE_GESTURE_CANCELLED =
-            getFlagForIndex(2, "STATE_GESTURE_CANCELLED");
-    private static final int STATE_GESTURE_COMPLETED =
-            getFlagForIndex(3, "STATE_GESTURE_COMPLETED");
-    private static final int STATE_APP_CONTROLLER_RECEIVED =
-            getFlagForIndex(4, "STATE_APP_CONTROLLER_RECEIVED");
-
-    public static class EndTargetAnimationParams {
-        private final float mEndProgress;
-        private final long mDurationMultiplier;
-        private final float mLauncherAlpha;
-
-        EndTargetAnimationParams(float endProgress, long durationMultiplier, float launcherAlpha) {
-            mEndProgress = endProgress;
-            mDurationMultiplier = durationMultiplier;
-            mLauncherAlpha = launcherAlpha;
-        }
-    }
-    private final ArrayMap<GestureEndTarget, EndTargetAnimationParams>
-            mEndTargetAnimationParams = new ArrayMap();
-
-    private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
-
-    private boolean mOverviewThresholdPassed = false;
-
-    private final boolean mInQuickSwitchMode;
-    private final boolean mContinuingLastGesture;
+    private FallbackHomeAnimationFactory mActiveAnimationFactory;
     private final boolean mRunningOverHome;
-    private final boolean mSwipeUpOverHome;
-    private boolean mTouchedHomeDuringTransition;
-
-    private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
-    private RunningWindowAnim mFinishAnimation;
-
-    // Used to control Recents components throughout the swipe gesture.
-    private AnimatorPlaybackController mLauncherTransitionController;
-    private boolean mHasLauncherTransitionControllerStarted;
-
-    private AnimationFactory mAnimationFactory = (t) -> { };
 
     public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer,
-            boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, deviceState, gestureState, inputConsumer);
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer);
 
-        mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
-        mContinuingLastGesture = continuingLastGesture;
         mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
-        mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
-
-        // Keep the home launcher invisible until we decide to land there.
-        mLauncherAlpha.value = mRunningOverHome ? 1 : 0;
-        if (mSwipeUpOverHome) {
-            mTransformParams.setBaseAlphaCallback((t, a) -> 1 - mLauncherAlpha.value);
-        } else {
-            mTransformParams.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
-        }
-
-        // Going home has an extra long progress to ensure that it animates into the screen
-        mEndTargetAnimationParams.put(HOME, new EndTargetAnimationParams(3, 100, 1));
-        mEndTargetAnimationParams.put(RECENTS, new EndTargetAnimationParams(1, 300, 0));
-        mEndTargetAnimationParams.put(LAST_TASK, new EndTargetAnimationParams(0, 150, 1));
-        mEndTargetAnimationParams.put(NEW_TASK, new EndTargetAnimationParams(0, 150, 1));
-
-        initAfterSubclassConstructor();
-        initStateCallbacks();
-    }
-
-    private void initStateCallbacks() {
-        mStateCallback = new MultiStateCallback(STATE_NAMES);
-
-        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED,
-                this::onHandlerInvalidated);
-        mStateCallback.runOnceAtState(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
-                this::onHandlerInvalidatedWithRecents);
-
-        mStateCallback.runOnceAtState(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
-                this::finishAnimationTargetSetAnimationComplete);
-
-        if (mInQuickSwitchMode) {
-            mStateCallback.runOnceAtState(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
-                            | STATE_RECENTS_PRESENT,
-                    this::finishAnimationTargetSet);
-        } else {
-            mStateCallback.runOnceAtState(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
-                    this::finishAnimationTargetSet);
-        }
-    }
-
-    private void onLauncherAlphaChanged() {
-        if (mRecentsAnimationTargets != null && mGestureState.getEndTarget() == null) {
-            applyWindowTransform();
-        }
     }
 
     @Override
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
-        super.onActivityInit(alreadyOnHome);
-        mActivity = mActivityInterface.getCreatedActivity();
-        mRecentsView = mActivity.getOverviewPanel();
-        mRecentsView.setOnPageTransitionEndCallback(null);
-        linkRecentsViewScroll();
-        if (!mContinuingLastGesture) {
-            if (mRunningOverHome) {
-                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
-            } else {
-                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
-            }
-        }
-        mStateCallback.setStateOnUiThread(STATE_RECENTS_PRESENT);
-        mDeviceState.enableMultipleRegions(false);
-
-        mAnimationFactory = mActivityInterface.prepareRecentsUI(alreadyOnHome,
-                this::onAnimatorPlaybackControllerCreated);
-        mAnimationFactory.createActivityInterface(mTransitionDragLength);
-        return true;
-    }
-
-    @Override
-    protected void initTransitionEndpoints(DeviceProfile dp) {
-        super.initTransitionEndpoints(dp);
-        if (canCreateNewOrUpdateExistingLauncherTransitionController()) {
-            mAnimationFactory.createActivityInterface(mTransitionDragLength);
-        }
-    }
-
-    private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
-        mLauncherTransitionController = anim;
-        mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mLauncherTransitionController.dispatchOnStart();
-        updateLauncherTransitionProgress();
-    }
-
-    private void updateLauncherTransitionProgress() {
-        if (mLauncherTransitionController == null
-                || !canCreateNewOrUpdateExistingLauncherTransitionController()) {
-            return;
-        }
-        // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
-        // anyway. The controller mimics the drag length factor by applying it to its interpolators.
-        float progress = mCurrentShift.value / mDragLengthFactor;
-        mLauncherTransitionController.setPlayFraction(progress);
-    }
-
-    /**
-     * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
-     * (it has its own animation) or if we're already animating the current controller.
-     * @return Whether we can create the launcher controller or update its progress.
-     */
-    private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
-        return mGestureState.getEndTarget() != HOME && !mHasLauncherTransitionControllerStarted;
-    }
-
-    @Override
-    protected boolean moveWindowWithRecentsScroll() {
-        return mInQuickSwitchMode;
-    }
-
-    @Override
-    public void initWhenReady(Intent intent) {
-        if (mInQuickSwitchMode) {
-            // Only init if we are in quickswitch mode
-            super.initWhenReady(intent);
-        }
-    }
-
-    @Override
-    public void updateDisplacement(float displacement) {
-        if (!mInQuickSwitchMode) {
-            super.updateDisplacement(displacement);
-        }
-    }
-
-    @Override
-    protected InputConsumer createNewInputProxyHandler() {
-        // Just consume all input on the active task
-        return new InputConsumer() {
-            @Override
-            public int getType() {
-                return InputConsumer.TYPE_NO_OP;
-            }
-
-            @Override
-            public void onMotionEvent(MotionEvent ev) {
-                mTouchedHomeDuringTransition = true;
-            }
-        };
-    }
-
-    @Override
-    public void onMotionPauseChanged(boolean isPaused) {
-        if (!mInQuickSwitchMode && mDeviceState.isFullyGesturalNavMode()) {
-            updateOverviewThresholdPassed(isPaused);
-        }
-    }
-
-    private void updateOverviewThresholdPassed(boolean passed) {
-        if (passed != mOverviewThresholdPassed) {
-            mOverviewThresholdPassed = passed;
-            if (mSwipeUpOverHome) {
-                mLauncherAlpha.animateToValue(mLauncherAlpha.value, passed ? 0 : 1)
-                        .setDuration(150).start();
-            }
-            performHapticFeedback();
-        }
-    }
-
-    @Override
-    public Intent getLaunchIntent() {
-        if (mInQuickSwitchMode || mSwipeUpOverHome || !mDeviceState.isFullyGesturalNavMode()) {
-            return mGestureState.getOverviewIntent();
-        } else {
-            return mGestureState.getHomeIntent();
-        }
-    }
-
-    @Override
-    public void updateFinalShift() {
-        mTransformParams.setProgress(mCurrentShift.value);
-        if (mRecentsAnimationController != null) {
-            boolean swipeUpThresholdPassed = mCurrentShift.value > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
-            mRecentsAnimationController.setUseLauncherSystemBarFlags(mInQuickSwitchMode
-                    || swipeUpThresholdPassed);
-            mRecentsAnimationController.setSplitScreenMinimized(!mInQuickSwitchMode
-                    && swipeUpThresholdPassed);
-        }
-
-        if (!mInQuickSwitchMode && !mDeviceState.isFullyGesturalNavMode()) {
-            updateOverviewThresholdPassed(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW);
-        }
-
-        applyWindowTransform();
-        updateLauncherTransitionProgress();
-    }
-
-    @Override
-    public void onGestureCancelled() {
-        updateDisplacement(0);
-        mGestureState.setEndTarget(LAST_TASK);
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED);
-    }
-
-    @Override
-    public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
-        mEndVelocityPxPerMs.set(0, velocity.y / 1000);
-        if (mInQuickSwitchMode) {
-            // For now set it to non-null, it will be reset before starting the animation
-            mGestureState.setEndTarget(LAST_TASK);
-        } else {
-            float flingThreshold = mContext.getResources()
-                    .getDimension(R.dimen.quickstep_fling_threshold_velocity);
-            boolean isFling = Math.abs(endVelocity) > flingThreshold;
-
-            if (mDeviceState.isFullyGesturalNavMode()) {
-                if (isFling) {
-                    mGestureState.setEndTarget(endVelocity < 0 ? HOME : LAST_TASK);
-                } else if (mOverviewThresholdPassed) {
-                    mGestureState.setEndTarget(RECENTS);
-                } else {
-                    mGestureState.setEndTarget(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW
-                            ? HOME
-                            : LAST_TASK);
-                }
-            } else {
-                GestureEndTarget startState = mSwipeUpOverHome ? HOME : LAST_TASK;
-                if (isFling) {
-                    mGestureState.setEndTarget(endVelocity < 0 ? RECENTS : startState);
-                } else {
-                    mGestureState.setEndTarget(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW
-                            ? RECENTS
-                            : startState);
-                }
-            }
-        }
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
-    }
-
-    @Override
-    public void onConsumerAboutToBeSwitched() {
-        if (mInQuickSwitchMode && mGestureState.getEndTarget() != null) {
-            mGestureState.setEndTarget(NEW_TASK);
-
-            mCanceled = true;
-            mCurrentShift.cancelAnimation();
-            if (mFinishAnimation != null) {
-                mFinishAnimation.cancel();
-            }
-
-            if (mRecentsView != null) {
-                mRecentsView.setOnScrollChangeListener(null);
-            }
-        } else {
-            mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-        }
-    }
-
-    private void onHandlerInvalidated() {
-        mActivityInitListener.unregister();
-        if (mGestureEndCallback != null) {
-            mGestureEndCallback.run();
-        }
-        if (mFinishAnimation != null) {
-            mFinishAnimation.end();
-        }
-    }
-
-    private void onHandlerInvalidatedWithRecents() {
-        mRecentsView.onGestureAnimationEnd();
-        mRecentsView.setDisallowScrollToClearAll(false);
-        mRecentsView.getClearAllButton().setVisibilityAlpha(1);
-    }
-
-    private void finishAnimationTargetSetAnimationComplete() {
-        switch (mGestureState.getEndTarget()) {
-            case HOME: {
-                if (mSwipeUpOverHome) {
-                    mRecentsAnimationController.finish(false, null, false);
-                    // Send a home intent to clear the task stack
-                    mContext.startActivity(mGestureState.getHomeIntent());
-                } else {
-                    // Notify swipe-to-home (recents animation) is finished
-                    SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished();
-                    mRecentsAnimationController.finish(true, () -> {
-                        if (!mTouchedHomeDuringTransition) {
-                            // If the user hasn't interacted with the screen during the transition,
-                            // send a home intent so launcher can go to the default home screen.
-                            // (If they are trying to touch something, we don't want to interfere.)
-                            mContext.startActivity(mGestureState.getHomeIntent());
-                        }
-                    }, true);
-                }
-                break;
-            }
-            case LAST_TASK:
-                mRecentsAnimationController.finish(false, null, false);
-                break;
-            case RECENTS: {
-                if (mSwipeUpOverHome || !mDeviceState.isFullyGesturalNavMode()) {
-                    mRecentsAnimationController.finish(true, null, true);
-                    break;
-                }
-
-                final int runningTaskId = mGestureState.getRunningTaskId();
-                ThumbnailData thumbnail = mRecentsAnimationController.screenshotTask(runningTaskId);
-                mRecentsAnimationController.setDeferCancelUntilNextTransition(true /* defer */,
-                        false /* screenshot */);
-
-                ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
-                ActivityOptionsCompat.setFreezeRecentTasksList(options);
-
-                Bundle extras = new Bundle();
-                extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
-                extras.putInt(EXTRA_TASK_ID, runningTaskId);
-
-                Intent intent = new Intent(mGestureState.getOverviewIntent())
-                        .putExtras(extras);
-                mContext.startActivity(intent, options.toBundle());
-                mRecentsAnimationController.cleanupScreenshot();
-                break;
-            }
-            case NEW_TASK: {
-                startNewTask(success -> { });
-                break;
-            }
-        }
-
-        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-    }
-
-    private void finishAnimationTargetSet() {
-        if (mInQuickSwitchMode) {
-            // Recalculate the end target, some views might have been initialized after
-            // gesture has ended.
-            if (mRecentsView == null || !hasTargets()) {
-                mGestureState.setEndTarget(LAST_TASK);
-            } else {
-                final int runningTaskIndex = getLastAppearedTaskIndex();
-                final int taskToLaunch = mRecentsView.getNextPage();
-                mGestureState.setEndTarget(
-                        (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
-                                ? NEW_TASK
-                                : LAST_TASK);
-            }
-        }
-
-        EndTargetAnimationParams params = mEndTargetAnimationParams.get(mGestureState.getEndTarget());
-        float endProgress = params.mEndProgress;
-        long duration = (long) (params.mDurationMultiplier *
-                Math.abs(endProgress - mCurrentShift.value));
-        if (mRecentsView != null) {
-            duration = Math.max(duration, mRecentsView.getScroller().getDuration());
-        }
-        if (mCurrentShift.value != endProgress || mInQuickSwitchMode) {
-            AnimationSuccessListener endListener = new AnimationSuccessListener() {
-
-                @Override
-                public void onAnimationSuccess(Animator animator) {
-                    if (mRecentsView != null) {
-                        mRecentsView.setOnPageTransitionEndCallback(FallbackSwipeHandler.this
-                                ::finishAnimationTargetSetAnimationComplete);
-                    } else {
-                        finishAnimationTargetSetAnimationComplete();
-                    }
-                    mFinishAnimation = null;
-                }
-            };
-
-            if (mGestureState.getEndTarget() == HOME && !mRunningOverHome) {
-                mRecentsAnimationController.enableInputProxy(mInputConsumer,
-                        this::createNewInputProxyHandler);
-                RectFSpringAnim anim = createWindowAnimationToHome(mCurrentShift.value, duration);
-                anim.addAnimatorListener(endListener);
-                anim.start(mContext, mEndVelocityPxPerMs);
-                mFinishAnimation = RunningWindowAnim.wrap(anim);
-            } else {
-
-                AnimatorSet anim = new AnimatorSet();
-                anim.play(mLauncherAlpha.animateToValue(
-                        mLauncherAlpha.value, params.mLauncherAlpha));
-                anim.play(mCurrentShift.animateToValue(mCurrentShift.value, endProgress));
-
-                anim.setDuration(duration);
-                anim.addListener(endListener);
-                anim.start();
-                mFinishAnimation = RunningWindowAnim.wrap(anim);
-            }
-
-        } else {
-            finishAnimationTargetSetAnimationComplete();
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets) {
-        super.onRecentsAnimationStart(controller, targets);
-        mRecentsAnimationController.enableInputConsumer();
-        applyWindowTransform();
-
-        mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
-    }
-
-    @Override
-    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
-
-        // Defer clearing the controller and the targets until after we've updated the state
-        super.onRecentsAnimationCanceled(thumbnailData);
+    protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
+        mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
+        ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
+        mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
+        return mActiveAnimationFactory;
     }
 
     @Override
     protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
-        return true;
-    }
+        if (mActiveAnimationFactory != null
+                && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
+            mActiveAnimationFactory = null;
+            return false;
+        }
 
-    /**
-     * 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.
-     */
-    private RectFSpringAnim createWindowAnimationToHome(float startProgress, long duration) {
-        HomeAnimationFactory factory = new HomeAnimationFactory(null) {
-            @Override
-            public AnimatorPlaybackController createActivityAnimationToHome() {
-                AnimatorSet anim = new AnimatorSet();
-                Animator fadeInLauncher = mLauncherAlpha.animateToValue(mLauncherAlpha.value, 1);
-                fadeInLauncher.setInterpolator(ACCEL_2);
-                anim.play(fadeInLauncher);
-                anim.setDuration(duration);
-                return AnimatorPlaybackController.wrap(anim, duration);
-            }
-        };
-        return createWindowAnimationToHome(startProgress, factory);
+        return super.handleTaskAppeared(appearedTaskTarget);
     }
 
     @Override
-    protected float getWindowAlpha(float progress) {
-        return 1 - ACCEL_1_5.getInterpolation(progress);
+    protected void finishRecentsControllerToHome(Runnable callback) {
+        mRecentsAnimationController.finish(
+                false /* toRecents */, callback, true /* sendUserLeaveHint */);
+    }
+
+    @Override
+    protected void notifyGestureAnimationStartToRecents() {
+        if (mRunningOverHome) {
+            mRecentsView.onGestureAnimationStartOnHome(mGestureState.getRunningTask());
+        } else {
+            super.notifyGestureAnimationStartToRecents();
+        }
+    }
+
+    private class FallbackHomeAnimationFactory extends HomeAnimationFactory
+            implements TransformParams.BuilderProxy {
+
+        private final TransformParams mHomeAlphaParams = new TransformParams();
+        private final AnimatedFloat mHomeAlpha = new AnimatedFloat(this::updateHomeAlpha);
+
+        private final long mDuration;
+        FallbackHomeAnimationFactory(long duration) {
+            super(null);
+            mDuration = duration;
+        }
+
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            PendingAnimation pa = new PendingAnimation(mDuration);
+            pa.setFloat(mHomeAlpha, AnimatedFloat.VALUE, 1, LINEAR);
+            return pa.createPlaybackController();
+        }
+
+        private void updateHomeAlpha() {
+            mHomeAlphaParams.setProgress(mHomeAlpha.value);
+            if (mHomeAlphaParams.getTargetSet() != null) {
+                mHomeAlphaParams.applySurfaceParams(mHomeAlphaParams.createSurfaceParams(this));
+            }
+        }
+
+        public boolean handleHomeTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+            if (appearedTaskTarget.activityType == ACTIVITY_TYPE_HOME) {
+                RemoteAnimationTargets targets = new RemoteAnimationTargets(
+                        new RemoteAnimationTargetCompat[] {appearedTaskTarget},
+                        new RemoteAnimationTargetCompat[0], appearedTaskTarget.mode);
+                mHomeAlphaParams.setTargetSet(targets);
+                updateHomeAlpha();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onBuildParams(Builder builder, RemoteAnimationTargetCompat app, int targetMode,
+                TransformParams params) { }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 317d4da..dae2f41 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -15,29 +15,18 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
-import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
 import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_SHELF_ANIM;
-import static com.android.quickstep.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.SysUINavigationMode.getMode;
 import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
 import static com.android.quickstep.util.LayoutUtils.getDefaultSwipeHeight;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 
-import android.animation.Animator;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.Log;
-import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.Nullable;
@@ -61,9 +50,7 @@
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.shared.LauncherOverlayManager;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -75,12 +62,12 @@
  * {@link BaseActivityInterface} for the in-launcher recents.
  */
 public final class LauncherActivityInterface extends
-        BaseActivityInterface<LauncherState, Launcher> {
+        BaseActivityInterface<LauncherState, BaseQuickstepLauncher> {
 
     public static final LauncherActivityInterface INSTANCE = new LauncherActivityInterface();
 
     private LauncherActivityInterface() {
-        super(true);
+        super(true, OVERVIEW, BACKGROUND_APP);
     }
 
     @Override
@@ -131,119 +118,43 @@
     @Override
     public AnimationFactory prepareRecentsUI(
             boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
-        BaseQuickstepLauncher launcher = getCreatedActivity();
-        final LauncherState startState = launcher.getStateManager().getState();
-
-        LauncherState resetState = startState;
-        if (startState.shouldDisableRestore()) {
-            resetState = launcher.getStateManager().getRestState();
-        }
-        launcher.getStateManager().setRestState(resetState);
-
-        launcher.getStateManager().goToState(BACKGROUND_APP, false);
-        // Since all apps is not visible, we can safely reset the scroll position.
-        // This ensures then the next swipe up to all-apps starts from scroll 0.
-        launcher.getAppsView().reset(false /* animate */);
-
-        return new AnimationFactory() {
-            private final ShelfPeekAnim mShelfAnim = launcher.getShelfPeekAnim();
-            private boolean mIsAttachedToWindow;
-
-            @Override
-            public void createActivityInterface(long transitionLength) {
-                callback.accept(createBackgroundToOverviewAnim(launcher, transitionLength));
-                // Creating the activity controller animation sometimes reapplies the launcher state
-                // (because we set the animation as the current state animation), so we reapply the
-                // attached state here as well to ensure recents is shown/hidden appropriately.
-                if (SysUINavigationMode.getMode(launcher) == Mode.NO_BUTTON) {
-                    setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
-                }
-            }
-
-            @Override
-            public void onTransitionCancelled() {
-                launcher.getStateManager().goToState(startState, false /* animate */);
-            }
-
+        DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
             @Override
             public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
                     long duration) {
-                mShelfAnim.setShelfState(shelfState, interpolator, duration);
+                mActivity.getShelfPeekAnim().setShelfState(shelfState, interpolator, duration);
             }
 
             @Override
-            public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
-                if (mIsAttachedToWindow == attached && animate) {
-                    return;
-                }
-                mIsAttachedToWindow = attached;
-                LauncherRecentsView recentsView = launcher.getOverviewPanel();
-                Animator fadeAnim = launcher.getStateManager()
-                        .createStateElementAnimation(
-                        INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+            protected void createBackgroundToOverviewAnim(BaseQuickstepLauncher activity,
+                    PendingAnimation pa) {
+                super.createBackgroundToOverviewAnim(activity, pa);
 
-                float fromTranslation = attached ? 1 : 0;
-                float toTranslation = attached ? 0 : 1;
-                launcher.getStateManager()
-                        .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
-                if (!recentsView.isShown() && animate) {
-                    ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
-                } else {
-                    fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
-                }
-                if (!animate) {
-                    ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
-                } else {
-                    launcher.getStateManager().createStateElementAnimation(
-                            INDEX_RECENTS_TRANSLATE_X_ANIM,
-                            fromTranslation, toTranslation).start();
+                if (!activity.getDeviceProfile().isVerticalBarLayout()
+                        && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
+                    // Don't animate the shelf when the mode is NO_BUTTON, because we
+                    // update it atomically.
+                    pa.add(activity.getStateManager().createStateElementAnimation(
+                            INDEX_SHELF_ANIM,
+                            BACKGROUND_APP.getVerticalProgress(activity),
+                            OVERVIEW.getVerticalProgress(activity)));
                 }
 
-                fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
-                fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
+                // Animate the blur and wallpaper zoom
+                float fromDepthRatio = BACKGROUND_APP.getDepth(activity);
+                float toDepthRatio = OVERVIEW.getDepth(activity);
+                pa.addFloat(getDepthController(),
+                        new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
+                        fromDepthRatio, toDepthRatio, LINEAR);
+
             }
         };
-    }
 
-    private AnimatorPlaybackController createBackgroundToOverviewAnim(
-            Launcher activity, long transitionLength) {
-
-        PendingAnimation pa = new PendingAnimation(transitionLength * 2);
-
-        if (!activity.getDeviceProfile().isVerticalBarLayout()
-                && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
-            // Don't animate the shelf when the mode is NO_BUTTON, because we update it atomically.
-            pa.add(activity.getStateManager().createStateElementAnimation(
-                    INDEX_SHELF_ANIM,
-                    BACKGROUND_APP.getVerticalProgress(activity),
-                    OVERVIEW.getVerticalProgress(activity)));
-        }
-
-        // Animate the blur and wallpaper zoom
-        float fromDepthRatio = BACKGROUND_APP.getDepth(activity);
-        float toDepthRatio = OVERVIEW.getDepth(activity);
-        pa.addFloat(getDepthController(), new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
-                fromDepthRatio, toDepthRatio, LINEAR);
-
-
-        //  Scale down recents from being full screen to being in overview.
-        RecentsView recentsView = activity.getOverviewPanel();
-        pa.addFloat(recentsView, SCALE_PROPERTY,
-                BACKGROUND_APP.getOverviewScaleAndOffset(activity)[0],
-                OVERVIEW.getOverviewScaleAndOffset(activity)[0],
-                LINEAR);
-        pa.addFloat(recentsView, FULLSCREEN_PROGRESS,
-                BACKGROUND_APP.getOverviewFullscreenProgress(),
-                OVERVIEW.getOverviewFullscreenProgress(),
-                LINEAR);
-
-        AnimatorPlaybackController controller = pa.createPlaybackController();
-        activity.getStateManager().setCurrentUserControlledAnimation(controller);
-
-        // Since we are changing the start position of the UI, reapply the state, at the end
-        controller.setEndAction(() -> activity.getStateManager().goToState(
-                controller.getInterpolatedProgress() > 0.5 ? OVERVIEW : BACKGROUND_APP, false));
-        return controller;
+        BaseQuickstepLauncher launcher = factory.initUI();
+        // Since all apps is not visible, we can safely reset the scroll position.
+        // This ensures then the next swipe up to all-apps starts from scroll 0.
+        launcher.getAppsView().reset(false /* animate */);
+        return factory;
     }
 
     @Override
@@ -252,6 +163,15 @@
                 onInitListener.test(alreadyOnHome));
     }
 
+    @Override
+    public void setOnDeferredActivityLaunchCallback(Runnable r) {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        launcher.setOnDeferredActivityLaunchCallback(r);
+    }
+
     @Nullable
     @Override
     public BaseQuickstepLauncher getCreatedActivity() {
@@ -259,11 +179,13 @@
     }
 
     @Nullable
-    @UiThread
-    private Launcher getVisibleLauncher() {
-        Launcher launcher = getCreatedActivity();
-        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
-                launcher : null;
+    @Override
+    public DepthController getDepthController() {
+        BaseQuickstepLauncher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return null;
+        }
+        return launcher.getDepthController();
     }
 
     @Nullable
@@ -274,6 +196,14 @@
                 ? launcher.getOverviewPanel() : null;
     }
 
+    @Nullable
+    @UiThread
+    private Launcher getVisibleLauncher() {
+        Launcher launcher = getCreatedActivity();
+        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus()
+                ? launcher : null;
+    }
+
     @Override
     public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
         if (TestProtocol.sDebugTracing) {
@@ -293,15 +223,6 @@
         return true;
     }
 
-    @Override
-    public void setHintUserWillBeActive() {
-        getCreatedActivity().setHintUserWillBeActive();
-    }
-
-    @Override
-    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
-        return deviceState.isInDeferredGestureRegion(ev);
-    }
 
     @Override
     public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
@@ -314,6 +235,16 @@
     }
 
     @Override
+    public void updateOverviewPredictionState() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
+                PredictionUiStateManager.Client.OVERVIEW);
+    }
+
+    @Override
     public int getContainerType() {
         final Launcher launcher = getVisibleLauncher();
         return launcher != null ? launcher.getStateManager().getState().containerType
@@ -351,51 +282,6 @@
     }
 
     @Override
-    public void setOnDeferredActivityLaunchCallback(Runnable r) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        launcher.setOnDeferredActivityLaunchCallback(r);
-    }
-
-    @Override
-    public void updateOverviewPredictionState() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
-                PredictionUiStateManager.Client.OVERVIEW);
-    }
-
-    @Nullable
-    @Override
-    public DepthController getDepthController() {
-        BaseQuickstepLauncher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return null;
-        }
-        return launcher.getDepthController();
-    }
-
-    @Override
-    public void getMultiWindowSize(Context context, DeviceProfile dp, PointF out) {
-        DeviceProfile fullDp = dp.getFullScreenProfile();
-        // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
-        // account for system insets
-        out.set(fullDp.availableWidthPx, fullDp.availableHeightPx);
-        float halfDividerSize = context.getResources()
-                .getDimension(R.dimen.multi_window_task_divider_size) / 2;
-
-        if (fullDp.isLandscape) {
-            out.x = out.x / 2 - halfDividerSize;
-        } else {
-            out.y = out.y / 2 - halfDividerSize;
-        }
-    }
-
-    @Override
     protected float getExtraSpace(Context context, DeviceProfile dp,
             PagedOrientationHandler orientationHandler) {
         if (dp.isVerticalBarLayout() ||
@@ -426,4 +312,5 @@
             }
         }
     }
+
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
new file mode 100644
index 0000000..fa7d268
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.UserHandle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.InputConsumerController;
+
+/**
+ * Temporary class to allow easier refactoring
+ */
+public class LauncherSwipeHandlerV2 extends
+        BaseSwipeUpHandlerV2<BaseQuickstepLauncher, RecentsView> {
+
+    public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer);
+    }
+
+
+    @Override
+    protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
+        HomeAnimationFactory homeAnimFactory;
+        if (mActivity != null) {
+            final TaskView runningTaskView = mRecentsView.getRunningTaskView();
+            final View workspaceView;
+            if (runningTaskView != null
+                    && runningTaskView.getTask().key.getComponent() != null) {
+                workspaceView = mActivity.getWorkspace().getFirstMatchForAppClose(
+                        runningTaskView.getTask().key.getComponent().getPackageName(),
+                        UserHandle.of(runningTaskView.getTask().key.userId));
+            } else {
+                workspaceView = null;
+            }
+            final RectF iconLocation = new RectF();
+            boolean canUseWorkspaceView =
+                    workspaceView != null && workspaceView.isAttachedToWindow();
+            FloatingIconView floatingIconView = canUseWorkspaceView
+                    ? FloatingIconView.getFloatingIconView(mActivity, workspaceView,
+                    true /* hideOriginal */, iconLocation, false /* isOpening */)
+                    : null;
+
+            mActivity.getRootView().setForceHideBackArrow(true);
+            mActivity.setHintUserWillBeActive();
+
+            homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
+
+                @Override
+                public RectF getWindowTargetRect() {
+                    if (canUseWorkspaceView) {
+                        return iconLocation;
+                    } else {
+                        return super.getWindowTargetRect();
+                    }
+                }
+
+                @NonNull
+                @Override
+                public AnimatorPlaybackController createActivityAnimationToHome() {
+                    // Return an empty APC here since we have an non-user controlled animation
+                    // to home.
+                    long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+                    return mActivity.getStateManager().createAnimationToNewWorkspace(
+                            NORMAL, accuracy, 0 /* animComponents */);
+                }
+
+                @Override
+                public void playAtomicAnimation(float velocity) {
+                    new StaggeredWorkspaceAnim(mActivity, velocity,
+                            true /* animateOverviewScrim */).start();
+                }
+            };
+
+        } else {
+            homeAnimFactory = new HomeAnimationFactory(null) {
+                @Override
+                public AnimatorPlaybackController createActivityAnimationToHome() {
+                    return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
+                }
+            };
+            mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+                    isPresent -> mRecentsView.startHome());
+        }
+        return homeAnimFactory;
+    }
+
+    @Override
+    protected void finishRecentsControllerToHome(Runnable callback) {
+        mRecentsAnimationController.finish(
+                true /* toRecents */, callback, true /* sendUserLeaveHint */);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index a4670fd..33b7f12 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.util.ActivityTracker;
@@ -57,6 +58,7 @@
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsRootView;
 import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -345,6 +347,11 @@
         dumpMisc(prefix + "\t", writer);
     }
 
+    @Override
+    public AtomicAnimationFactory<RecentsState> createAtomicAnimationFactory() {
+        return new RecentsAtomicAnimationFactory<>(this, 0);
+    }
+
     private AnimatorListenerAdapter resetStateListener() {
         return new AnimatorListenerAdapter() {
             @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 8bffc78..a0bb631 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -85,7 +85,6 @@
 import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.util.ProtoTracer;
 import com.android.quickstep.util.SplitScreenBounds;
-import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.IOverviewProxy;
@@ -833,16 +832,16 @@
         }
     }
 
-    private BaseSwipeUpHandler createLauncherSwipeHandler(GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+    private BaseSwipeUpHandler createLauncherSwipeHandler(
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+        return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackSwipeHandler(GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return new FallbackSwipeHandler(this, mDeviceState, gestureState,
-                mInputConsumer, isLikelyToStartNewTask, continuingLastGesture);
+    private BaseSwipeUpHandler createFallbackSwipeHandler(
+            GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+        return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+                gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
     protected boolean shouldNotifyBackGesture() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index f958e6d..9242771 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -24,11 +24,13 @@
 import android.os.Build;
 import android.util.AttributeSet;
 
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 
@@ -38,7 +40,7 @@
 public class FallbackRecentsView extends RecentsView<RecentsActivity>
         implements StateListener<RecentsState> {
 
-    private RunningTaskInfo mRunningTaskInfo;
+    private RunningTaskInfo mHomeTaskInfo;
 
     public FallbackRecentsView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -67,16 +69,40 @@
         return false;
     }
 
-    public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
-        mRunningTaskInfo = runningTaskInfo;
-        onGestureAnimationStart(runningTaskInfo == null ? -1 : runningTaskInfo.taskId);
+    /**
+     * When starting gesture interaction from home, we add a temporary invisible tile corresponding
+     * 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;
+        onGestureAnimationStart(homeTaskInfo == null ? -1 : homeTaskInfo.taskId);
+    }
+
+    /**
+     * When the gesture ends and recents view become interactive, we also remove the temporary
+     * invisible tile added for the home task. This also pushes the remaining tiles back
+     * to the center.
+     */
+    @Override
+    public void onGestureAnimationEnd() {
+        super.onGestureAnimationEnd();
+        if (mHomeTaskInfo != null) {
+            TaskView tv = getTaskView(mHomeTaskInfo.taskId);
+            if (tv != null) {
+                PendingAnimation pa = createTaskDismissAnimation(tv, true, false, 150);
+                pa.addEndListener(e -> setCurrentTask(-1));
+                runDismissAnimation(pa);
+            }
+        }
     }
 
     @Override
     public void setCurrentTask(int runningTaskId) {
         super.setCurrentTask(runningTaskId);
-        if (mRunningTaskInfo != null && mRunningTaskInfo.taskId != runningTaskId) {
-            mRunningTaskInfo = null;
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId != runningTaskId) {
+            mHomeTaskInfo = null;
+            setRunningTaskHidden(false);
         }
     }
 
@@ -85,7 +111,7 @@
         // When quick-switching on 3p-launcher, we add a "dummy" 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.
-        if (mRunningTaskInfo != null && mRunningTaskInfo.taskId == mRunningTaskId) {
+        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId) {
             // Check if the task list has running task
             boolean found = false;
             for (Task t : tasks) {
@@ -97,7 +123,7 @@
             if (!found) {
                 ArrayList<Task> newList = new ArrayList<>(tasks.size() + 1);
                 newList.addAll(tasks);
-                newList.add(Task.from(new TaskKey(mRunningTaskInfo), mRunningTaskInfo, false));
+                newList.add(Task.from(new TaskKey(mHomeTaskInfo), mHomeTaskInfo, false));
                 tasks = newList;
             }
         }
@@ -105,6 +131,15 @@
     }
 
     @Override
+    public void setRunningTaskHidden(boolean isHidden) {
+        if (mHomeTaskInfo != null) {
+            // Always keep the home task hidden
+            isHidden = true;
+        }
+        super.setRunningTaskHidden(isHidden);
+    }
+
+    @Override
     public void setModalStateEnabled(boolean isModalState) {
         super.setModalStateEnabled(isModalState);
         if (isModalState) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index abe4af4..3a97216 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -21,7 +21,7 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
-import static com.android.quickstep.LauncherSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.BaseSwipeUpHandlerV2.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 6b0d7a3..ab9ec21 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -207,7 +207,7 @@
                 // Start the window animation on down to give more time for launcher to draw if the
                 // user didn't start the gesture over the back button
                 if (!mIsDeferredDownTarget) {
-                    startTouchTrackingForWindowAnimation(ev.getEventTime(), false);
+                    startTouchTrackingForWindowAnimation(ev.getEventTime());
                 }
 
                 TraceHelper.INSTANCE.endSection(traceToken);
@@ -275,8 +275,7 @@
                         if (mIsDeferredDownTarget) {
                             // Deferred gesture, start the animation and gesture tracking once
                             // we pass the actual touch slop
-                            startTouchTrackingForWindowAnimation(
-                                    ev.getEventTime(), isLikelyToStartNewTask);
+                            startTouchTrackingForWindowAnimation(ev.getEventTime());
                         }
                         if (!mPassedWindowMoveSlop) {
                             mPassedWindowMoveSlop = true;
@@ -326,12 +325,11 @@
         mInteractionHandler.onGestureStarted();
     }
 
-    private void startTouchTrackingForWindowAnimation(
-            long touchTimeMs, boolean isLikelyToStartNewTask) {
+    private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
         ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
 
         mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
-                mTaskAnimationManager.isRecentsAnimationRunning(), isLikelyToStartNewTask);
+                mTaskAnimationManager.isRecentsAnimationRunning());
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
         Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
new file mode 100644
index 0000000..5b0d503
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.quickstep.views.RecentsView;
+
+public class RecentsAtomicAnimationFactory<ACTIVITY_TYPE extends StatefulActivity, STATE_TYPE>
+        extends AtomicAnimationFactory<STATE_TYPE> {
+
+    public static final int INDEX_RECENTS_FADE_ANIM = AtomicAnimationFactory.NEXT_INDEX + 0;
+    public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = AtomicAnimationFactory.NEXT_INDEX + 1;
+
+    private static final int MY_ANIM_COUNT = 2;
+    protected static final int NEXT_INDEX = AtomicAnimationFactory.NEXT_INDEX + MY_ANIM_COUNT;
+
+    protected final ACTIVITY_TYPE mActivity;
+
+    /**
+     * @param extraAnims number of animations supported by the subclass. This should not include
+     *                  the 2 animations supported by this class.
+     */
+    public RecentsAtomicAnimationFactory(ACTIVITY_TYPE activity, int extraAnims) {
+        super(MY_ANIM_COUNT + extraAnims);
+        mActivity = activity;
+    }
+
+    @Override
+    public Animator createStateElementAnimation(int index, float... values) {
+        switch (index) {
+            case INDEX_RECENTS_FADE_ANIM:
+                return ObjectAnimator.ofFloat(mActivity.getOverviewPanel(),
+                        RecentsView.CONTENT_ALPHA, values);
+            case INDEX_RECENTS_TRANSLATE_X_ANIM: {
+                RecentsView rv = mActivity.getOverviewPanel();
+                return new SpringAnimationBuilder(mActivity)
+                        .setMinimumVisibleChange(1f / rv.getPageOffsetScale())
+                        .setDampingRatio(0.8f)
+                        .setStiffness(250)
+                        .setValues(values)
+                        .build(rv, ADJACENT_PAGE_OFFSET);
+            }
+            default:
+                return super.createStateElementAnimation(index, values);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
index 2c85618..fd74357 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
@@ -47,24 +47,21 @@
     private boolean mIsRtl;
 
     private int mScrollOffset;
-    private RecentsView mParent;
 
     public ClearAllButton(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        PagedOrientationHandler orientationHandler = mParent.getPagedOrientationHandler();
-        mScrollOffset = orientationHandler.getClearAllScrollOffset(mParent, mIsRtl);
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+        mScrollOffset = orientationHandler.getClearAllScrollOffset(getRecentsView(), mIsRtl);
     }
 
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mParent = (RecentsView) getParent();
-        mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+    private RecentsView getRecentsView() {
+        return (RecentsView) getParent();
     }
 
     @Override
@@ -94,7 +91,7 @@
 
     @Override
     public void onPageScroll(ScrollState scrollState) {
-        PagedOrientationHandler orientationHandler = mParent.getPagedOrientationHandler();
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
         float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
         if (orientationSize == 0) {
             return;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index aee86db..3273e85 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -17,6 +17,8 @@
 package com.android.quickstep.views;
 
 import static android.view.Surface.ROTATION_0;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
 
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
@@ -1080,9 +1082,9 @@
      * Called when a gesture from an app has finished.
      */
     public void onGestureAnimationEnd() {
+        setOnScrollChangeListener(null);
         setEnableFreeScroll(true);
         setEnableDrawingLiveTile(true);
-        setOnScrollChangeListener(null);
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             setRunningTaskViewShowScreenshot(true);
         }
@@ -1113,6 +1115,12 @@
                     false, true, false, false, new ActivityManager.TaskDescription(), 0,
                     new ComponentName("", ""), 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());
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -1489,7 +1497,7 @@
         return true;
     }
 
-    private void runDismissAnimation(PendingAnimation pendingAnim) {
+    protected void runDismissAnimation(PendingAnimation pendingAnim) {
         AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
         controller.dispatchOnStart();
         controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 19932cb..022977b 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,15 +15,24 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.SysUINavigationMode.getMode;
 import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 
+import android.animation.Animator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Build;
 import android.view.MotionEvent;
@@ -35,6 +44,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -58,11 +68,15 @@
 public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
         ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>> {
 
-    private final PointF mTempPoint = new PointF();
     public final boolean rotationSupportedByActivity;
 
-    protected BaseActivityInterface(boolean rotationSupportedByActivity) {
+    private final STATE_TYPE mOverviewState, mBackgroundState;
+
+    protected BaseActivityInterface(boolean rotationSupportedByActivity,
+            STATE_TYPE overviewState, STATE_TYPE backgroundState) {
         this.rotationSupportedByActivity = rotationSupportedByActivity;
+        mOverviewState = overviewState;
+        mBackgroundState = backgroundState;
     }
 
     public void onTransitionCancelled(boolean activityVisible) {
@@ -87,7 +101,7 @@
         activity.getStateManager().reapplyState();
     }
 
-    public void onSwipeUpToHomeComplete() { }
+    public abstract void onSwipeUpToHomeComplete();
 
     public abstract void onAssistantVisibilityChanged(float visibility);
 
@@ -133,7 +147,7 @@
     public abstract boolean allowMinimizeSplitScreen();
 
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
-        return true;
+        return deviceState.isInDeferredGestureRegion(ev);
     }
 
     /**
@@ -177,13 +191,6 @@
         recentsView.switchToScreenshot(thumbnailData, runnable);
     }
 
-    public void setHintUserWillBeActive() {}
-
-    /**
-     * Sets the expected window size in multi-window mode
-     */
-    public abstract void getMultiWindowSize(Context context, DeviceProfile dp, PointF out);
-
     /**
      * Calculates the taskView size for the provided device configuration
      */
@@ -259,7 +266,7 @@
     /**
      * Calculates the modal taskView size for the provided device configuration
      */
-    public void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
+    public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
         float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
                 ? R.dimen.multi_window_task_card_horz_space
                 : dp.isVerticalBarLayout()
@@ -273,7 +280,7 @@
     }
 
     /** Gets the space that the overview actions will take, including margins. */
-    public float getOverviewActionsHeight(Context context) {
+    public final float getOverviewActionsHeight(Context context) {
         Resources res = context.getResources();
         float actionsBottomMargin = 0;
         if (getMode(context) == Mode.THREE_BUTTONS) {
@@ -290,8 +297,6 @@
 
     public interface AnimationFactory {
 
-        default void onRemoteAnimationReceived(RemoteAnimationTargets targets) { }
-
         void createActivityInterface(long transitionLength);
 
         default void onTransitionCancelled() { }
@@ -307,6 +312,97 @@
         default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
     }
 
+    class DefaultAnimationFactory implements AnimationFactory {
+
+        protected final ACTIVITY_TYPE mActivity;
+        private final STATE_TYPE mStartState;
+        private final Consumer<AnimatorPlaybackController> mCallback;
+
+        private boolean mIsAttachedToWindow;
+
+        DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
+            mCallback = callback;
+
+            mActivity = getCreatedActivity();
+            mStartState = mActivity.getStateManager().getState();
+        }
+
+        protected ACTIVITY_TYPE initUI() {
+            STATE_TYPE resetState = mStartState;
+            if (mStartState.shouldDisableRestore()) {
+                resetState = mActivity.getStateManager().getRestState();
+            }
+            mActivity.getStateManager().setRestState(resetState);
+            mActivity.getStateManager().goToState(mBackgroundState, false);
+            return mActivity;
+        }
+
+        @Override
+        public void createActivityInterface(long transitionLength) {
+            PendingAnimation pa = new PendingAnimation(transitionLength * 2);
+            createBackgroundToOverviewAnim(mActivity, pa);
+            AnimatorPlaybackController controller = pa.createPlaybackController();
+            mActivity.getStateManager().setCurrentUserControlledAnimation(controller);
+
+            // Since we are changing the start position of the UI, reapply the state, at the end
+            controller.setEndAction(() -> mActivity.getStateManager().goToState(
+                    controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
+                    false));
+            mCallback.accept(controller);
+
+            // Creating the activity controller animation sometimes reapplies the launcher state
+            // (because we set the animation as the current state animation), so we reapply the
+            // attached state here as well to ensure recents is shown/hidden appropriately.
+            if (SysUINavigationMode.getMode(mActivity) == Mode.NO_BUTTON) {
+                setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
+            }
+        }
+
+        @Override
+        public void onTransitionCancelled() {
+            mActivity.getStateManager().goToState(mStartState, false /* animate */);
+        }
+
+        @Override
+        public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
+            if (mIsAttachedToWindow == attached && animate) {
+                return;
+            }
+            mIsAttachedToWindow = attached;
+            RecentsView recentsView = mActivity.getOverviewPanel();
+            Animator fadeAnim = mActivity.getStateManager()
+                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+
+            float fromTranslation = attached ? 1 : 0;
+            float toTranslation = attached ? 0 : 1;
+            mActivity.getStateManager()
+                    .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+            if (!recentsView.isShown() && animate) {
+                ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
+            } else {
+                fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
+            }
+            if (!animate) {
+                ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
+            } else {
+                mActivity.getStateManager().createStateElementAnimation(
+                        INDEX_RECENTS_TRANSLATE_X_ANIM,
+                        fromTranslation, toTranslation).start();
+            }
+
+            fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+            fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
+        }
+
+        protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
+            //  Scale down recents from being full screen to being in overview.
+            RecentsView recentsView = activity.getOverviewPanel();
+            pa.addFloat(recentsView, SCALE_PROPERTY,
+                    recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
+            pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
+        }
+    }
+
     protected static boolean showOverviewActions(Context context) {
         return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
     }
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 97dc052..44f7db9 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -554,6 +554,8 @@
      */
     public static class AtomicAnimationFactory<STATE_TYPE> {
 
+        protected static final int NEXT_INDEX = 0;
+
         private final Animator[] mStateElementAnimators;
 
         /**