Launcher side implementation of gesture seekable back to home animation.

This is a two part animation. The first part is an animation that tracks gesture location to scale and move the leaving app window. Once the gesture is committed, the second part takes over the app window and plays the rest of app close transitions in one go.

This animation is used only for apps that enable back dispatching via {@link android.view.OnBackInvokedDispatcher}. The controller registers an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back navigation to launcher starts.

Apps using the legacy back dispatching will keep triggering the WALLPAPER_OPEN remote transition registered in {@link QuickstepTransitionManager}.

Bug: b/195946584
Test: m -j
Test: Swipe back to home on pre-T and T apps, at different life cycle
stages.

Change-Id: I615c5171cd875130f10346fa3ca2a8e9670176cf
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 8f8ac8e..4d4143a 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -109,6 +109,7 @@
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.quickstep.LauncherBackAnimationController;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskViewUtils;
@@ -213,6 +214,7 @@
     private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
     private RemoteTransitionCompat mLauncherOpenTransition;
 
+    private LauncherBackAnimationController mBackAnimationController;
     private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
         @Override
         public void onAnimationStart(Animator animation) {
@@ -238,6 +240,8 @@
         mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS);
         mHandler = new Handler(Looper.getMainLooper());
         mDeviceProfile = mLauncher.getDeviceProfile();
+        mBackAnimationController = new LauncherBackAnimationController(
+                mDeviceProfile, mLauncher, this);
 
         Resources res = mLauncher.getResources();
         mContentScale = res.getFloat(R.dimen.content_scale);
@@ -1136,6 +1140,9 @@
             mLauncherOpenTransition.addHomeOpenCheck(mLauncher.getComponentName());
             SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition);
         }
+        if (mBackAnimationController != null) {
+            mBackAnimationController.registerBackCallbacks(mHandler);
+        }
     }
 
     public void onActivityDestroyed() {
@@ -1171,6 +1178,10 @@
             mLauncherOpenTransition = null;
             mWallpaperOpenTransitionRunner = null;
         }
+        if (mBackAnimationController != null) {
+            mBackAnimationController.unregisterBackCallbacks();
+            mBackAnimationController = null;
+        }
     }
 
     private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
@@ -1323,8 +1334,9 @@
     /**
      * Closing animator that animates the window into its final location on the workspace.
      */
-    private void getClosingWindowAnimators(AnimatorSet animation,
-            RemoteAnimationTargetCompat[] targets, View launcherView, PointF velocityPxPerS) {
+    private RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation,
+            RemoteAnimationTargetCompat[] targets, View launcherView, PointF velocityPxPerS,
+            RectF closingWindowStartRect) {
         FloatingIconView floatingIconView = null;
         FloatingWidgetView floatingWidget = null;
         RectF targetRect = new RectF();
@@ -1356,8 +1368,7 @@
             targetRect.set(getDefaultWindowTargetRect());
         }
 
-        final RectF startRect = new RectF(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx);
-        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mLauncher,
+        RectFSpringAnim anim = new RectFSpringAnim(closingWindowStartRect, targetRect, mLauncher,
                 mDeviceProfile);
 
         // Hook up floating views to the closing window animators.
@@ -1391,7 +1402,7 @@
 
             final float floatingWidgetAlpha = isTransluscent ? 0 : 1;
             FloatingWidgetView finalFloatingWidget = floatingWidget;
-            RectFSpringAnim.OnUpdateListener  runner = new SpringAnimRunner(targets, targetRect,
+            RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect,
                     windowTargetBounds) {
                 @Override
                 public void onUpdate(RectF currentRectF, float progress) {
@@ -1415,6 +1426,7 @@
                 anim.start(mLauncher, velocityPxPerS);
             }
         });
