Merge "Launcher side implementation of gesture seekable back to home animation." into tm-dev
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 9a70146..f14e2a2 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -111,6 +111,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;
@@ -216,6 +217,7 @@
     private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
     private RemoteTransitionCompat mLauncherOpenTransition;
 
+    private LauncherBackAnimationController mBackAnimationController;
     private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
         @Override
         public void onAnimationStart(Animator animation) {
@@ -241,6 +243,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);
@@ -1159,6 +1163,9 @@
             mLauncherOpenTransition.addHomeOpenCheck(mLauncher.getComponentName());
             SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition);
         }
+        if (mBackAnimationController != null) {
+            mBackAnimationController.registerBackCallbacks(mHandler);
+        }
     }
 
     public void onActivityDestroyed() {
@@ -1194,6 +1201,10 @@
             mLauncherOpenTransition = null;
             mWallpaperOpenTransitionRunner = null;
         }
+        if (mBackAnimationController != null) {
+            mBackAnimationController.unregisterBackCallbacks();
+            mBackAnimationController = null;
+        }
     }
 
     private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
@@ -1346,8 +1357,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();
@@ -1379,8 +1391,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.
@@ -1414,7 +1425,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) {
@@ -1438,6 +1449,7 @@
                 anim.start(mLauncher, velocityPxPerS);
             }
         });
+        return anim;
     }
 
     /**
@@ -1562,6 +1574,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 {
@@ -1601,84 +1704,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>