Merge changes from topic "cherrypicker-L06600000959260644:N82800001349763665" into udc-dev am: 05ee5fc955

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/22144003

Change-Id: Ifa1f8568f7f9e5ed07a1ad298d9537dd0294c99e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index 21ba403..12026aa 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -79,6 +79,13 @@
     private Object[] mCachedList;
 
     /**
+     * Tracks whether we've notified listeners of the onAnimationStart() event. This can be
+     * complex to keep track of since we notify listeners at different times depending on
+     * startDelay and whether start() was called before end().
+     */
+    boolean mStartListenersCalled = false;
+
+    /**
      * Sets the duration for delaying pausing animators when apps go into the background.
      * Used by AnimationHandler when requested to pause animators.
      *
@@ -165,7 +172,9 @@
      * @see AnimatorPauseListener
      */
     public void pause() {
-        if (isStarted() && !mPaused) {
+        // We only want to pause started Animators or animators that setCurrentPlayTime()
+        // have been called on. mStartListenerCalled will be true if seek has happened.
+        if ((isStarted() || mStartListenersCalled) && !mPaused) {
             mPaused = true;
             notifyPauseListeners(AnimatorCaller.ON_PAUSE);
         }
@@ -444,6 +453,7 @@
                 anim.mPauseListeners = new ArrayList<AnimatorPauseListener>(mPauseListeners);
             }
             anim.mCachedList = null;
+            anim.mStartListenersCalled = false;
             return anim;
         } catch (CloneNotSupportedException e) {
            throw new AssertionError();
@@ -608,6 +618,22 @@
         callOnList(mPauseListeners, notification, this, false);
     }
 
+    void notifyStartListeners(boolean isReversing) {
+        boolean startListenersCalled = mStartListenersCalled;
+        mStartListenersCalled = true;
+        if (mListeners != null && !startListenersCalled) {
+            notifyListeners(AnimatorCaller.ON_START, isReversing);
+        }
+    }
+
+    void notifyEndListeners(boolean isReversing) {
+        boolean startListenersCalled = mStartListenersCalled;
+        mStartListenersCalled = false;
+        if (mListeners != null && startListenersCalled) {
+            notifyListeners(AnimatorCaller.ON_END, isReversing);
+        }
+    }
+
     /**
      * Calls <code>call</code> for every item in <code>list</code> with <code>animator</code> and
      * <code>isReverse</code> as parameters.
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 09eec9d..60659dc 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -189,11 +189,6 @@
      */
     private long[] mChildStartAndStopTimes;
 