+        return anim;
     }
 
     /**
@@ -1539,6 +1551,97 @@
     }
 
     /**
+     * Creates the {@link RectFSpringAnim} and {@link AnimatorSet} required to animate
+     * the transition.
+     */
+    public Pair<RectFSpringAnim, AnimatorSet> createWallpaperOpenAnimations(
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            boolean fromUnlock,
+            RectF startRect) {
+        AnimatorSet anim = null;
+        RectFSpringAnim rectFSpringAnim = null;
+
+        RemoteAnimationProvider provider = mRemoteAnimationProvider;
+        if (provider != null) {
+            anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
+        }
+
+        if (anim == null) {
+            anim = new AnimatorSet();
+
+            final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
+                    || launcherIsATargetWithMode(appTargets, MODE_OPENING);
+
+            View launcherView = findLauncherView(appTargets);
+            boolean playFallBackAnimation = (launcherView == null
+                    && launcherIsForceInvisibleOrOpening)
+                    || mLauncher.getWorkspace().isOverlayShown()
+                    || hasMultipleTargetsWithMode(appTargets, MODE_CLOSING);
+
+            boolean playWorkspaceReveal = true;
+            boolean skipAllAppsScale = false;
+            if (fromUnlock) {
+                anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
+            } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
+                    && !playFallBackAnimation) {
+                // Use a fixed velocity to start the animation.
+                float velocityPxPerS = DynamicResource.provider(mLauncher)
+                        .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
+                PointF velocity = new PointF(0, -velocityPxPerS);
+                rectFSpringAnim = getClosingWindowAnimators(
+                        anim, appTargets, launcherView, velocity, startRect);
+                if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
+                    anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
+                            true /* animateOverviewScrim */, launcherView).getAnimators());
+                    // We play StaggeredWorkspaceAnim as a part of the closing window animation.
+                    playWorkspaceReveal = false;
+                } else {
+                    // Skip scaling all apps, otherwise FloatingIconView will get wrong
+                    // layout bounds.
+                    skipAllAppsScale = true;
+                }
+            } else {
+                anim.play(getFallbackClosingWindowAnimators(appTargets));
+            }
+
+            // Normally, we run the launcher content animation when we are transitioning
+            // home, but if home is already visible, then we don't want to animate the
+            // contents of launcher unless we know that we are animating home as a result
+            // of the home button press with quickstep, which will result in launcher being
+            // started on touch down, prior to the animation home (and won't be in the
+            // targets list because it is already visible). In that case, we force
+            // invisibility on touch down, and only reset it after the animation to home
+            // is initialized.
+            if (launcherIsForceInvisibleOrOpening) {
+                addCujInstrumentation(
+                        anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+                // Only register the content animation for cancellation when state changes
+                mLauncher.getStateManager().setCurrentAnimation(anim);
+
+                if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+                    Pair<AnimatorSet, Runnable> contentAnimator =
+                            getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
+                                    skipAllAppsScale);
+                    anim.play(contentAnimator.first);
+                    anim.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            contentAnimator.second.run();
+                        }
+                    });
+                } else {
+                    if (playWorkspaceReveal) {
+                        anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
+                    }
+                }
+            }
+        }
+
+        return new Pair(rectFSpringAnim, anim);
+    }
+
+    /**
      * Remote animation runner for animation from the app to Launcher, including recents.
      */
     protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory {
@@ -1578,84 +1681,12 @@
                 mLauncher.getStateManager().moveToRestState();
             }
 
