From 328149549807b0581cfdf51adf02307788497061 Mon Sep 17 00:00:00 2001
From: Charles Chen <charlesccchen@google.com>
Date: Thu, 23 May 2024 20:50:47 +0800
Subject: [PATCH] Move AnimationOptions to Change

There are several reasons for this requests:
1. Fix "Dim flicker on package installer"
2. Implement overlay transition by animation resources override
3. Activity Embedding Animation Customization

Test: atest TransitionTests ActivityEmbeddingAnimationRunnerTests ActivityEmbeddingControllerTests
Bug: 327332488
Bug: 295805497
Flag: com.android.window.flags.move_animation_options_to_change
Change-Id: I060b87506a1bf732228ec6b44e7e9dd48782ec0c
---
 core/java/android/window/TransitionInfo.java  | 79 ++++++++++++++++++-
 .../ActivityEmbeddingAnimationSpec.java       | 15 +++-
 .../ActivityEmbeddingController.java          | 41 +++++++---
 .../transition/DefaultTransitionHandler.java  | 16 +++-
 .../transition/TransitionAnimationHelper.java | 10 ++-
 ...ActivityEmbeddingAnimationRunnerTests.java | 32 +++++++-
 .../ActivityEmbeddingControllerTests.java     | 42 +++++++++-
 .../com/android/server/wm/Transition.java     | 70 ++++++++++++----
 .../android/server/wm/TransitionTests.java    |  7 ++
 9 files changed, 271 insertions(+), 41 deletions(-)

diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 4ffd88093531..8a79754398db 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -39,6 +39,7 @@ import static android.view.WindowManager.TransitionFlags;
 import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
 
+import android.annotation.AnimRes;
 import android.annotation.ColorInt;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -54,6 +55,8 @@ import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 
