Play seekable animation for customize activity transition API.

For cross-activity animation, support seekable animation if app
has customize activity exit transition by Window#setWindowAnimations or
android:windowAnimationStyle.
Because there could fall back to default cross-activity animation when
the customized animation was not able to load, defer assigning the
mActiveCallback when received onAnimationStart.

Bug: 259427810
Test: verify customized animation can play.
Test: atest BackNavigationControllerTests BackAnimationControllerTest\
CustomizeActivityAnimationTest

Change-Id: I59dc6ef75a226c634b06f483aeed7aec03087d18
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index d7bca30..5140594 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -92,6 +92,8 @@
     @Nullable
     private final IOnBackInvokedCallback mOnBackInvokedCallback;
     private final boolean mPrepareRemoteAnimation;
+    @Nullable
+    private final CustomAnimationInfo mCustomAnimationInfo;
 
     /**
      * Create a new {@link BackNavigationInfo} instance.
@@ -104,11 +106,13 @@
     private BackNavigationInfo(@BackTargetType int type,
             @Nullable RemoteCallback onBackNavigationDone,
             @Nullable IOnBackInvokedCallback onBackInvokedCallback,
-            boolean isPrepareRemoteAnimation) {
+            boolean isPrepareRemoteAnimation,
+            @Nullable CustomAnimationInfo customAnimationInfo) {
         mType = type;
         mOnBackNavigationDone = onBackNavigationDone;
         mOnBackInvokedCallback = onBackInvokedCallback;
         mPrepareRemoteAnimation = isPrepareRemoteAnimation;
+        mCustomAnimationInfo = customAnimationInfo;
     }
 
     private BackNavigationInfo(@NonNull Parcel in) {
@@ -116,6 +120,7 @@
         mOnBackNavigationDone = in.readTypedObject(RemoteCallback.CREATOR);
         mOnBackInvokedCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder());
         mPrepareRemoteAnimation = in.readBoolean();
+        mCustomAnimationInfo = in.readTypedObject(CustomAnimationInfo.CREATOR);
     }
 
     /** @hide */
@@ -125,6 +130,7 @@
         dest.writeTypedObject(mOnBackNavigationDone, flags);
         dest.writeStrongInterface(mOnBackInvokedCallback);
         dest.writeBoolean(mPrepareRemoteAnimation);
+        dest.writeTypedObject(mCustomAnimationInfo, flags);
     }
 
     /**
@@ -172,6 +178,15 @@
         }
     }
 
+    /**
+     * Get customize animation info.
+     * @hide
+     */
+    @Nullable
+    public CustomAnimationInfo getCustomAnimationInfo() {
+        return mCustomAnimationInfo;
+    }
+
     /** @hide */
     @Override
     public int describeContents() {
@@ -197,6 +212,7 @@
                 + "mType=" + typeToString(mType) + " (" + mType + ")"
                 + ", mOnBackNavigationDone=" + mOnBackNavigationDone
                 + ", mOnBackInvokedCallback=" + mOnBackInvokedCallback
+                + ", mCustomizeAnimationInfo=" + mCustomAnimationInfo
                 + '}';
     }
 
@@ -223,6 +239,67 @@
     }
 
     /**
+     * Information for customize back animation.
+     * @hide
+     */
+    public static final class CustomAnimationInfo implements Parcelable {
+        private final String mPackageName;
+        private int mWindowAnimations;
+
+        /**
+         * The package name of the windowAnimations.
+         */
+        @NonNull
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        /**
+         * The resource Id of window animations.
+         */
+        public int getWindowAnimations() {
+            return mWindowAnimations;
+        }
+
+        public CustomAnimationInfo(@NonNull String packageName) {
+            this.mPackageName = packageName;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeString8(mPackageName);
+            dest.writeInt(mWindowAnimations);
+        }
+
+        private CustomAnimationInfo(@NonNull Parcel in) {
+            mPackageName = in.readString8();
+            mWindowAnimations = in.readInt();
+        }
+
+        @Override
+        public String toString() {
+            return "CustomAnimationInfo, package name= " + mPackageName;
+        }
+
+        @NonNull
+        public static final Creator<CustomAnimationInfo> CREATOR = new Creator<>() {
+            @Override
+            public CustomAnimationInfo createFromParcel(Parcel in) {
+                return new CustomAnimationInfo(in);
+            }
+
+            @Override
+            public CustomAnimationInfo[] newArray(int size) {
+                return new CustomAnimationInfo[size];
+            }
+        };
+    }
+    /**
      * @hide
      */
     @SuppressWarnings("UnusedReturnValue") // Builder pattern