-            AnimatorSet anim = null;
-            RemoteAnimationProvider provider = mRemoteAnimationProvider;
-            if (provider != null) {
-                anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
-            }
-
-            if (anim == null) {
-                anim = new AnimatorSet();
-
-                final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
-                        || launcherIsATargetWithMode(appTargets, MODE_OPENING);
-
-                View launcherView = findLauncherView(appTargets);
-                boolean playFallBackAnimation = (launcherView == null
-                        && launcherIsForceInvisibleOrOpening)
-                        || mLauncher.getWorkspace().isOverlayShown()
-                        || hasMultipleTargetsWithMode(appTargets, MODE_CLOSING);
-
-                boolean playWorkspaceReveal = true;
-                boolean skipAllAppsScale = false;
-                if (mFromUnlock) {
-                    anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
-                } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
-                        && !playFallBackAnimation) {
-                    // Use a fixed velocity to start the animation.
-                    float velocityPxPerS = DynamicResource.provider(mLauncher)
-                            .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
-                    PointF velocity = new PointF(0, -velocityPxPerS);
-                    getClosingWindowAnimators(anim, appTargets, launcherView, velocity);
-                    if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
-                        anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
-                                true /* animateOverviewScrim */, launcherView).getAnimators());
-                        // We play StaggeredWorkspaceAnim as a part of the closing window animation.
-                        playWorkspaceReveal = false;
-                    } else {
-                        // Skip scaling all apps, otherwise FloatingIconView will get wrong
-                        // layout bounds.
-                        skipAllAppsScale = true;
-                    }
-                } else {
-                    anim.play(getFallbackClosingWindowAnimators(appTargets));
-                }
-
-                // Normally, we run the launcher content animation when we are transitioning
-                // home, but if home is already visible, then we don't want to animate the
-                // contents of launcher unless we know that we are animating home as a result
-                // of the home button press with quickstep, which will result in launcher being
-                // started on touch down, prior to the animation home (and won't be in the
-                // targets list because it is already visible). In that case, we force
-                // invisibility on touch down, and only reset it after the animation to home
-                // is initialized.
-                if (launcherIsForceInvisibleOrOpening) {
-                    addCujInstrumentation(
-                            anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
-                    // Only register the content animation for cancellation when state changes
-                    mLauncher.getStateManager().setCurrentAnimation(anim);
-
-                    if (mLauncher.isInState(LauncherState.ALL_APPS)) {
-                        Pair<AnimatorSet, Runnable> contentAnimator =
-                                getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
-                                        skipAllAppsScale);
-                        anim.play(contentAnimator.first);
-                        anim.addListener(new AnimatorListenerAdapter() {
-                            @Override
-                            public void onAnimationEnd(Animator animation) {
-                                contentAnimator.second.run();
-                            }
-                        });
-                    } else {
-                        if (playWorkspaceReveal) {
-                            anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
-                        }
-                    }
-                }
-            }
+            Pair<RectFSpringAnim, AnimatorSet> pair = createWallpaperOpenAnimations(
+                    appTargets, wallpaperTargets, mFromUnlock,
+                    new RectF(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx));
 
             mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
