Move all Fragment animations in the same direction

Say you have 3 fragments, A, B, and C. If you add A and B with one set
of entering and exiting animations. And then pop B, before adding C with
an different set of entering and exiting animations while using the
recommended setReorderingAllowed=true flag on your fragment
transactions, it is possible to get in a scenario where the visible
exiting fragment, B, uses the animations from the popping opertion to A
instead of the adding operation with C.

The reason for this is that the animations for B are set during the
first operation with the pop from B to A. The system is then
techinically doing a replace operation from A to C, since B has already
been popped, so it sets the proper animations on A and leaves B
untouched. This causes B to run the stale animations from the pop
instead of the current animations from the replace.

We should ensure that we always run the animations from the operation
that determines what will be visible to user on the screen.

RelNote: "Fragments will now run the proper animations when mixing pop
and replace operations."
Test: added FragmentAnimationTest
Bug: 214835303

Change-Id: Ib1c07bd0e05c0c1a3d785e29456bad9afb183e2d
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
index a595660..4fc400a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
@@ -863,6 +863,112 @@
         assertThat(fragment2.loadedAnimation).isEqualTo(POP_EXIT)
     }
 
+    @Test
+    fun ensureProperAnimationOnPopUpAndReplace() {
+        waitForAnimationReady()
+        val fm = activityRule.activity.supportFragmentManager
+        val fragment1 = AnimationFragment()
+        val fragment2 = AnimationFragment()
+        val fragment3 = AnimationFragment()
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER_OTHER, EXIT_OTHER)
+            .add(R.id.fragmentContainer, fragment1, "fragment1")
+            .setPrimaryNavigationFragment(fragment1)
+            .addToBackStack("fragment1")
+            .commit()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(ENTER_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(0)
+        assertThat(fragment3.loadedAnimation).isEqualTo(0)
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER_OTHER, EXIT_OTHER)
+            .replace(R.id.fragmentContainer, fragment2, "fragment2")
+            .setPrimaryNavigationFragment(fragment2)
+            .addToBackStack("fragment2")
+            .commit()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(EXIT_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(ENTER_OTHER)
+        assertThat(fragment3.loadedAnimation).isEqualTo(0)
+
+        fm.popBackStack()
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER, EXIT)
+            .replace(R.id.fragmentContainer, fragment3, "fragment3")
+            .setPrimaryNavigationFragment(fragment3)
+            .addToBackStack("fragment3")
+            .commit()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(EXIT_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(EXIT)
+        assertThat(fragment3.loadedAnimation).isEqualTo(ENTER)
+    }
+
+    @Test
+    fun ensureProperAnimationOnDoublePop() {
+        waitForAnimationReady()
+        val fm = activityRule.activity.supportFragmentManager
+        val fragment1 = AnimationFragment()
+        val fragment2 = AnimationFragment()
+        val fragment3 = AnimationFragment()
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER_OTHER, EXIT_OTHER, ENTER_OTHER, EXIT_OTHER)
+            .add(R.id.fragmentContainer, fragment1, "fragment1")
+            .setPrimaryNavigationFragment(fragment1)
+            .addToBackStack("fragment1")
+            .commit()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(ENTER_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(0)
+        assertThat(fragment3.loadedAnimation).isEqualTo(0)
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER_OTHER, EXIT_OTHER, ENTER_OTHER, EXIT_OTHER)
+            .replace(R.id.fragmentContainer, fragment2, "fragment2")
+            .setPrimaryNavigationFragment(fragment2)
+            .addToBackStack("fragment2")
+            .commit()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(EXIT_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(ENTER_OTHER)
+        assertThat(fragment3.loadedAnimation).isEqualTo(0)
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER, EXIT, ENTER, EXIT)
+            .replace(R.id.fragmentContainer, fragment3, "fragment3")
+            .setPrimaryNavigationFragment(fragment3)
+            .addToBackStack("fragment3")
+            .commit()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(EXIT_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(EXIT)
+        assertThat(fragment3.loadedAnimation).isEqualTo(ENTER)
+
+        fm.popBackStack()
+        fm.popBackStack()
+        activityRule.waitForExecution()
+
+        assertThat(fragment1.loadedAnimation).isEqualTo(ENTER_OTHER)
+        assertThat(fragment2.loadedAnimation).isEqualTo(EXIT)
+        assertThat(fragment3.loadedAnimation).isEqualTo(EXIT_OTHER)
+    }
+
     private fun assertEnterPopExit(fragment: AnimationFragment) {
         assertFragmentAnimation(fragment, 1, true, ENTER)
 
@@ -1070,6 +1176,10 @@
         private val POP_ENTER = 3
         @AnimRes
         private val POP_EXIT = 4
+        @AnimRes
+        private val ENTER_OTHER = 5
+        @AnimRes
+        private val EXIT_OTHER = 6
     }
 }
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
index c2f2434..5c30741 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
@@ -87,6 +87,9 @@
         List<TransitionInfo> transitions = new ArrayList<>();
         final List<Operation> awaitingContainerChanges = new ArrayList<>(operations);
 
+        // sync animations together before we start loading them.
+        syncAnimations(operations);
+
         for (final Operation operation : operations) {
             // Create the animation CancellationSignal
             CancellationSignal animCancellationSignal = new CancellationSignal();
@@ -133,6 +136,25 @@
         }
     }
 
+    /**
+     * Syncs the animations of all other operations with the animations of the last operation.
+     */
+    private void syncAnimations(@NonNull List<Operation> operations) {
+        // get the last operation's fragment
+        Fragment lastOpFragment = operations.get(operations.size() - 1).getFragment();
+        // change the animations of all other fragments to match the last one.
+        for (final Operation operation : operations) {
+            operation.getFragment().mAnimationInfo.mEnterAnim =
+                    lastOpFragment.mAnimationInfo.mEnterAnim;
+            operation.getFragment().mAnimationInfo.mExitAnim =
+                    lastOpFragment.mAnimationInfo.mExitAnim;
+            operation.getFragment().mAnimationInfo.mPopEnterAnim =
+                    lastOpFragment.mAnimationInfo.mPopEnterAnim;
+            operation.getFragment().mAnimationInfo.mPopExitAnim =
+                    lastOpFragment.mAnimationInfo.mPopExitAnim;
+        }
+    }
+
     private void startAnimations(@NonNull List<AnimationInfo> animationInfos,
             @NonNull List<Operation> awaitingContainerChanges,
             boolean startedAnyTransition, @NonNull Map<Operation, Boolean> startedTransitions) {