@@ -233,6 +310,7 @@
         @Nullable
         private IOnBackInvokedCallback mOnBackInvokedCallback = null;
         private boolean mPrepareRemoteAnimation;
+        private CustomAnimationInfo mCustomAnimationInfo;
 
         /**
          * @see BackNavigationInfo#getType()
@@ -268,12 +346,22 @@
         }
 
         /**
+         * Set windowAnimations for customize animation.
+         */
+        public Builder setWindowAnimations(String packageName, int windowAnimations) {
+            mCustomAnimationInfo = new CustomAnimationInfo(packageName);
+            mCustomAnimationInfo.mWindowAnimations = windowAnimations;
+            return this;
+        }
+
+        /**
          * Builds and returns an instance of {@link BackNavigationInfo}
          */
         public BackNavigationInfo build() {
             return new BackNavigationInfo(mType, mOnBackNavigationDone,
                     mOnBackInvokedCallback,
-                    mPrepareRemoteAnimation);
+                    mPrepareRemoteAnimation,
+                    mCustomAnimationInfo);
         }
     }
 }
diff --git a/core/java/android/window/IBackAnimationRunner.aidl b/core/java/android/window/IBackAnimationRunner.aidl
index 1c67789..b1d7582 100644
--- a/core/java/android/window/IBackAnimationRunner.aidl
+++ b/core/java/android/window/IBackAnimationRunner.aidl
@@ -37,14 +37,13 @@
 
     /**
      * Called when the system is ready for the handler to start animating all the visible tasks.
-     * @param type The back navigation type.
      * @param apps The list of departing (type=MODE_CLOSING) and entering (type=MODE_OPENING)
                    windows to animate,
      * @param wallpapers The list of wallpapers to animate.
      * @param nonApps The list of non-app windows such as Bubbles to animate.
      * @param finishedCallback The callback to invoke when the animation is finished.
      */
-    void onAnimationStart(in int type,
+    void onAnimationStart(
             in RemoteAnimationTarget[] apps,
             in RemoteAnimationTarget[] wallpapers,
             in RemoteAnimationTarget[] nonApps,
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 600ae50..5cab674 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -265,6 +265,34 @@
         }
         return null;
     }