-    /**
-     * Tracks whether we've notified listeners of the onAnimationStart() event.
-     */
-    private boolean mStartListenersCalled;
-
     // This is to work around a bug in b/34736819. This needs to be removed once app team
     // fixes their side.
     private AnimatorListenerAdapter mAnimationEndListener = new AnimatorListenerAdapter() {
@@ -424,7 +419,7 @@
         if (Looper.myLooper() == null) {
             throw new AndroidRuntimeException("Animators may only be run on Looper threads");
         }
-        if (isStarted()) {
+        if (isStarted() || mStartListenersCalled) {
             notifyListeners(AnimatorCaller.ON_CANCEL, false);
             callOnPlayingSet(Animator::cancel);
             mPlayingSet.clear();
@@ -486,13 +481,13 @@
             return;
         }
         if (isStarted()) {
+            mStarted = false; // don't allow reentrancy
             // Iterate the animations that haven't finished or haven't started, and end them.
             if (mReversing) {
                 // Between start() and first frame, mLastEventId would be unset (i.e. -1)
                 mLastEventId = mLastEventId == -1 ? mEvents.size() : mLastEventId;
-                while (mLastEventId > 0) {
-                    mLastEventId = mLastEventId - 1;
-                    AnimationEvent event = mEvents.get(mLastEventId);
+                for (int eventId = mLastEventId - 1; eventId >= 0; eventId--) {
+                    AnimationEvent event = mEvents.get(eventId);
                     Animator anim = event.mNode.mAnimation;
                     if (mNodeMap.get(anim).mEnded) {
                         continue;
@@ -508,11 +503,10 @@
                     }
                 }
             } else {
-                while (mLastEventId < mEvents.size() - 1) {
+                for (int eventId = mLastEventId + 1; eventId < mEvents.size(); eventId++) {
                     // Avoid potential reentrant loop caused by child animators manipulating
                     // AnimatorSet's lifecycle (i.e. not a recommended approach).
-                    mLastEventId = mLastEventId + 1;
-                    AnimationEvent event = mEvents.get(mLastEventId);
+                    AnimationEvent event = mEvents.get(eventId);
                     Animator anim = event.mNode.mAnimation;
                     if (mNodeMap.get(anim).mEnded) {
                         continue;
@@ -527,7 +521,6 @@
                     }
                 }
             }
-            mPlayingSet.clear();
         }
         endAnimation();
     }
@@ -723,6 +716,10 @@
         if (Looper.myLooper() == null) {
             throw new AndroidRuntimeException("Animators may only be run on Looper threads");
         }
+        if (inReverse == mReversing && selfPulse == mSelfPulse && mStarted) {
+            // It is already started
+            return;
+        }
         mStarted = true;
         mSelfPulse = selfPulse;
         mPaused = false;
@@ -756,20 +753,6 @@
         }
     }
 
-    private void notifyStartListeners(boolean inReverse) {
-        if (mListeners != null && !mStartListenersCalled) {
-            notifyListeners(AnimatorCaller.ON_START, inReverse);
-        }
-        mStartListenersCalled = true;
-    }
-
-    private void notifyEndListeners(boolean inReverse) {
-        if (mListeners != null && mStartListenersCalled) {
-            notifyListeners(AnimatorCaller.ON_END, inReverse);
-        }
-        mStartListenersCalled = false;
-    }
-
     // Returns true if set is empty or contains nothing but animator sets with no start delay.
     private static boolean isEmptySet(AnimatorSet set) {
         if (set.getStartDelay() > 0) {
@@ -936,12 +919,18 @@
                                 lastPlayTime - node.mStartTime,
                                 notify
                         );
+                        if (notify) {
+                            mPlayingSet.remove(node);
+                        }
                     } else if (start <= currentPlayTime && currentPlayTime <= end) {
                         animator.animateSkipToEnds(
                                 currentPlayTime - node.mStartTime,
                                 lastPlayTime - node.mStartTime,
                                 notify
                         );
+                        if (notify && !mPlayingSet.contains(node)) {
+                            mPlayingSet.add(node);
+                        }
                     }
                 }
             }
@@ -969,12 +958,18 @@
                                 lastPlayTime - node.mStartTime,
                                 notify
                         );
+                        if (notify) {
+                            mPlayingSet.remove(node);
+                        }
                     } else if (start <= currentPlayTime && currentPlayTime <= end) {
                         animator.animateSkipToEnds(
                                 currentPlayTime - node.mStartTime,
                                 lastPlayTime - node.mStartTime,
                                 notify
                         );
+                        if (notify && !mPlayingSet.contains(node)) {
+                            mPlayingSet.add(node);
+                        }
                     }
                 }
             }
@@ -1115,8 +1110,8 @@
                 mSeekState.setPlayTime(0, mReversing);
             }
         }
-        animateBasedOnPlayTime(playTime, lastPlayTime, mReversing, true);
         mSeekState.setPlayTime(playTime, mReversing);
+        animateBasedOnPlayTime(playTime, lastPlayTime, mReversing, true);
     }
 
     /**
@@ -1498,7 +1493,6 @@
         anim.mNodeMap = new ArrayMap<Animator, Node>();
         anim.mNodes = new ArrayList<Node>(nodeCount);
         anim.mEvents = new ArrayList<AnimationEvent>();
-        anim.mStartListenersCalled = false;
         anim.mAnimationEndListener = new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 719f596..5d69f8b 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -199,13 +199,6 @@
     private boolean mStarted = false;
 
     /**
-     * Tracks whether we've notified listeners of the onAnimationStart() event. This can be
-     * complex to keep track of since we notify listeners at different times depending on
-     * startDelay and whether start() was called before end().
-     */
-    private boolean mStartListenersCalled = false;
-
-    /**
      * Flag that denotes whether the animation is set up and ready to go. Used to
      * set up animation that has not yet been started.
      */
@@ -1108,20 +1101,6 @@
         }
     }
 