+import com.android.window.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -211,6 +214,8 @@ public final class TransitionInfo implements Parcelable {
     private final ArrayList<Change> mChanges = new ArrayList<>();
     private final ArrayList<Root> mRoots = new ArrayList<>();
 
+    // TODO(b/327332488): Clean-up usages after the flag is fully enabled.
+    @Deprecated
     private AnimationOptions mOptions;
 
     /** This is only a BEST-EFFORT id used for log correlation. DO NOT USE for any real work! */
@@ -275,7 +280,15 @@ public final class TransitionInfo implements Parcelable {
         mRoots.add(other);
     }
 
+    /**
+     * @deprecated Set {@link AnimationOptions} to change. This method is only used if
+     * {@link Flags#FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE} is disabled.
+     */
+    @Deprecated
     public void setAnimationOptions(@Nullable AnimationOptions options) {
+        if (Flags.moveAnimationOptionsToChange()) {
+            return;
+        }
         mOptions = options;
     }
 
@@ -340,6 +353,11 @@ public final class TransitionInfo implements Parcelable {
         return mRoots.get(0).mLeash;
     }
 
+    /**
+     * @deprecated Use {@link Change#getAnimationOptions()} instead. This method is called only
+     * if {@link Flags#FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE} is disabled.
+     */
+    @Deprecated
     @Nullable
     public AnimationOptions getAnimationOptions() {
         return mOptions;
@@ -661,6 +679,7 @@ public final class TransitionInfo implements Parcelable {
         private SurfaceControl mSnapshot = null;
         private float mSnapshotLuma;
         private ComponentName mActivityComponent = null;
+        private AnimationOptions mAnimationOptions = null;
 
         public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) {
             mContainer = container;
@@ -690,6 +709,7 @@ public final class TransitionInfo implements Parcelable {
             mSnapshot = in.readTypedObject(SurfaceControl.CREATOR);
             mSnapshotLuma = in.readFloat();
             mActivityComponent = in.readTypedObject(ComponentName.CREATOR);
+            mAnimationOptions = in.readTypedObject(AnimationOptions.CREATOR);
         }
 
         private Change localRemoteCopy() {
@@ -713,6 +733,7 @@ public final class TransitionInfo implements Parcelable {
             out.mSnapshot = mSnapshot != null ? new SurfaceControl(mSnapshot, "localRemote") : null;
             out.mSnapshotLuma = mSnapshotLuma;
             out.mActivityComponent = mActivityComponent;
+            out.mAnimationOptions = mAnimationOptions;
             return out;
         }
 
@@ -813,6 +834,16 @@ public final class TransitionInfo implements Parcelable {
             mActivityComponent = component;
         }
 
+        /**
+         * Sets {@link AnimationOptions} to override animation.
+         */
+        public void setAnimationOptions(@Nullable AnimationOptions options) {
+            if (!Flags.moveAnimationOptionsToChange()) {
+                return;
+            }
+            mAnimationOptions = options;
+        }
+
         /** @return the container that is changing. May be null if non-remotable (eg. activity) */
         @Nullable
         public WindowContainerToken getContainer() {
@@ -952,6 +983,14 @@ public final class TransitionInfo implements Parcelable {
             return mActivityComponent;
         }
 
+        /**
+         * Returns the {@link AnimationOptions}.
+         */
+        @Nullable
+        public AnimationOptions getAnimationOptions() {
+            return mAnimationOptions;
+        }
+
         /** @hide */
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -976,6 +1015,7 @@ public final class TransitionInfo implements Parcelable {
             dest.writeTypedObject(mSnapshot, flags);
             dest.writeFloat(mSnapshotLuma);
             dest.writeTypedObject(mActivityComponent, flags);
+            dest.writeTypedObject(mAnimationOptions, flags);
         }
 
         @NonNull
@@ -1028,6 +1068,9 @@ public final class TransitionInfo implements Parcelable {
             if (mEndFixedRotation != ROTATION_UNDEFINED) {
                 sb.append(" endFixedRotation="); sb.append(mEndFixedRotation);
             }
+            if (mBackgroundColor != 0) {
+                sb.append(" bc=").append(Integer.toHexString(mBackgroundColor));
+            }
             if (mSnapshot != null) {
                 sb.append(" snapshot="); sb.append(mSnapshot);
             }
@@ -1042,6 +1085,9 @@ public final class TransitionInfo implements Parcelable {
                 sb.append(" taskParent=");
                 sb.append(mTaskInfo.parentTaskId);
             }
+            if (mAnimationOptions != null) {
+                sb.append(" opt=").append(mAnimationOptions);
+            }
             sb.append('}');
             return sb.toString();
         }
@@ -1051,14 +1097,23 @@ public final class TransitionInfo implements Parcelable {
     @SuppressWarnings("UserHandleName")
     public static final class AnimationOptions implements Parcelable {
 
+        /**
+         * The default value for animation resources ID, which means to use the system default
+         * animation.
+         */
+        @SuppressWarnings("ResourceType") // Use as a hint to use the system default animation.
+        @AnimRes
+        public static final int DEFAULT_ANIMATION_RESOURCES_ID = 0xFFFFFFFF;
+
         private int mType;
-        private int mEnterResId;
-        private int mExitResId;
+        private @AnimRes int mEnterResId = DEFAULT_ANIMATION_RESOURCES_ID;
+        private @AnimRes int mExitResId = DEFAULT_ANIMATION_RESOURCES_ID;
         private boolean mOverrideTaskTransition;
         private String mPackageName;
         private final Rect mTransitionBounds = new Rect();
         private HardwareBuffer mThumbnail;
         private int mAnimations;
+        // TODO(b/295805497): Extract it from AnimationOptions
         private @ColorInt int mBackgroundColor;
         // Customize activity transition animation
         private CustomActivityTransition mCustomActivityOpenTransition;
@@ -1121,10 +1176,18 @@ public final class TransitionInfo implements Parcelable {
             customTransition.addCustomActivityTransition(enterResId, exitResId, backgroundColor);
         }
 
-        /** Make options for a custom animation based on anim resources */
+        /**
+         * Make options for a custom animation based on anim resources.
+         *
+         * @param packageName the package name to find the animation resources
+         * @param enterResId the open animation resources ID
+         * @param exitResId the close animation resources ID
+         * @param backgroundColor the background color
+         * @param overrideTaskTransition whether to override the task transition
+         */
         @NonNull
         public static AnimationOptions makeCustomAnimOptions(@NonNull String packageName,
-                int enterResId, int exitResId, @ColorInt int backgroundColor,
+                @AnimRes int enterResId, @AnimRes int exitResId, @ColorInt int backgroundColor,
                 boolean overrideTaskTransition) {
             AnimationOptions options = new AnimationOptions(ANIM_CUSTOM);
             options.mPackageName = packageName;
@@ -1182,10 +1245,12 @@ public final class TransitionInfo implements Parcelable {
             return mType;
         }
 
+        @AnimRes
         public int getEnterResId() {
             return mEnterResId;
         }
 
+        @AnimRes
         public int getExitResId() {
             return mExitResId;
         }
@@ -1284,6 +1349,12 @@ public final class TransitionInfo implements Parcelable {
             if (!mTransitionBounds.isEmpty()) {
                 sb.append(" bounds=").append(mTransitionBounds);
             }
+            if (mEnterResId != DEFAULT_ANIMATION_RESOURCES_ID) {
+                sb.append(" enterResId=").append(mEnterResId);
+            }
+            if (mExitResId != DEFAULT_ANIMATION_RESOURCES_ID) {
+                sb.append(" exitResId=").append(mExitResId);
+            }
             sb.append('}');
             return sb.toString();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 0272f1cda6ef..b9868629e64b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -38,6 +38,7 @@ import android.view.animation.TranslateAnimation;
 import android.window.TransitionInfo;
 
 import com.android.internal.policy.TransitionAnimation;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.shared.TransitionUtil;
 
 /** Animation spec for ActivityEmbedding transition. */
@@ -202,7 +203,7 @@ class ActivityEmbeddingAnimationSpec {
     Animation loadOpenAnimation(@NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
-        final Animation customAnimation = loadCustomAnimation(info, isEnter);
+        final Animation customAnimation = loadCustomAnimation(info, change, isEnter);
         final Animation animation;
         if (customAnimation != null) {
             animation = customAnimation;
@@ -229,7 +230,7 @@ class ActivityEmbeddingAnimationSpec {
     Animation loadCloseAnimation(@NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
-        final Animation customAnimation = loadCustomAnimation(info, isEnter);
+        final Animation customAnimation = loadCustomAnimation(info, change, isEnter);
         final Animation animation;
         if (customAnimation != null) {
             animation = customAnimation;
@@ -261,8 +262,14 @@ class ActivityEmbeddingAnimationSpec {
     }
 
     @Nullable
-    private Animation loadCustomAnimation(@NonNull TransitionInfo info, boolean isEnter) {
-        final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+    private Animation loadCustomAnimation(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change change, boolean isEnter) {
+        final TransitionInfo.AnimationOptions options;
+        if (Flags.moveAnimationOptionsToChange()) {
+            options = change.getAnimationOptions();
+        } else {
+            options = info.getAnimationOptions();
+        }
         if (options == null || options.getType() != ANIM_CUSTOM) {
             return null;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index d6b9d34c5ab3..b4ef9f0fc2ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -32,6 +32,7 @@ import android.os.IBinder;
 import android.util.ArrayMap;
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
+import android.window.TransitionInfo.AnimationOptions;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 
@@ -39,6 +40,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.shared.TransitionUtil;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
@@ -117,24 +119,39 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
             return false;
         }
 
-        final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
-        if (options != null) {
-            // Scene-transition should be handled by app side.
-            if (options.getType() == ANIM_SCENE_TRANSITION) {
+        return shouldAnimateAnimationOptions(info);
+    }
+
+    private boolean shouldAnimateAnimationOptions(@NonNull TransitionInfo info) {
+        if (!Flags.moveAnimationOptionsToChange()) {
+            return shouldAnimateAnimationOptions(info.getAnimationOptions());
+        }
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (!shouldAnimateAnimationOptions(change.getAnimationOptions())) {
+                // If any of override animation is not supported, don't animate the transition.
                 return false;
             }
-            // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition,
-            // and Activity#overrideActivityTransition are supported.
-            if (options.getType() == ANIM_CUSTOM) {
-                return true;
-            }
-            // Use default transition handler to animate other override animation.
-            return !isSupportedOverrideAnimation(options);
         }
-
         return true;
     }
 
+    private boolean shouldAnimateAnimationOptions(@Nullable AnimationOptions options) {
+        if (options == null) {
+            return true;
+        }
+        // Scene-transition should be handled by app side.
+        if (options.getType() == ANIM_SCENE_TRANSITION) {
+            return false;
+        }
+        // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition,
+        // and Activity#overrideActivityTransition are supported.
+        if (options.getType() == ANIM_CUSTOM) {
+            return true;
+        }
+        // Use default transition handler to animate other override animation.
+        return !isSupportedOverrideAnimation(options);
+    }
+
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 2d6ba6ee7217..018c9044e2f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -103,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
@@ -543,7 +544,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
                         mTransactionPool, mMainExecutor, animRelOffset, cornerRadius,
                         clipRect);
 
-                if (info.getAnimationOptions() != null) {
+                final TransitionInfo.AnimationOptions options;
+                if (Flags.moveAnimationOptionsToChange()) {
+                    options = info.getAnimationOptions();
+                } else {
+                    options = change.getAnimationOptions();
+                }
+                if (options != null) {
                     attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(),
                             cornerRadius);
                 }
@@ -725,7 +732,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
         final boolean isOpeningType = TransitionUtil.isOpeningType(type);
         final boolean enter = TransitionUtil.isOpeningType(changeMode);
         final boolean isTask = change.getTaskInfo() != null;
-        final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+        final TransitionInfo.AnimationOptions options;
+        if (Flags.moveAnimationOptionsToChange()) {
+            options = change.getAnimationOptions();
+        } else {
+            options = info.getAnimationOptions();
+        }
         final int overrideType = options != null ? options.getType() : ANIM_NONE;
         final Rect endBounds = TransitionUtil.isClosingType(changeMode)
                 ? mRotator.getEndBoundsInStartRotation(change)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index ad4f02d13cc6..2047b5a88604 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -55,6 +55,7 @@ import android.window.TransitionInfo;
 import com.android.internal.R;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.TransitionUtil;
 
@@ -71,7 +72,12 @@ public class TransitionAnimationHelper {
         final int changeFlags = change.getFlags();
         final boolean enter = TransitionUtil.isOpeningType(changeMode);
         final boolean isTask = change.getTaskInfo() != null;
-        final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+        final TransitionInfo.AnimationOptions options;
+        if (Flags.moveAnimationOptionsToChange()) {
+            options = change.getAnimationOptions();
+        } else {
+            options = info.getAnimationOptions();
+        }
         final int overrideType = options != null ? options.getType() : ANIM_NONE;
         int animAttr = 0;
         boolean translucent = false;
@@ -246,7 +252,7 @@ public class TransitionAnimationHelper {
         if (!a.getShowBackdrop()) {
             return defaultColor;
         }
-        if (info.getAnimationOptions() != null
+        if (!Flags.moveAnimationOptionsToChange() && info.getAnimationOptions() != null
                 && info.getAnimationOptions().getBackgroundColor() != 0) {
             // If available use the background color provided through AnimationOptions
             return info.getAnimationOptions().getBackgroundColor();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index ea522cdf2509..bd20c1143262 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -32,14 +32,19 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.animation.Animator;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.window.TransitionInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.window.flags.Flags;
 import com.android.wm.shell.transition.TransitionInfoBuilder;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -56,12 +61,16 @@ import java.util.ArrayList;
 @RunWith(AndroidJUnit4.class)
 public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
 
+    @Rule
+    public SetFlagsRule mRule = new SetFlagsRule();
+
     @Before
     public void setup() {
         super.setUp();
         doNothing().when(mController).onAnimationFinished(any());
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testStartAnimation() {
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -87,6 +96,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
         verify(mController).onAnimationFinished(mTransition);
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testChangesBehindStartingWindow() {
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -101,6 +111,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
         assertEquals(0, animator.getDuration());
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testTransitionTypeDragResize() {
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TASK_FRAGMENT_DRAG_RESIZE, 0)
@@ -115,8 +126,9 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
         assertEquals(0, animator.getDuration());
     }
 
+    @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
-    public void testInvalidCustomAnimation() {
+    public void testInvalidCustomAnimation_disableAnimationOptionsPerChange() {
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
                 .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
                 .build();
@@ -131,4 +143,22 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
         // An invalid custom animation is equivalent to jump-cut.
         assertEquals(0, animator.getDuration());
     }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testInvalidCustomAnimation_enableAnimationOptionsPerChange() {
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+                .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
+                .build();
+        info.getChanges().getFirst().setAnimationOptions(TransitionInfo.AnimationOptions
+                .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */,
+                        0 /* backgroundColor */, false /* overrideTaskTransition */));
+        final Animator animator = mAnimRunner.createAnimator(
+                info, mStartTransaction, mFinishTransaction,
+                () -> mFinishCallback.onTransitionFinished(null /* wct */),
+                new ArrayList<>());
+
+        // An invalid custom animation is equivalent to jump-cut.
+        assertEquals(0, animator.getDuration());
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index 974d69b2ac5d..39d55079ca3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -32,6 +32,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
 import android.animation.Animator;
 import android.animation.ValueAnimator;
 import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
 
@@ -39,9 +42,11 @@ import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.window.flags.Flags;
 import com.android.wm.shell.transition.TransitionInfoBuilder;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -59,6 +64,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
     private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500);
     private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500);
 
+    @Rule
+    public SetFlagsRule mRule = new SetFlagsRule();
+
     @Before
     public void setup() {
         super.setUp();
@@ -66,11 +74,13 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
                 any());
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testInstantiate() {
         verify(mShellInit).addInitCallback(any(), any());
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testOnInit() {
         mController.onInit();
@@ -78,6 +88,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verify(mTransitions).addHandler(mController);
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testSetAnimScaleSetting() {
         mController.setAnimScaleSetting(1.0f);
@@ -86,6 +97,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verify(mAnimSpec).setAnimScaleSetting(1.0f);
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testStartAnimation_containsNonActivityEmbeddingChange() {
         final TransitionInfo.Change nonEmbeddedOpen = createChange(0 /* flags */);
@@ -122,6 +134,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         assertFalse(info2.getChanges().contains(nonEmbeddedClose));
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() {
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -138,6 +151,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verifyNoMoreInteractions(mFinishCallback);
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testStartAnimation_containsActivityEmbeddingSplitChange() {
         // Change that occupies only part of the Task.
@@ -155,6 +169,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verifyNoMoreInteractions(mFinishTransaction);
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() {
         // Change that is entering ActivityEmbedding split.
@@ -171,6 +186,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verifyNoMoreInteractions(mFinishTransaction);
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() {
         // Change that is exiting ActivityEmbedding split.
@@ -187,8 +203,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verifyNoMoreInteractions(mFinishTransaction);
     }
 
+    @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
-    public void testShouldAnimate_containsAnimationOptions() {
+    public void testShouldAnimate_containsAnimationOptions_disableAnimOptionsPerChange() {
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
                 .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS))
                 .build();
@@ -206,6 +223,28 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         assertFalse(mController.shouldAnimate(info));
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testShouldAnimate_containsAnimationOptions_enableAnimOptionsPerChange() {
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+                .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS))
+                .build();
+        final TransitionInfo.Change change = info.getChanges().getFirst();
+
+        change.setAnimationOptions(TransitionInfo.AnimationOptions
+                .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */,
+                        0 /* backgroundColor */, false /* overrideTaskTransition */));
+        assertTrue(mController.shouldAnimate(info));
+
+        change.setAnimationOptions(TransitionInfo.AnimationOptions
+                .makeSceneTransitionAnimOptions());
+        assertFalse(mController.shouldAnimate(info));
+
+        change.setAnimationOptions(TransitionInfo.AnimationOptions.makeCrossProfileAnimOptions());
+        assertFalse(mController.shouldAnimate(info));
+    }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @UiThreadTest
     @Test
     public void testMergeAnimation() {
@@ -242,6 +281,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
         verify(mFinishCallback).onTransitionFinished(any());
     }
 
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
     @Test
     public void testOnAnimationFinished() {
         // Should not call finish when there is no transition.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 28369fa74527..d2b912e9a7e3 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -43,6 +43,7 @@ import static android.view.WindowManager.TransitionFlags;
 import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
 import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
+import static android.window.TransitionInfo.AnimationOptions;
 import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION;
 import static android.window.TransitionInfo.FLAG_CONFIG_AT_END;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
@@ -105,6 +106,7 @@ import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.window.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -318,6 +320,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
      */
     ArrayList<ActivityRecord> mConfigAtEndActivities = null;
 
+    @VisibleForTesting
     Transition(@TransitionType int type, @TransitionFlags int flags,
             TransitionController controller, BLASTSyncEngine syncEngine) {
         mType = type;
@@ -2668,6 +2671,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
             return out;
         }
 
+        final AnimationOptions animOptions = calculateAnimationOptionsForActivityTransition(type,
+                sortedTargets);
+        if (!Flags.moveAnimationOptionsToChange() && animOptions != null) {
+            out.setAnimationOptions(animOptions);
+        }
+
         // Convert all the resolved ChangeInfos into TransactionInfo.Change objects in order.
         final int count = sortedTargets.size();
         for (int i = 0; i < count; ++i) {
@@ -2757,6 +2766,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
                 change.setBackgroundColor(ColorUtils.setAlphaComponent(backgroundColor, 255));
             }
 
+            if (Flags.moveAnimationOptionsToChange() && activityRecord != null
+                    && animOptions != null) {
+                change.setAnimationOptions(animOptions);
+            }
+
             if (activityRecord != null) {
                 change.setActivityComponent(activityRecord.mActivityComponent);
             }
@@ -2768,11 +2782,26 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
 
             out.addChange(change);
         }
+        return out;
+    }
 
+    /**
+     * Calculates {@link AnimationOptions} for activity-to-activity transition.
+     * It returns a valid {@link AnimationOptions} if:
+     * <ul>
+     *   <li>the top animation target is an Activity</li>
+     *   <li>there's a {@link android.view.Window#setWindowAnimations(int)} and there's only
+     *     {@link WindowState}, {@link WindowToken} and {@link ActivityRecord} target</li>
+     * </ul>
+     * Otherwise, it returns {@code null}.
+     */
+    @Nullable
+    private static AnimationOptions calculateAnimationOptionsForActivityTransition(
+            @TransitionType int type, @NonNull ArrayList<ChangeInfo> sortedTargets) {
         TransitionInfo.AnimationOptions animOptions = null;
 
-        // Check if the top-most app is an activity (ie. activity->activity). If so, make sure to
-        // honor its custom transition options.
+        // Check if the top-most app is an activity (ie. activity->activity). If so, make sure
+        // to honor its custom transition options.
         WindowContainer<?> topApp = null;
         for (int i = 0; i < sortedTargets.size(); i++) {
             if (isWallpaper(sortedTargets.get(i).mContainer)) continue;
@@ -2781,16 +2810,18 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
         }
         if (topApp.asActivityRecord() != null) {
             final ActivityRecord topActivity = topApp.asActivityRecord();
-            animOptions = addCustomActivityTransition(topActivity, true/* open */, null);
-            animOptions = addCustomActivityTransition(topActivity, false/* open */, animOptions);
+            animOptions = addCustomActivityTransition(topActivity, true/* open */,
+                    null /* animOptions */);
+            animOptions = addCustomActivityTransition(topActivity, false/* open */,
+                    animOptions);
         }
         final WindowManager.LayoutParams animLp =
                 getLayoutParamsForAnimationsStyle(type, sortedTargets);
         if (animLp != null && animLp.type != TYPE_APPLICATION_STARTING
                 && animLp.windowAnimations != 0) {
-            // Don't send animation options if no windowAnimations have been set or if the we are
-            // running an app starting animation, in which case we don't want the app to be able to
-            // change its animation directly.
+            // Don't send animation options if no windowAnimations have been set or if the we
+            // are running an app starting animation, in which case we don't want the app to be
+            // able to change its animation directly.
             if (animOptions != null) {
                 animOptions.addOptionsFromLayoutParameters(animLp);
             } else {
@@ -2798,20 +2829,29 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
                         .makeAnimOptionsFromLayoutParameters(animLp);
             }
         }
-        if (animOptions != null) {
-            out.setAnimationOptions(animOptions);
-        }
-        return out;
+        return animOptions;
     }
 
-    static TransitionInfo.AnimationOptions addCustomActivityTransition(ActivityRecord topActivity,
-            boolean open, TransitionInfo.AnimationOptions animOptions) {
+    /**
+     * Returns {@link TransitionInfo.AnimationOptions} with custom Activity transition appended if
+     * {@code topActivity} specifies {@link ActivityRecord#getCustomAnimation(boolean)}, or
+     * {@code animOptions}, otherwise.
+     * <p>
+     * If the passed {@code animOptions} is {@code null}, this method will creates an
+     * {@link TransitionInfo.AnimationOptions} with custom animation appended
+     *
+     * @param open {@code true} to add a custom open animation, and {@false} to add a close one
+     */
+    @Nullable
+    private static TransitionInfo.AnimationOptions addCustomActivityTransition(
+            @NonNull ActivityRecord activity, boolean open,
+            @Nullable TransitionInfo.AnimationOptions animOptions) {
         final ActivityRecord.CustomAppTransition customAnim =
-                topActivity.getCustomAnimation(open);
+                activity.getCustomAnimation(open);
         if (customAnim != null) {
             if (animOptions == null) {
                 animOptions = TransitionInfo.AnimationOptions
-                        .makeCommonAnimOptions(topActivity.packageName);
+                        .makeCommonAnimOptions(activity.packageName);
             }
             animOptions.addCustomActivityTransition(open, customAnim.mEnterAnim,
                     customAnim.mExitAnim, customAnim.mBackgroundColor);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 698afaa8e8ab..78480f1465d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -81,7 +81,9 @@ import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.view.SurfaceControl;
@@ -100,7 +102,9 @@ import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.graphics.ColorUtils;
+import com.android.window.flags.Flags;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -116,6 +120,7 @@ import java.util.function.Function;
  * Build/Install/Run:
  *  atest WmTests:TransitionTests
  */
+@EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
 @SmallTest
 @Presubmit
 @RunWith(WindowTestRunner.class)
@@ -123,6 +128,8 @@ public class TransitionTests extends WindowTestsBase {
     final SurfaceControl.Transaction mMockT = mock(SurfaceControl.Transaction.class);
     private BLASTSyncEngine mSyncEngine;
 
+    @Rule
+    public SetFlagsRule mRule = new SetFlagsRule();
     private Transition createTestTransition(int transitType, TransitionController controller) {
         final Transition transition = new Transition(transitType, 0 /* flags */, controller,
                 controller.mSyncEngine);
-- 
GitLab