-            result.setAnimation(anim, mLauncher);
+            result.setAnimation(pair.second, mLauncher);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
new file mode 100644
index 0000000..7abcbdb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2022 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.BaseActivity.INVISIBLE_ALL;
+import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
+import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Pair;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.window.BackEvent;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.QuickstepTransitionManager;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+/**
+ * Controls the animation of swiping back and returning to launcher.
+ *
+ * This is a two part animation. The first part is an animation that tracks gesture location to
+ * scale and move the leaving app window. Once the gesture is committed, the second part takes over
+ * the app window and plays the rest of app close transitions in one go.
+ *
+ * This animation is used only for apps that enable back dispatching via
+ * {@link android.view.OnBackInvokedDispatcher}. The controller registers
+ * an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back
+ * navigation to launcher starts.
+ *
+ * Apps using the legacy back dispatching will keep triggering the WALLPAPER_OPEN remote
+ * transition registered in {@link QuickstepTransitionManager}.
+ *
+ */
+public class LauncherBackAnimationController {
+    private static final int CANCEL_TRANSITION_DURATION = 150;
+    private static final String TAG = "LauncherBackAnimationController";
+    private final DeviceProfile mDeviceProfile;
+    private final QuickstepTransitionManager mQuickstepTransitionManager;
+    private final Matrix mTransformMatrix = new Matrix();
+    private final RectF mTargetRectF = new RectF();
+    private final RectF mStartRectF = new RectF();
+    private final RectF mCurrentRect = new RectF();
+    private final BaseQuickstepLauncher mLauncher;
+    private final int mWindowScaleMarginX;
+    private final int mWindowScaleMarginY;
+    private final float mWindowScaleEndCornerRadius;
+    private final float mWindowScaleStartCornerRadius;
+
+    private RemoteAnimationTargetCompat mBackTarget;
+    private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+    private boolean mSpringAnimationInProgress = false;
+    private boolean mAnimatorSetInProgress = false;
+    @BackEvent.SwipeEdge
+    private int mSwipeEdge;
+    private float mBackProgress = 0;
+    private boolean mBackInProgress = false;
+
+    public LauncherBackAnimationController(
+            DeviceProfile deviceProfile,
+            BaseQuickstepLauncher launcher,
+            QuickstepTransitionManager quickstepTransitionManager) {
+        mDeviceProfile = deviceProfile;
+        mLauncher = launcher;
+        mQuickstepTransitionManager = quickstepTransitionManager;
+        mWindowScaleEndCornerRadius = QuickStepContract.supportsRoundedCornersOnWindows(
+                mLauncher.getResources())
+                ? mLauncher.getResources().getDimensionPixelSize(
+                        R.dimen.swipe_back_window_corner_radius)
+                : 0;
+        mWindowScaleStartCornerRadius = QuickStepContract.getWindowCornerRadius(mLauncher);
+        mWindowScaleMarginX = mLauncher.getResources().getDimensionPixelSize(
+                R.dimen.swipe_back_window_scale_x_margin);
+        mWindowScaleMarginY = mLauncher.getResources().getDimensionPixelSize(
+                R.dimen.swipe_back_window_scale_y_margin);
+    }
+
+    /**
+     * Registers {@link IOnBackInvokedCallback} to receive back dispatches from shell.
+     * @param handler Handler to the thread to run the animations on.
+     */
+    public void registerBackCallbacks(Handler handler) {
+        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
+        if (systemUiProxy == null) {
+            Log.e(TAG, "SystemUiProxy is null. Skip registering back invocation callbacks");
+            return;
+        }
+        systemUiProxy.setBackToLauncherCallback(
+                new IOnBackInvokedCallback.Stub() {
+                    @Override
+                    public void onBackCancelled() {
+                        handler.post(() -> resetPositionAnimated());
+                    }
+
+                    @Override
+                    public void onBackInvoked() {
+                        handler.post(() -> startTransition());
+                    }
+
+                    @Override
+                    public void onBackProgressed(BackEvent backEvent) {
+                        mBackProgress = backEvent.getProgress();
+                        // TODO: Update once the interpolation curve spec is finalized.
+                        mBackProgress =
+                                1 - (1 - mBackProgress) * (1 - mBackProgress) * (1
+                                        - mBackProgress);
+                        if (!mBackInProgress) {
+                            startBack(backEvent);
+                        } else {
+                            updateBackProgress(mBackProgress);
+                        }
+                    }
+
+                    public void onBackStarted() { }
+                });
+    }
+
+    private void resetPositionAnimated() {
+        ValueAnimator cancelAnimator = ValueAnimator.ofFloat(mBackProgress, 0);
+        cancelAnimator.setDuration(CANCEL_TRANSITION_DURATION);
+        cancelAnimator.addUpdateListener(
+                animation -> {
+                    updateBackProgress((float) animation.getAnimatedValue());
+                });
+        cancelAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finishAnimation();
+            }
+        });
+        cancelAnimator.start();
+    }
+
+    /** Unregisters the back to launcher callback in shell. */
+    public void unregisterBackCallbacks() {
+        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
+        if (systemUiProxy != null) {
+            systemUiProxy.clearBackToLauncherCallback();
+        }
+    }
+
+    private void startBack(BackEvent backEvent) {
+        mBackInProgress = true;
+        RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
+
+        if (appTarget == null) {
+            return;
+        }
+
+        mTransaction.show(appTarget.leash).apply();
+        mTransaction.setAnimationTransaction();
+        mBackTarget = new RemoteAnimationTargetCompat(appTarget);
+        mSwipeEdge = backEvent.getSwipeEdge();
+        float screenWidth = mDeviceProfile.widthPx;
+        float screenHeight = mDeviceProfile.heightPx;
+        float targetHeight = screenHeight - 2 * mWindowScaleMarginY;
+        float targetWidth = targetHeight * screenWidth / screenHeight;
+        float left;
+        if (mSwipeEdge == BackEvent.EDGE_LEFT) {
+            left = screenWidth - targetWidth - mWindowScaleMarginX;
+        } else {
+            left = mWindowScaleMarginX;
+        }
+        float top = mWindowScaleMarginY;
+        // TODO(b/218916755): Offset start rectangle in multiwindow mode.
+        mStartRectF.set(0, 0, screenWidth, screenHeight);
+        mTargetRectF.set(left, top, targetWidth + left, targetHeight + top);
+    }
+
+    private void updateBackProgress(float progress) {
+        if (mBackTarget == null) {
+            return;
+        }
+
+        mCurrentRect.set(
+                MathUtils.lerp(mStartRectF.left, mTargetRectF.left, progress),
+                MathUtils.lerp(mStartRectF.top, mTargetRectF.top, progress),
+                MathUtils.lerp(mStartRectF.right, mTargetRectF.right, progress),
+                MathUtils.lerp(mStartRectF.bottom, mTargetRectF.bottom, progress));
+        SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder builder =
+                new SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder(mBackTarget.leash);
+
+        Rect currentRect = new Rect();
+        mCurrentRect.round(currentRect);
+
+        // Scale the target window to match the currentRectF.
+        final float scale = mCurrentRect.width() / mStartRectF.width();
+        mTransformMatrix.reset();
+        mTransformMatrix.setScale(scale, scale);
+        mTransformMatrix.postTranslate(mCurrentRect.left, mCurrentRect.top);
+        Rect startRect = new Rect();
+        mStartRectF.round(startRect);
+        float cornerRadius = Utilities.mapRange(
+                progress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
+        builder.withMatrix(mTransformMatrix)
+                .withWindowCrop(startRect)
+                .withCornerRadius(cornerRadius);
+        SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams = builder.build();
+
+        if (surfaceParams.surface.isValid()) {
+            surfaceParams.applyTo(mTransaction);
+        }
+        mTransaction.apply();
+    }
+
+    private void startTransition() {
+        if (mBackTarget == null) {
+            // Trigger transition system instead of custom transition animation.
+            finishAnimation();
+            return;
+        }
+        if (mLauncher.isDestroyed()) {
+            return;
+        }
+        // TODO: Catch the moment when launcher becomes visible after the top app un-occludes
+        //  launcher and start animating afterwards. Currently we occasionally get a flicker from
+        //  animating when launcher is still invisible.
+        if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) {
+            mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS);
+            mLauncher.getStateManager().moveToRestState();
+        }
+
+        Pair<RectFSpringAnim, AnimatorSet> pair =
+                mQuickstepTransitionManager.createWallpaperOpenAnimations(
+                    new RemoteAnimationTargetCompat[]{mBackTarget},
+                    new RemoteAnimationTargetCompat[]{},
+                    false /* fromUnlock */,
+                    mCurrentRect);
+        startTransitionAnimations(pair.first, pair.second);
+        mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
+    }
+
+    private void finishAnimation() {
+        mBackTarget = null;
+        mBackInProgress = false;
+        mBackProgress = 0;
+        mSwipeEdge = BackEvent.EDGE_LEFT;
+        mTransformMatrix.reset();
+        mTargetRectF.setEmpty();
+        mCurrentRect.setEmpty();
+        mStartRectF.setEmpty();
+        mAnimatorSetInProgress = false;
+        mSpringAnimationInProgress = false;
+        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
+        if (systemUiProxy != null) {
+            SystemUiProxy.INSTANCE.getNoCreate().onBackToLauncherAnimationFinished();
+        }
+    }
+
+    private void startTransitionAnimations(RectFSpringAnim springAnim, AnimatorSet anim) {
+        mAnimatorSetInProgress = anim != null;
+        mSpringAnimationInProgress = springAnim != null;
+        if (springAnim != null) {
+            springAnim.addAnimatorListener(
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mSpringAnimationInProgress = false;
+                            tryFinishBackAnimation();
+                        }
+                    }
+            );
+        }
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimatorSetInProgress = false;
+                tryFinishBackAnimation();
+            }
+        });
+        anim.start();
+    }
+
+    private void tryFinishBackAnimation() {
+        if (!mSpringAnimationInProgress && !mAnimatorSetInProgress) {
+            finishAnimation();
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 667ea14..d8cbd36 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -39,6 +39,7 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.window.IOnBackInvokedCallback;
 
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
@@ -50,6 +51,7 @@
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.SmartspaceState;
+import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
 import com.android.wm.shell.pip.IPipAnimationListener;
@@ -82,6 +84,7 @@
     private IShellTransitions mShellTransitions;
     private IStartingWindow mStartingWindow;
     private IRecentTasks mRecentTasks;
+    private IBackAnimation mBackAnimation;
     private final DeathRecipient mSystemUiProxyDeathRecipient = () -> {
         MAIN_EXECUTOR.execute(() -> clearProxy());
     };
@@ -96,6 +99,7 @@
     private ILauncherUnlockAnimationController mPendingLauncherUnlockAnimationController;
     private IRecentTasksListener mRecentTasksListener;
     private final ArrayList<RemoteTransitionCompat> mRemoteTransitions = new ArrayList<>();
+    private IOnBackInvokedCallback mBackToLaunchCallback;
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -162,8 +166,8 @@
     public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
             IOneHanded oneHanded, IShellTransitions shellTransitions,
             IStartingWindow startingWindow, IRecentTasks recentTasks,
-            ISysuiUnlockAnimationController sysuiUnlockAnimationController) {
-
+            ISysuiUnlockAnimationController sysuiUnlockAnimationController,
+            IBackAnimation backAnimation) {
         unlinkToDeath();
         mSystemUiProxy = proxy;
         mPip = pip;
@@ -173,6 +177,7 @@
         mStartingWindow = startingWindow;
         mSysuiUnlockAnimationController = sysuiUnlockAnimationController;
         mRecentTasks = recentTasks;
+        mBackAnimation = backAnimation;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
         if (mPipAnimationListener != null && mPip != null) {
@@ -195,6 +200,9 @@
         if (mRecentTasksListener != null && mRecentTasks != null) {
             registerRecentTasksListener(mRecentTasksListener);
         }
+        if (mBackAnimation != null && mBackToLaunchCallback != null) {
+            setBackToLauncherCallback(mBackToLaunchCallback);
+        }
 
         if (mPendingSetNavButtonAlpha != null) {
             mPendingSetNavButtonAlpha.run();
@@ -203,7 +211,7 @@
     }
 
     public void clearProxy() {
-        setProxy(null, null, null, null, null, null, null, null);
+        setProxy(null, null, null, null, null, null, null, null, null);
     }
 
     // TODO(141886704): Find a way to remove this
@@ -822,6 +830,49 @@
         mRecentTasksListener = null;
     }
 
+    //
+    // Back navigation transitions
+    //
+
+    /** Sets the launcher {@link android.window.IOnBackInvokedCallback} to shell */
+    public void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
+        mBackToLaunchCallback = callback;
+        if (mBackAnimation == null) {
+            return;
+        }
+        try {
+            mBackAnimation.setBackToLauncherCallback(callback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed call setBackToLauncherCallback", e);
+        }
+    }
+
+    /** Clears the previously registered {@link IOnBackInvokedCallback}. */
+    public void clearBackToLauncherCallback() {
+        if (mBackAnimation == null) {
+            return;
+        }
+        try {
+            mBackAnimation.clearBackToLauncherCallback();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed call clearBackToLauncherCallback", e);
+        }
+    }
+
+    /**
+     * Notifies shell that all back to launcher animations have finished (including the transition
+     * that plays after the gesture is committed and before the app is closed.
+     */
+    public void onBackToLauncherAnimationFinished() {
+        if (mBackAnimation != null) {
+            try {
+                mBackAnimation.onBackToLauncherAnimationFinished();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onBackAnimationFinished", e);
+            }
+        }
+    }
+
     public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
         if (mRecentTasks != null) {
             try {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 021048a..ff67b09 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -26,6 +26,7 @@
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_BACK_ANIMATION;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
@@ -106,6 +107,7 @@
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.shared.tracing.ProtoTraceable;
+import com.android.wm.shell.back.IBackAnimation;
 import com.android.wm.shell.onehanded.IOneHanded;
 import com.android.wm.shell.pip.IPip;
 import com.android.wm.shell.recents.IRecentTasks;
@@ -166,10 +168,12 @@
                             bundle.getBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER));
             IRecentTasks recentTasks = IRecentTasks.Stub.asInterface(
                     bundle.getBinder(KEY_EXTRA_RECENT_TASKS));
+            IBackAnimation backAnimation = IBackAnimation.Stub.asInterface(
+                    bundle.getBinder(KEY_EXTRA_SHELL_BACK_ANIMATION));
             MAIN_EXECUTOR.execute(() -> {
                 SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
                         splitscreen, onehanded, shellTransitions, startingWindow, recentTasks,
-                        launcherUnlockAnimationController);
+                        launcherUnlockAnimationController, backAnimation);
                 TouchInteractionService.this.initInputMonitor();
                 preloadOverview(true /* fromInit */);
             });
diff --git a/res/values/config.xml b/res/values/config.xml
index 509f363..e2fd0e3 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -166,4 +166,8 @@
     <!-- Name of the class used to generate colors from the wallpaper colors. Must be implementing the LauncherAppWidgetHostView.ColorGenerator interface. -->
     <string name="color_generator_class" translatable="false"/>
 
+    <!-- Swipe back to home related -->
+    <dimen name="swipe_back_window_scale_x_margin">10dp</dimen>
+    <dimen name="swipe_back_window_scale_y_margin">80dp</dimen>
+    <dimen name="swipe_back_window_corner_radius">40dp</dimen>
 </resources>