-    private void notifyStartListeners(boolean isReversing) {
-        if (mListeners != null && !mStartListenersCalled) {
-            notifyListeners(AnimatorCaller.ON_START, isReversing);
-        }
-        mStartListenersCalled = true;
-    }
-
-    private void notifyEndListeners(boolean isReversing) {
-        if (mListeners != null && mStartListenersCalled) {
-            notifyListeners(AnimatorCaller.ON_END, isReversing);
-        }
-        mStartListenersCalled = false;
-    }
-
     /**
      * Start the animation playing. This version of start() takes a boolean flag that indicates
      * whether the animation should play in reverse. The flag is usually false, but may be set
@@ -1139,6 +1118,10 @@
         if (Looper.myLooper() == null) {
             throw new AndroidRuntimeException("Animators may only be run on Looper threads");
         }
+        if (playBackwards == mResumed && mSelfPulse == !mSuppressSelfPulseRequested && mStarted) {
+            // already started
+            return;
+        }
         mReversing = playBackwards;
         mSelfPulse = !mSuppressSelfPulseRequested;
         // Special case: reversing from seek-to-0 should act as if not seeked at all.
@@ -1209,7 +1192,7 @@
         // Only cancel if the animation is actually running or has been started and is about
         // to run
         // Only notify listeners if the animator has actually started
-        if ((mStarted || mRunning) && mListeners != null) {
+        if ((mStarted || mRunning || mStartListenersCalled) && mListeners != null) {
             if (!mRunning) {
                 // If it's not yet running, then start listeners weren't called. Call them now.
                 notifyStartListeners(mReversing);
@@ -1217,7 +1200,6 @@
             notifyListeners(AnimatorCaller.ON_CANCEL, false);
         }
         endAnimation();
-
     }
 
     @Override
@@ -1320,11 +1302,11 @@
             // If it's not yet running, then start listeners weren't called. Call them now.
             notifyStartListeners(mReversing);
         }
-        mRunning = false;
-        mStarted = false;
         mLastFrameTime = -1;
         mFirstFrameTime = -1;
         mStartTime = -1;
+        mRunning = false;
+        mStarted = false;
         notifyEndListeners(mReversing);
         // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
         mReversing = false;
@@ -1687,7 +1669,6 @@
         anim.mRunning = false;
         anim.mPaused = false;
         anim.mResumed = false;
-        anim.mStartListenersCalled = false;
         anim.mStartTime = -1;
         anim.mStartTimeCommitted = false;
         anim.mAnimationEndRequested = false;
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java b/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java
index 7a1de0c..a753870 100644
--- a/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java
+++ b/core/tests/coretests/src/android/animation/AnimatorSetActivityTest.java
@@ -435,9 +435,11 @@
         mActivityRule.runOnUiThread(s::start);
 
         while (!listener.endIsCalled) {
-            boolean passedStartDelay = a1.isStarted() || a2.isStarted() || a3.isStarted() ||
-                    a4.isStarted() || a5.isStarted();
-            assertEquals(passedStartDelay, s.isRunning());
+            mActivityRule.runOnUiThread(() -> {
+                boolean passedStartDelay = a1.isStarted() || a2.isStarted() || a3.isStarted()
+                        || a4.isStarted() || a5.isStarted();
+                assertEquals(passedStartDelay, s.isRunning());
+            });
             Thread.sleep(50);
         }
         assertFalse(s.isRunning());
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
index 22da0aa..43266a5 100644
--- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
+++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
@@ -17,6 +17,7 @@
 package android.animation;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.util.PollingCheck;
 import android.view.View;
@@ -31,6 +32,8 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 @MediumTest
 public class AnimatorSetCallsTest {
@@ -40,6 +43,7 @@
 
     private AnimatorSetActivity mActivity;
     private AnimatorSet mSet1;
+    private AnimatorSet mSet2;
     private ObjectAnimator mAnimator;
     private CountListener mListener1;
     private CountListener mListener2;
@@ -56,10 +60,10 @@
             mSet1.addListener(mListener1);
             mSet1.addPauseListener(mListener1);
 
-            AnimatorSet set2 = new AnimatorSet();
+            mSet2 = new AnimatorSet();
             mListener2 = new CountListener();
-            set2.addListener(mListener2);
-            set2.addPauseListener(mListener2);
+            mSet2.addListener(mListener2);
+            mSet2.addPauseListener(mListener2);
 
             mAnimator = ObjectAnimator.ofFloat(square, "translationX", 0f, 100f);
             mListener3 = new CountListener();
@@ -67,8 +71,8 @@
             mAnimator.addPauseListener(mListener3);
             mAnimator.setDuration(1);
 
-            set2.play(mAnimator);
-            mSet1.play(set2);
+            mSet2.play(mAnimator);
+            mSet1.play(mSet2);
         });
     }
 
@@ -175,6 +179,7 @@
         assertEquals(1, updateValues.size());
         assertEquals(0f, updateValues.get(0), 0f);
     }
+
     @Test
     public void updateOnlyWhileRunning() {
         ArrayList<Float> updateValues = new ArrayList<>();
@@ -207,6 +212,226 @@
         }
     }
 
+    @Test
+    public void pauseResumeSeekingAnimators() {
+        ValueAnimator animator2 = ValueAnimator.ofFloat(0f, 1f);
+        mSet2.play(animator2).after(mAnimator);
+        mSet2.setStartDelay(100);
+        mSet1.setStartDelay(100);
+        mAnimator.setDuration(100);
+
+        mActivity.runOnUiThread(() -> {
+            mSet1.setCurrentPlayTime(0);
+            mSet1.pause();
+
+            // only startForward and pause should have been called once
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 0
+            );
+            mListener2.assertValues(
+                    0, 0, 0, 0, 0, 0, 0, 0
+            );
+            mListener3.assertValues(
+                    0, 0, 0, 0, 0, 0, 0, 0
+            );
+
+            mSet1.resume();
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 1
+            );
+            mListener2.assertValues(
+                    0, 0, 0, 0, 0, 0, 0, 0
+            );
+            mListener3.assertValues(
+                    0, 0, 0, 0, 0, 0, 0, 0
+            );
+
+            mSet1.setCurrentPlayTime(200);
+
+            // resume and endForward should have been called once
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 1
+            );
+            mListener2.assertValues(
+                    1, 0, 0, 0, 0, 0, 0, 0
+            );
+            mListener3.assertValues(
+                    1, 0, 0, 0, 0, 0, 0, 0
+            );
+
+            mSet1.pause();
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 2, 1
+            );
+            mListener2.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 0
+            );
+            mListener3.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 0
+            );
+            mSet1.resume();
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 2, 2
+            );
+            mListener2.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 1
+            );
+            mListener3.assertValues(
+                    1, 0, 0, 0, 0, 0, 1, 1
+            );
+
+            // now go to animator2
+            mSet1.setCurrentPlayTime(400);
+            mSet1.pause();
+            mSet1.resume();
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 3, 3
+            );
+            mListener2.assertValues(
+                    1, 0, 0, 0, 0, 0, 2, 2
+            );
+            mListener3.assertValues(
+                    1, 0, 1, 0, 0, 0, 1, 1
+            );
+
+            // now go back to mAnimator
+            mSet1.setCurrentPlayTime(250);
+            mSet1.pause();
+            mSet1.resume();
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 4, 4
+            );
+            mListener2.assertValues(
+                    1, 0, 0, 0, 0, 0, 3, 3
+            );
+            mListener3.assertValues(
+                    1, 1, 1, 0, 0, 0, 2, 2
+            );
+
+            // now go back to before mSet2 was being run
+            mSet1.setCurrentPlayTime(1);
+            mSet1.pause();
+            mSet1.resume();
+            mListener1.assertValues(
+                    1, 0, 0, 0, 0, 0, 5, 5
+            );
+            mListener2.assertValues(
+                    1, 0, 0, 1, 0, 0, 3, 3
+            );
+            mListener3.assertValues(
+                    1, 1, 1, 1, 0, 0, 2, 2
+            );
+        });
+    }
+
+    @Test
+    public void endInCancel() throws Throwable {
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mSet1.end();
+            }
+        };
+        mSet1.addListener(listener);
+        mActivity.runOnUiThread(() -> {
+            mSet1.start();
+            mSet1.cancel();
+            // Should go to the end value
+            View square = mActivity.findViewById(R.id.square1);
+            assertEquals(100f, square.getTranslationX(), 0.001f);
+        });
+    }
+
+    @Test
+    public void reentrantStart() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(3);
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation, boolean isReverse) {
+                mSet1.start();
+                latch.countDown();
+            }
+        };
+        mSet1.addListener(listener);
+        mSet2.addListener(listener);
+        mAnimator.addListener(listener);
+        mActivity.runOnUiThread(() -> mSet1.start());
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+        mActivity.runOnUiThread(() -> {});
+    }
+
+    @Test
+    public void reentrantEnd() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(3);
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation, boolean isReverse) {
+                mSet1.end();
+                latch.countDown();
+            }
+        };
+        mSet1.addListener(listener);
+        mSet2.addListener(listener);
+        mAnimator.addListener(listener);
+        mActivity.runOnUiThread(() -> {
+            mSet1.start();
+            mSet1.end();
+        });
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+        mActivity.runOnUiThread(() -> {});
+    }
+
+    @Test
+    public void reentrantPause() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(3);
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationPause(Animator animation) {
+                mSet1.pause();
+                latch.countDown();
+            }
+        };
+        mSet1.addPauseListener(listener);
+        mSet2.addPauseListener(listener);
+        mAnimator.addPauseListener(listener);
+        mActivity.runOnUiThread(() -> {
+            mSet1.start();
+            mSet1.pause();
+        });
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+        mActivity.runOnUiThread(() -> {});
+    }
+
+    @Test
+    public void reentrantResume() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(3);
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationResume(Animator animation) {
+                mSet1.resume();
+                latch.countDown();
+            }
+        };
+        mSet1.addPauseListener(listener);
+        mSet2.addPauseListener(listener);
+        mAnimator.addPauseListener(listener);
+        mActivity.runOnUiThread(() -> {
+            mSet1.start();
+            mSet1.pause();
+            mSet1.resume();
+        });
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread hasn't been destroyed by a stack overflow...
+        mActivity.runOnUiThread(() -> {});
+    }
+
     private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) {
         final boolean[] value = new boolean[1];
         PollingCheck.waitFor(() -> {
@@ -238,16 +463,16 @@
                 int pause,
                 int resume
         ) {
-            assertEquals(0, startNoParam);
-            assertEquals(0, endNoParam);
-            assertEquals(startForward, this.startForward);
-            assertEquals(startReverse, this.startReverse);
-            assertEquals(endForward, this.endForward);
-            assertEquals(endReverse, this.endReverse);
-            assertEquals(cancel, this.cancel);
-            assertEquals(repeat, this.repeat);
-            assertEquals(pause, this.pause);
-            assertEquals(resume, this.resume);
+            assertEquals("onAnimationStart() without direction", 0, startNoParam);
+            assertEquals("onAnimationEnd() without direction", 0, endNoParam);
+            assertEquals("onAnimationStart(forward)", startForward, this.startForward);
+            assertEquals("onAnimationStart(reverse)", startReverse, this.startReverse);
+            assertEquals("onAnimationEnd(forward)", endForward, this.endForward);
+            assertEquals("onAnimationEnd(reverse)", endReverse, this.endReverse);
+            assertEquals("onAnimationCancel()", cancel, this.cancel);
+            assertEquals("onAnimationRepeat()", repeat, this.repeat);
+            assertEquals("onAnimationPause()", pause, this.pause);
+            assertEquals("onAnimationResume()", resume, this.resume);
         }
 
         @Override
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index dee0a3e..a53d57f 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -40,6 +40,8 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
@@ -1067,6 +1069,64 @@
         });
     }
 
+    @Test
+    public void reentrantStart() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(1);
+        a1.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation, boolean isReverse) {
+                a1.start();
+                latch.countDown();
+            }
+        });
+        mActivityRule.runOnUiThread(() -> a1.start());
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread isn't blocked by an infinite loop:
+        mActivityRule.runOnUiThread(() -> {});
+    }
+
+    @Test
+    public void reentrantPause() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(1);
+        a1.addPauseListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationPause(Animator animation) {
+                a1.pause();
+                latch.countDown();
+            }
+        });
+        mActivityRule.runOnUiThread(() -> {
+            a1.start();
+            a1.pause();
+        });
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread isn't blocked by an infinite loop:
+        mActivityRule.runOnUiThread(() -> {});
+    }
+
+    @Test
+    public void reentrantResume() throws Throwable {
+        CountDownLatch latch = new CountDownLatch(1);
+        a1.addPauseListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationResume(Animator animation) {
+                a1.resume();
+                latch.countDown();
+            }
+        });
+        mActivityRule.runOnUiThread(() -> {
+            a1.start();
+            a1.pause();
+            a1.resume();
+        });
+        assertTrue(latch.await(1, TimeUnit.SECONDS));
+
+        // Make sure that the UI thread isn't blocked by an infinite loop:
+        mActivityRule.runOnUiThread(() -> {});
+    }
+
     class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener {
         boolean wasRunning = false;
         long firstRunningFrameTime = -1;