+
+    /** Get animation resId by attribute Id from specific LayoutParams */
+    public int getAnimationResId(LayoutParams lp, int animAttr, int transit) {
+        int resId = Resources.ID_NULL;
+        if (animAttr >= 0) {
+            AttributeCache.Entry ent = getCachedAnimations(lp);
+            if (ent != null) {
+                resId = ent.array.getResourceId(animAttr, 0);
+            }
+        }
+        resId = updateToTranslucentAnimIfNeeded(resId, transit);
+        return resId;
+    }
+
+    /** Get default animation resId */
+    public int getDefaultAnimationResId(int animAttr, int transit) {
+        int resId = Resources.ID_NULL;
+        if (animAttr >= 0) {
+            AttributeCache.Entry ent = getCachedAnimations(DEFAULT_PACKAGE,
+                    mDefaultWindowAnimationStyleResId);
+            if (ent != null) {
+                resId = ent.array.getResourceId(animAttr, 0);
+            }
+        }
+        resId = updateToTranslucentAnimIfNeeded(resId, transit);
+        return resId;
+    }
+
     /**
      * Load animation by attribute Id from a specific AnimationStyle resource.
      *
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 0b87598..349ff36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -120,6 +120,9 @@
     @Nullable
     private IOnBackInvokedCallback mActiveCallback;
 
+    private CrossActivityAnimation mDefaultActivityAnimation;
+    private CustomizeActivityAnimation mCustomizeActivityAnimation;
+
     @VisibleForTesting
     final RemoteCallback mNavigationObserver = new RemoteCallback(
             new RemoteCallback.OnResultListener() {
@@ -194,10 +197,12 @@
                 new CrossTaskBackAnimation(mContext, mAnimationBackground);
         mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
                 crossTaskAnimation.mBackAnimationRunner);
-        final CrossActivityAnimation crossActivityAnimation =
+        mDefaultActivityAnimation =
                 new CrossActivityAnimation(mContext, mAnimationBackground);
         mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
-                crossActivityAnimation.mBackAnimationRunner);
+                mDefaultActivityAnimation.mBackAnimationRunner);
+        mCustomizeActivityAnimation =
+                new CustomizeActivityAnimation(mContext, mAnimationBackground);
         // TODO (236760237): register dialog close animation when it's completed.
     }
 
@@ -368,7 +373,6 @@
         final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
         if (shouldDispatchToAnimator) {
             if (mAnimationDefinition.contains(backType)) {
-                mActiveCallback = mAnimationDefinition.get(backType).getCallback();
                 mAnimationDefinition.get(backType).startGesture();
             } else {
                 mActiveCallback = null;
@@ -542,13 +546,12 @@
         }
 
         final int backType = mBackNavigationInfo.getType();
+        final BackAnimationRunner runner = mAnimationDefinition.get(backType);
         // Simply trigger and finish back navigation when no animator defined.
-        if (!shouldDispatchToAnimator() || mActiveCallback == null) {
+        if (!shouldDispatchToAnimator() || runner == null) {
             invokeOrCancelBack();
             return;
         }
-
-        final BackAnimationRunner runner = mAnimationDefinition.get(backType);
         if (runner.isWaitingAnimation()) {
             ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
             return;
@@ -607,6 +610,12 @@
         mShouldStartOnNextMoveEvent = false;
         mTouchTracker.reset();
         mActiveCallback = null;
+        // reset to default
+        if (mDefaultActivityAnimation != null
+                && mAnimationDefinition.contains(BackNavigationInfo.TYPE_CROSS_ACTIVITY)) {
+            mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+                    mDefaultActivityAnimation.mBackAnimationRunner);
+        }
         if (mBackNavigationInfo != null) {
             mBackNavigationInfo.onBackNavigationFinished(mTriggerBack);
             mBackNavigationInfo = null;
@@ -614,14 +623,35 @@
         mTriggerBack = false;
     }
 
+    private BackAnimationRunner getAnimationRunnerAndInit() {
+        int type = mBackNavigationInfo.getType();
+        // Initiate customized cross-activity animation, or fall back to cross activity animation
+        if (type == BackNavigationInfo.TYPE_CROSS_ACTIVITY && mAnimationDefinition.contains(type)) {
+            final BackNavigationInfo.CustomAnimationInfo animationInfo =
+                    mBackNavigationInfo.getCustomAnimationInfo();
+            if (animationInfo != null && mCustomizeActivityAnimation != null
+                    && mCustomizeActivityAnimation.prepareNextAnimation(animationInfo)) {
+                mAnimationDefinition.get(type).resetWaitingAnimation();
+                mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+                        mCustomizeActivityAnimation.mBackAnimationRunner);
+            }
+        }
+        return mAnimationDefinition.get(type);
+    }
+
     private void createAdapter() {
         IBackAnimationRunner runner = new IBackAnimationRunner.Stub() {
             @Override
-            public void onAnimationStart(int type, RemoteAnimationTarget[] apps,
+            public void onAnimationStart(RemoteAnimationTarget[] apps,
                     RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                     IBackAnimationFinishedCallback finishedCallback) {
                 mShellExecutor.execute(() -> {
-                    final BackAnimationRunner runner = mAnimationDefinition.get(type);
+                    if (mBackNavigationInfo == null) {
+                        Log.e(TAG, "Lack of navigation info to start animation.");
+                        return;
+                    }
+                    final int type = mBackNavigationInfo.getType();
+                    final BackAnimationRunner runner = getAnimationRunnerAndInit();
                     if (runner == null) {
                         Log.e(TAG, "Animation didn't be defined for type "
                                 + BackNavigationInfo.typeToString(type));
@@ -634,6 +664,7 @@
                         }
                         return;
                     }
+                    mActiveCallback = runner.getCallback();
                     mBackAnimationFinishedCallback = finishedCallback;
 
                     ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
@@ -645,11 +676,13 @@
                                 mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
                     }
 
+                    // Dispatch the first progress after animation start for smoothing the initial
+                    // animation, instead of waiting for next onMove.
+                    final BackMotionEvent backFinish = mTouchTracker.createProgressEvent();
+                    dispatchOnBackProgressed(mActiveCallback, backFinish);
                     if (!mBackGestureStarted) {
                         // if the down -> up gesture happened before animation start, we have to
                         // trigger the uninterruptible transition to finish the back animation.
-                        final BackMotionEvent backFinish = mTouchTracker.createProgressEvent();
-                        dispatchOnBackProgressed(mActiveCallback, backFinish);
                         startPostCommitAnimation();
                     }
                 });
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 82c523f..22b841a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -99,4 +99,8 @@
     boolean isAnimationCancelled() {
         return mAnimationCancelled;
     }
+
+    void resetWaitingAnimation() {
+        mWaitingAnimation = false;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
new file mode 100644
index 0000000..ae33b94
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.back;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.FloatProperty;
+import android.view.Choreographer;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Transformation;
+import android.window.BackEvent;
+import android.window.BackMotionEvent;
+import android.window.BackNavigationInfo;
+import android.window.BackProgressAnimator;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+/**
+ * Class that handle customized close activity transition animation.
+ */
+@ShellMainThread
+class CustomizeActivityAnimation {
+    private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+    final BackAnimationRunner mBackAnimationRunner;
+    private final float mCornerRadius;
+    private final SurfaceControl.Transaction mTransaction;
+    private final BackAnimationBackground mBackground;
+    private RemoteAnimationTarget mEnteringTarget;
+    private RemoteAnimationTarget mClosingTarget;
+    private IRemoteAnimationFinishedCallback mFinishCallback;
+    /** Duration of post animation after gesture committed. */
+    private static final int POST_ANIMATION_DURATION = 250;
+
+    private static final int SCALE_FACTOR = 1000;
+    private final SpringAnimation mProgressSpring;
+    private float mLatestProgress = 0.0f;
+
+    private static final float TARGET_COMMIT_PROGRESS = 0.5f;
+
+    private final float[] mTmpFloat9 = new float[9];
+    private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
+
+    final CustomAnimationLoader mCustomAnimationLoader;
+    private Animation mEnterAnimation;
+    private Animation mCloseAnimation;
+    final Transformation mTransformation = new Transformation();
+
+    private final Choreographer mChoreographer;
+
+    CustomizeActivityAnimation(Context context, BackAnimationBackground background) {
+        this(context, background, new SurfaceControl.Transaction(), null);
+    }
+
+    CustomizeActivityAnimation(Context context, BackAnimationBackground background,
+            SurfaceControl.Transaction transaction, Choreographer choreographer) {
+        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+        mBackground = background;
+        mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+        mCustomAnimationLoader = new CustomAnimationLoader(context);
+
+        mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
+        mProgressSpring.setSpring(new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+        mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction;
+        mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance();
+    }
+
+    private float getLatestProgress() {
+        return mLatestProgress * SCALE_FACTOR;
+    }
+    private void setLatestProgress(float value) {
+        mLatestProgress = value / SCALE_FACTOR;
+        applyTransformTransaction(mLatestProgress);
+    }
+
+    private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP =
+            new FloatProperty<>("enter") {
+                @Override
+                public void setValue(CustomizeActivityAnimation anim, float value) {
+                    anim.setLatestProgress(value);
+                }
+
+                @Override
+                public Float get(CustomizeActivityAnimation object) {
+                    return object.getLatestProgress();
+                }
+            };
+
+    // The target will lose focus when alpha == 0, so keep a minimum value for it.
+    private static float keepMinimumAlpha(float transAlpha) {
+        return Math.max(transAlpha, 0.005f);
+    }
+
+    private static void initializeAnimation(Animation animation, Rect bounds) {
+        final int width = bounds.width();
+        final int height = bounds.height();
+        animation.initialize(width, height, width, height);
+    }
+
+    private void startBackAnimation() {
+        if (mEnteringTarget == null || mClosingTarget == null
+                || mCloseAnimation == null || mEnterAnimation == null) {
+            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
+            return;
+        }
+        initializeAnimation(mCloseAnimation, mClosingTarget.localBounds);
+        initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds);
+
+        // Draw background with task background color.
+        if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) {
+            mBackground.ensureBackground(
+                    mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
+        }
+    }
+
+    private void applyTransformTransaction(float progress) {
+        if (mClosingTarget == null || mEnteringTarget == null) {
+            return;
+        }
+        applyTransform(mClosingTarget.leash, progress, mCloseAnimation);
+        applyTransform(mEnteringTarget.leash, progress, mEnterAnimation);
+        mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+        mTransaction.apply();
+    }
+
+    private void applyTransform(SurfaceControl leash, float progress, Animation animation) {
+        mTransformation.clear();
+        animation.getTransformationAt(progress, mTransformation);
+        mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9);
+        mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha()));
+        mTransaction.setCornerRadius(leash, mCornerRadius);
+    }
+
+    void finishAnimation() {
+        if (mCloseAnimation != null) {
+            mCloseAnimation.reset();
+            mCloseAnimation = null;
+        }
+        if (mEnterAnimation != null) {
+            mEnterAnimation.reset();
+            mEnterAnimation = null;
+        }
+        if (mEnteringTarget != null) {
+            mEnteringTarget.leash.release();
+            mEnteringTarget = null;
+        }
+        if (mClosingTarget != null) {
+            mClosingTarget.leash.release();
+            mClosingTarget = null;
+        }
+        if (mBackground != null) {
+            mBackground.removeBackground(mTransaction);
+        }
+        mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+        mTransaction.apply();
+        mTransformation.clear();
+        mLatestProgress = 0;
+        if (mFinishCallback != null) {
+            try {
+                mFinishCallback.onAnimationFinished();
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+            mFinishCallback = null;
+        }
+        mProgressSpring.animateToFinalPosition(0);
+        mProgressSpring.skipToEnd();
+    }
+
+    void onGestureProgress(@NonNull BackEvent backEvent) {
+        if (mEnteringTarget == null || mClosingTarget == null
+                || mCloseAnimation == null || mEnterAnimation == null) {
+            return;
+        }
+
+        final float progress = backEvent.getProgress();
+
+        float springProgress = (progress > 0.1f
+                ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f)
+                : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
+
+        mProgressSpring.animateToFinalPosition(springProgress);
+    }
+
+    static float mapLinear(float x, float a1, float a2, float b1, float b2) {
+        return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
+    }
+
+    void onGestureCommitted() {
+        if (mEnteringTarget == null || mClosingTarget == null
+                || mCloseAnimation == null || mEnterAnimation == null) {
+            finishAnimation();
+            return;
+        }
+        mProgressSpring.cancel();
+
+        // Enter phase 2 of the animation
+        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f)
+                .setDuration(POST_ANIMATION_DURATION);
+        valueAnimator.setInterpolator(mDecelerateInterpolator);
+        valueAnimator.addUpdateListener(animation -> {
+            float progress = (float) animation.getAnimatedValue();
+            applyTransformTransaction(progress);
+        });
+
+        valueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finishAnimation();
+            }
+        });
+        valueAnimator.start();
+    }
+
+    /**
+     * Load customize animation before animation start.
+     */
+    boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) {
+        mCloseAnimation = mCustomAnimationLoader.load(
+                animationInfo, false /* enterAnimation */);
+        if (mCloseAnimation != null) {
+            mEnterAnimation = mCustomAnimationLoader.load(
+                    animationInfo, true /* enterAnimation */);
+            return true;
+        }
+        return false;
+    }
+
+    private final class Callback extends IOnBackInvokedCallback.Default {
+        @Override
+        public void onBackStarted(BackMotionEvent backEvent) {
+            mProgressAnimator.onBackStarted(backEvent,
+                    CustomizeActivityAnimation.this::onGestureProgress);
+        }
+
+        @Override
+        public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+            mProgressAnimator.onBackProgressed(backEvent);
+        }
+
+        @Override
+        public void onBackCancelled() {
+            mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::finishAnimation);
+        }
+
+        @Override
+        public void onBackInvoked() {
+            mProgressAnimator.reset();
+            onGestureCommitted();
+        }
+    }
+
+    private final class Runner extends IRemoteAnimationRunner.Default {
+        @Override
+        public void onAnimationStart(
+                int transit,
+                RemoteAnimationTarget[] apps,
+                RemoteAnimationTarget[] wallpapers,
+                RemoteAnimationTarget[] nonApps,
+                IRemoteAnimationFinishedCallback finishedCallback) {
+            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to customize animation.");
+            for (RemoteAnimationTarget a : apps) {
+                if (a.mode == MODE_CLOSING) {
+                    mClosingTarget = a;
+                }
+                if (a.mode == MODE_OPENING) {
+                    mEnteringTarget = a;
+                }
+            }
+            if (mCloseAnimation == null || mEnterAnimation == null) {
+                ProtoLog.d(WM_SHELL_BACK_PREVIEW,
+                        "No animation loaded, should choose cross-activity animation?");
+            }
+
+            startBackAnimation();
+            mFinishCallback = finishedCallback;
+        }
+
+        @Override
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
+            finishAnimation();
+        }
+    }
+
+    /**
+     * Helper class to load custom animation.
+     */
+    static class CustomAnimationLoader {
+        private final TransitionAnimation mTransitionAnimation;
+
+        CustomAnimationLoader(Context context) {
+            mTransitionAnimation = new TransitionAnimation(
+                    context, false /* debug */, "CustomizeBackAnimation");
+        }
+
+        Animation load(BackNavigationInfo.CustomAnimationInfo animationInfo,
+                boolean enterAnimation) {
+            final String packageName = animationInfo.getPackageName();
+            if (packageName.isEmpty()) {
+                return null;
+            }
+            final int windowAnimations = animationInfo.getWindowAnimations();
+            if (windowAnimations == 0) {
+                return null;
+            }
+            final int attrs = enterAnimation
+                    ? com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation
+                    : com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation;
+            Animation a = mTransitionAnimation.loadAnimationAttr(packageName, windowAnimations,
+                    attrs, false /* translucent */);
+            // Only allow to load default animation for opening target.
+            if (a == null && enterAnimation) {
+                a = mTransitionAnimation.loadDefaultAnimationAttr(attrs, false /* translucent */);
+            }
+            if (a != null) {
+                ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a);
+            } else {
+                ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded");
+            }
+            return a;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 5a4a44f..6dae479 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -422,7 +422,7 @@
         RemoteAnimationTarget animationTarget = createAnimationTarget();
         RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget};
         if (mController.mBackAnimationAdapter != null) {
-            mController.mBackAnimationAdapter.getRunner().onAnimationStart(type,
+            mController.mBackAnimationAdapter.getRunner().onAnimationStart(
                     targets, null, null, mBackAnimationFinishedCallback);
             mShellExecutor.flushAll();
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
new file mode 100644
index 0000000..2814ef9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.back;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.WindowConfiguration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Choreographer;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.BackNavigationInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class CustomizeActivityAnimationTest extends ShellTestCase {
+    private static final int BOUND_SIZE = 100;
+    @Mock
+    private BackAnimationBackground mBackAnimationBackground;
+    @Mock
+    private Animation mMockCloseAnimation;
+    @Mock
+    private Animation mMockOpenAnimation;
+
+    private CustomizeActivityAnimation mCustomizeActivityAnimation;
+
+    @Before
+    public void setUp() throws Exception {
+        mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext,
+                mBackAnimationBackground, mock(SurfaceControl.Transaction.class),
+                mock(Choreographer.class));
+        spyOn(mCustomizeActivityAnimation);
+        spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader);
+        doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+                .load(any(), eq(false));
+        doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+                .load(any(), eq(true));
+    }
+
+    RemoteAnimationTarget createAnimationTarget(boolean open) {
+        SurfaceControl topWindowLeash = new SurfaceControl();
+        return new RemoteAnimationTarget(1,
+                open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING,
+                topWindowLeash, false, new Rect(), new Rect(), -1,
+                new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(),
+                new WindowConfiguration(), true, null, null, null, false, -1);
+    }
+
+    @Test
+    public void receiveFinishAfterInvoke() throws InterruptedException {
+        mCustomizeActivityAnimation.prepareNextAnimation(
+                new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+        final RemoteAnimationTarget close = createAnimationTarget(false);
+        final RemoteAnimationTarget open = createAnimationTarget(true);
+        // start animation with remote animation targets
+        final CountDownLatch finishCalled = new CountDownLatch(1);
+        final Runnable finishCallback = finishCalled::countDown;
+        mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+                new RemoteAnimationTarget[]{close, open}, null, null, finishCallback);
+        verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+                eq(BOUND_SIZE), eq(BOUND_SIZE));
+        verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+                eq(BOUND_SIZE), eq(BOUND_SIZE));
+
+        try {
+            mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked();
+        } catch (RemoteException r) {
+            fail("onBackInvoked throw remote exception");
+        }
+        verify(mCustomizeActivityAnimation).onGestureCommitted();
+        finishCalled.await(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void receiveFinishAfterCancel() throws InterruptedException {
+        mCustomizeActivityAnimation.prepareNextAnimation(
+                new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+        final RemoteAnimationTarget close = createAnimationTarget(false);
+        final RemoteAnimationTarget open = createAnimationTarget(true);
+        // start animation with remote animation targets
+        final CountDownLatch finishCalled = new CountDownLatch(1);
+        final Runnable finishCallback = finishCalled::countDown;
+        mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+                new RemoteAnimationTarget[]{close, open}, null, null, finishCallback);
+        verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+                eq(BOUND_SIZE), eq(BOUND_SIZE));
+        verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+                eq(BOUND_SIZE), eq(BOUND_SIZE));
+
+        try {
+            mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackCancelled();
+        } catch (RemoteException r) {
+            fail("onBackCancelled throw remote exception");
+        }
+        finishCalled.await(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException {
+        mCustomizeActivityAnimation.prepareNextAnimation(
+                new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+        // start animation without any remote animation targets
+        final CountDownLatch finishCalled = new CountDownLatch(1);
+        final Runnable finishCallback = finishCalled::countDown;
+        mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+                new RemoteAnimationTarget[]{}, null, null, finishCallback);
+
+        try {
+            mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked();
+        } catch (RemoteException r) {
+            fail("onBackInvoked throw remote exception");
+        }
+        verify(mCustomizeActivityAnimation).onGestureCommitted();
+        finishCalled.await(1, TimeUnit.SECONDS);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 2d45dc2..f9f972c 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -21,6 +21,7 @@
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_NONE;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
@@ -31,6 +32,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.res.ResourceId;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -51,12 +53,14 @@
 import android.window.TaskSnapshot;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.LocalServices;
 import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
@@ -80,6 +84,8 @@
     // re-parenting leashes and set launch behind, etc. Will be handled when transition finished.
     private AnimationHandler.ScheduleAnimationBuilder mPendingAnimationBuilder;
 
+    private static int sDefaultAnimationResId;
+
     /**
      * true if the back predictability feature is enabled
      */
@@ -255,9 +261,19 @@
             } else if (prevActivity != null) {
                 if (!isOccluded || prevActivity.canShowWhenLocked()) {
                     // We have another Activity in the same currentTask to go to
-                    backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY;
+                    final WindowContainer parent = currentActivity.getParent();
+                    final boolean isCustomize = parent != null
+                            && (parent.asTask() != null
+                            || (parent.asTaskFragment() != null
+                            && parent.canCustomizeAppTransition()))
+                            && isCustomizeExitAnimation(window);
+                    if (isCustomize) {
+                        infoBuilder.setWindowAnimations(
+                                window.mAttrs.packageName, window.mAttrs.windowAnimations);
+                    }
                     removedWindowContainer = currentActivity;
                     prevTask = prevActivity.getTask();
+                    backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY;
                 } else {
                     backType = BackNavigationInfo.TYPE_CALLBACK;
                 }
@@ -370,6 +386,37 @@
         return kc.isKeyguardLocked(displayId) && kc.isDisplayOccluded(displayId);
     }
 
+    /**
+     * There are two ways to customize activity exit animation, one is to provide the
+     * windowAnimationStyle by Activity#setTheme, another one is to set resId by
+     * Window#setWindowAnimations.
+     * Not all run-time customization methods can be checked from here, such as
+     * overridePendingTransition, which the animation resource will be set just before the
+     * transition is about to happen.
+     */
+    private static boolean isCustomizeExitAnimation(WindowState window) {
+        // The default animation ResId is loaded from system package, so the result must match.
+        if (Objects.equals(window.mAttrs.packageName, "android")) {
+            return false;
+        }
+        if (window.mAttrs.windowAnimations != 0) {
+            final TransitionAnimation transitionAnimation = window.getDisplayContent()
+                    .mAppTransition.mTransitionAnimation;
+            final int attr = com.android.internal.R.styleable
+                    .WindowAnimation_activityCloseExitAnimation;
+            final int appResId = transitionAnimation.getAnimationResId(
+                    window.mAttrs, attr, TRANSIT_OLD_NONE);
+            if (ResourceId.isValid(appResId)) {
+                if (sDefaultAnimationResId == 0) {
+                    sDefaultAnimationResId = transitionAnimation.getDefaultAnimationResId(attr,
+                            TRANSIT_OLD_NONE);
+                }
+                return sDefaultAnimationResId != appResId;
+            }
+        }
+        return false;
+    }
+
     // For legacy transition.
     /**
      *  Once we find the transition targets match back animation targets, remove the target from
@@ -997,7 +1044,7 @@
 
                 return () -> {
                     try {
-                        mBackAnimationAdapter.getRunner().onAnimationStart(mType,
+                        mBackAnimationAdapter.getRunner().onAnimationStart(
                                 targets, null, null, callback);
                     } catch (RemoteException e) {
                         e.printStackTrace();
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 56461f0..b80c3e8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -26,6 +26,7 @@
 import static android.window.BackNavigationInfo.typeToString;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -149,6 +150,28 @@
     }
 
     @Test
+    public void backTypeCrossActivityWithCustomizeExitAnimation() {
+        CrossActivityTestCase testCase = createTopTaskWithTwoActivities();
+        IOnBackInvokedCallback callback = withSystemCallback(testCase.task);
+        testCase.windowFront.mAttrs.windowAnimations = 0x10;
+        spyOn(mDisplayContent.mAppTransition.mTransitionAnimation);
+        doReturn(0xffff00AB).when(mDisplayContent.mAppTransition.mTransitionAnimation)
+                .getAnimationResId(any(), anyInt(), anyInt());
+        doReturn(0xffff00CD).when(mDisplayContent.mAppTransition.mTransitionAnimation)
+                .getDefaultAnimationResId(anyInt(), anyInt());
+
+        BackNavigationInfo backNavigationInfo = startBackNavigation();
+        assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
+        assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
+        assertThat(backNavigationInfo.getCustomAnimationInfo().getWindowAnimations())
+                .isEqualTo(testCase.windowFront.mAttrs.windowAnimations);
+        assertThat(typeToString(backNavigationInfo.getType()))
+                .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
+        // verify if back animation would start.
+        assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation());
+    }
+
+    @Test
     public void backTypeCrossActivityWhenBackToPreviousActivity() {
         CrossActivityTestCase testCase = createTopTaskWithTwoActivities();
         IOnBackInvokedCallback callback = withSystemCallback(testCase.task);
@@ -158,6 +181,8 @@
         assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
         assertThat(typeToString(backNavigationInfo.getType()))
                 .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
+        // verify if back animation would start.
+        assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation());
 
         // reset drawing status
         testCase.recordFront.forAllWindows(w -> {
@@ -510,6 +535,8 @@
         testCase.task = task;
         testCase.recordBack = record1;
         testCase.recordFront = record2;
+        testCase.windowBack = window1;
+        testCase.windowFront = window2;
         return testCase;
     }
 
@@ -525,6 +552,8 @@
     private class CrossActivityTestCase {
         public Task task;
         public ActivityRecord recordBack;
+        public WindowState windowBack;
         public ActivityRecord recordFront;
+        public WindowState windowFront;
     }
 }