/*
* Copyright (C) 2015 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 android.animation;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.Choreographer;
import android.view.animation.LinearInterpolator;

import java.util.ArrayList;

import static android.test.MoreAsserts.assertNotEqual;

public class ValueAnimatorTests extends ActivityInstrumentationTestCase2<BasicAnimatorActivity> {
    private static final long WAIT_TIME_OUT = 5000;
    private ValueAnimator a1;
    private ValueAnimator a2;

    // Tolerance of error in calculations related to duration, frame time, etc. due to frame delay.
    private final static long TOLERANCE = 100; // ms
    private final static long POLL_INTERVAL = 100; // ms

    private final static float A1_START_VALUE = 0f;
    private final static float A1_END_VALUE = 1f;
    private final static int A2_START_VALUE = 100;
    private final static int A2_END_VALUE = 200;

    private final static long DEFAULT_FRAME_INTERVAL = 5; //ms
    private final static long COMMIT_DELAY = 3; //ms

    public ValueAnimatorTests() {
        super(BasicAnimatorActivity.class);
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        a1 = ValueAnimator.ofFloat(A1_START_VALUE, A1_END_VALUE).setDuration(300);
        a2 = ValueAnimator.ofInt(A2_START_VALUE, A2_END_VALUE).setDuration(500);
    }

    @Override
    public void tearDown() throws Exception {
        a1 = null;
        a2 = null;
        super.tearDown();
    }

    @SmallTest
    public void testStartDelay() throws Throwable {
        final ValueAnimator a = ValueAnimator.ofFloat(5f, 20f);
        assertEquals(a.getStartDelay(), 0);
        final long delay = 200;
        a.setStartDelay(delay);
        assertEquals(a.getStartDelay(), delay);

        final MyUpdateListener listener = new MyUpdateListener();
        a.addUpdateListener(listener);
        final long[] startTime = new long[1];

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Test the time between isRunning() and isStarted()
                assertFalse(a.isStarted());
                assertFalse(a.isRunning());
                a.start();
                startTime[0] = SystemClock.uptimeMillis();
                assertTrue(a.isStarted());
                assertFalse(a.isRunning());
            }
        });

        Thread.sleep(a.getTotalDuration());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(listener.wasRunning);
                assertTrue(listener.firstRunningFrameTime - startTime[0] >= delay);
            }
        });

        Thread.sleep(a.getTotalDuration());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(a.isStarted());
            }
        });
    }

    @SmallTest
    public void testListenerCallbacks() throws Throwable {
        final MyListener l1 = new MyListener();
        final MyListener l2 = new MyListener();
        a1.addListener(l1);
        a2.addListener(l2);
        a2.setStartDelay(400);

        assertFalse(l1.startCalled);
        assertFalse(l1.cancelCalled);
        assertFalse(l1.endCalled);
        assertFalse(l2.startCalled);
        assertFalse(l2.cancelCalled);
        assertFalse(l2.endCalled);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.start();
                a2.start();
            }
        });

        long wait = 0;
        Thread.sleep(POLL_INTERVAL);
        wait += POLL_INTERVAL;

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.cancelCalled);
                a1.cancel();
                assertTrue(l1.cancelCalled);
                assertTrue(l1.endCalled);
            }
        });

        while (wait < a2.getStartDelay()) {
            runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // Make sure a2's start listener isn't called during start delay.
                    assertTrue(l1.startCalled);
                    assertFalse(l2.startCalled);
                }
            });
            Thread.sleep(POLL_INTERVAL);
            wait += POLL_INTERVAL;
        }

        long delay = Math.max(a1.getTotalDuration(), a2.getTotalDuration()) + TOLERANCE;
        Thread.sleep(delay);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // a1 is canceled.
                assertTrue(l1.startCalled);
                assertTrue(l1.cancelCalled);
                assertTrue(l1.endCalled);

                // a2 is supposed to finish normally
                assertTrue(l2.startCalled);
                assertFalse(l2.cancelCalled);
                assertTrue(l2.endCalled);
            }
        });
    }

    @SmallTest
    public void testIsStarted() throws Throwable {
        assertFalse(a1.isStarted());
        assertFalse(a2.isStarted());
        assertFalse(a1.isRunning());
        assertFalse(a2.isRunning());
        final long startDelay = 150;
        a1.setStartDelay(startDelay);
        final long[] startTime = new long[1];

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.start();
                a2.start();
                startTime[0] = SystemClock.uptimeMillis();
                assertTrue(a1.isStarted());
                assertTrue(a2.isStarted());
            }
        });
        long delayMs = 0;
        while (delayMs < startDelay) {
            Thread.sleep(POLL_INTERVAL);
            delayMs += POLL_INTERVAL;
            runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (SystemClock.uptimeMillis() - startTime[0] < startDelay) {
                        assertFalse(a1.isRunning());
                    }
                }
            });
        }

        Thread.sleep(startDelay);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(a1.isRunning());
                assertTrue(a2.isRunning());
            }
        });

        long delay = Math.max(a1.getTotalDuration(), a2.getTotalDuration()) * 2;
        Thread.sleep(delay);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(a1.isStarted());
                assertFalse(a1.isRunning());
                assertFalse(a2.isStarted());
                assertFalse(a2.isRunning());
            }
        });
    }

    @SmallTest
    public void testPause() throws Throwable {
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(a1.isPaused());
                assertFalse(a2.isPaused());

                a1.start();
                a2.start();

                assertFalse(a1.isPaused());
                assertFalse(a2.isPaused());
                assertTrue(a1.isStarted());
                assertTrue(a2.isStarted());
            }
        });

        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(a1.isRunning());
                assertTrue(a2.isRunning());
                a1.pause();
                assertTrue(a1.isPaused());
                assertFalse(a2.isPaused());
                assertTrue(a1.isRunning());
            }
        });

        Thread.sleep(a2.getTotalDuration());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // By this time, a2 should have finished, and a1 is still paused
                assertFalse(a2.isStarted());
                assertFalse(a2.isRunning());
                assertTrue(a1.isStarted());
                assertTrue(a1.isRunning());
                assertTrue(a1.isPaused());

                a1.resume();
            }
        });

        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(a1.isRunning());
                assertTrue(a1.isStarted());
                assertFalse(a1.isPaused());
            }
        });

        Thread.sleep(a1.getTotalDuration());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // a1 should finish by now.
                assertFalse(a1.isRunning());
                assertFalse(a1.isStarted());
                assertFalse(a1.isPaused());
            }
        });

    }

    @SmallTest
    public void testPauseListener() throws Throwable {
        MyPauseListener l1 = new MyPauseListener();
        MyPauseListener l2 = new MyPauseListener();
        a1.addPauseListener(l1);
        a2.addPauseListener(l2);

        assertFalse(l1.pauseCalled);
        assertFalse(l1.resumeCalled);
        assertFalse(l2.pauseCalled);
        assertFalse(l2.resumeCalled);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.start();
                a2.start();
            }
        });

        Thread.sleep(a1.getTotalDuration() / 2);
        a1.pause();

        Thread.sleep(a2.getTotalDuration());

        // Only a1's pause listener should be called.
        assertTrue(l1.pauseCalled);
        assertFalse(l1.resumeCalled);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.resume();
            }
        });

        Thread.sleep(a1.getTotalDuration());

        assertTrue(l1.pauseCalled);
        assertTrue(l1.resumeCalled);
        assertFalse(l2.pauseCalled);
        assertFalse(l2.resumeCalled);
    }

    @SmallTest
    public void testResume() throws Throwable {
        final MyUpdateListener l1 = new MyUpdateListener();
        final long totalDuration = a1.getTotalDuration();
        a1.addUpdateListener(l1);
        // Set a longer duration on a1 for this test
        a1.setDuration(1000);
        assertTrue(l1.firstRunningFrameTime < 0);
        assertTrue(l1.lastUpdateTime < 0);

        final long[] lastUpdate = new long[1];

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.start();
            }
        });

        Thread.sleep(totalDuration / 2);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(l1.firstRunningFrameTime > 0);
                assertTrue(l1.lastUpdateTime > l1.firstRunningFrameTime);
                lastUpdate[0] = l1.lastUpdateTime;
                a1.pause();
            }
        });

        Thread.sleep(totalDuration);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // There should be no update after pause()
                assertEquals(lastUpdate[0], l1.lastUpdateTime);
                a1.resume();
            }
        });

        do {
            Thread.sleep(POLL_INTERVAL);
            runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    assertTrue(l1.lastUpdateTime > lastUpdate[0]);
                    lastUpdate[0] = l1.lastUpdateTime;
                }
            });
        } while (!a1.isStarted());

        // Time between pause and resume: totalDuration
        long entireSpan = totalDuration * 2;
        long frameDelta = l1.lastUpdateTime - l1.firstRunningFrameTime;
        assertTrue(Math.abs(entireSpan - frameDelta) < TOLERANCE);
    }

    @SmallTest
    public void testEnd() throws Throwable {
        final MyListener l1 = new MyListener();
        final MyListener l2 = new MyListener();
        a1.addListener(l1);
        a2.addListener(l2);
        a1.addListener(new MyListener() {
            @Override
            public void onAnimationEnd(Animator anim) {
                anim.cancel();
            }
        });
        a2.addListener(new MyListener() {
            @Override
            public void onAnimationCancel(Animator anim) {
                anim.end();
            }
        });

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.cancelCalled);
                assertFalse(l1.endCalled);
                assertFalse(l2.cancelCalled);
                assertFalse(l2.endCalled);
                a1.start();
                a2.start();
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.end();
                a2.cancel();
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Calling cancel from onAnimationEnd will be ignored.
                assertFalse(l1.cancelCalled);
                assertTrue(l1.endCalled);
                assertTrue(l2.cancelCalled);
                assertTrue(l2.endCalled);

                float value1 = (Float) a1.getAnimatedValue();
                int value2 = (Integer) a2.getAnimatedValue();
                assertEquals(A1_END_VALUE, value1);
                assertEquals(A2_END_VALUE, value2);
            }
        });

    }

    @SmallTest
    public void testEndValue() throws Throwable {
        final MyListener l1 = new MyListener();
        a1.addListener(l1);

        final MyListener l2 = new MyListener();
        a2.addListener(l2);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.start();
                a2.start();
            }
        });

        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Animation has started but not finished, check animated values against end values
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);
                assertNotEqual(A1_END_VALUE, a1.getAnimatedValue());
                assertNotEqual(A1_END_VALUE, a2.getAnimatedValue());

                // Force a2 to end.
                a2.end();
            }
        });

        Thread.sleep(a1.getTotalDuration());

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.cancelCalled);
                assertTrue(l1.endCalled);
                assertFalse(l2.cancelCalled);
                assertTrue(l2.endCalled);

                // By now a1 should have finished normally and a2 has skipped to the end, check
                // their end values.
                assertEquals(A1_END_VALUE, ((Float) (a1.getAnimatedValue())).floatValue());
                assertEquals(A2_END_VALUE, ((Integer) (a2.getAnimatedValue())).intValue());
            }
        });
    }

    @SmallTest
    public void testUpdateListener() throws InterruptedException {

        final MyFrameCallbackProvider provider = new MyFrameCallbackProvider();
        long sleep = 0;
        while (provider.mHandler == null) {
            Thread.sleep(POLL_INTERVAL);
            sleep += POLL_INTERVAL;
            if (sleep > WAIT_TIME_OUT) {
                break;
            }
        }
        // Either the looper has started, or timed out
        assertNotNull(provider.mHandler);

        final MyListener listener = new MyListener();
        final MyUpdateListener l1 = new MyUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                long currentTime = SystemClock.uptimeMillis();
                long frameDelay = provider.getFrameDelay();
                if (lastUpdateTime > 0) {
                    // Error tolerance here is one frame.
                    assertTrue((currentTime - lastUpdateTime) < frameDelay * 2);
                } else {
                    // First frame:
                    assertTrue(listener.startCalled);
                    assertTrue(listener.startTime > 0);
                    assertTrue(currentTime - listener.startTime < frameDelay * 2);
                }
                super.onAnimationUpdate(animation);
            }
        };
        a1.addUpdateListener(l1);
        a1.addListener(listener);
        a1.setStartDelay(100);

        provider.mHandler.post(new Runnable() {
            @Override
            public void run() {
                AnimationHandler.getInstance().setProvider(provider);
                a1.start();
            }
        });
        Thread.sleep(POLL_INTERVAL);
        assertTrue(a1.isStarted());
        Thread.sleep(a1.getTotalDuration() + TOLERANCE);
        // Finished by now.
        assertFalse(a1.isStarted());
        assertTrue(listener.endTime > 0);

        // Check the time difference between last frame and end time.
        assertTrue(listener.endTime >= l1.lastUpdateTime);
        assertTrue(listener.endTime - l1.lastUpdateTime < 2 * provider.getFrameDelay());
    }


    @SmallTest
    public void testConcurrentModification() throws Throwable {
        // Attempt to modify list of animations as the list is being iterated
        final ValueAnimator a0 = ValueAnimator.ofInt(100, 200).setDuration(500);
        final ValueAnimator a3 = ValueAnimator.ofFloat(0, 1).setDuration(500);
        final ValueAnimator a4 = ValueAnimator.ofInt(200, 300).setDuration(500);
        final MyListener listener = new MyListener() {
            @Override
            public void onAnimationEnd(Animator anim) {
                super.onAnimationEnd(anim);
                // AnimationHandler should be iterating the list at the moment, end/cancel all
                // the other animations. No ConcurrentModificationException should happen.
                a0.cancel();
                a1.end();
                a3.end();
                a4.cancel();
            }
        };
        a2.addListener(listener);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a0.start();
                a1.start();
                a2.start();
                a3.start();
                a4.start();
            }
        });
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(a0.isStarted());
                assertTrue(a1.isStarted());
                assertTrue(a2.isStarted());
                assertTrue(a3.isStarted());
                assertTrue(a4.isStarted());
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // End the animator that should be in the middle of the list.
                a2.end();
            }
        });
        Thread.sleep(POLL_INTERVAL);
        assertTrue(listener.endCalled);
        assertFalse(a0.isStarted());
        assertFalse(a1.isStarted());
        assertFalse(a2.isStarted());
        assertFalse(a3.isStarted());
        assertFalse(a4.isStarted());
    }

    @SmallTest
    public void testSeek() throws Throwable {
        final MyListener l1 = new MyListener();
        final MyListener l2 = new MyListener();
        final MyUpdateListener updateListener1 = new MyUpdateListener();
        final MyUpdateListener updateListener2 = new MyUpdateListener();
        final float a1StartFraction = 0.2f;
        final float a2StartFraction = 0.3f;

        // Extend duration so we have plenty of latitude to manipulate the animations when they
        // are running.
        a1.setDuration(1000);
        a2.setDuration(1000);
        a1.addListener(l1);
        a2.addListener(l2);
        a1.addUpdateListener(updateListener1);
        a2.addUpdateListener(updateListener2);
        TimeInterpolator interpolator = new LinearInterpolator();
        a1.setInterpolator(interpolator);
        a2.setInterpolator(interpolator);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(a1.isStarted());
                assertFalse(a1.isRunning());
                assertFalse(a2.isStarted());
                assertFalse(a2.isRunning());

                // Test isRunning() and isStarted() before and after seek
                a1.setCurrentFraction(a1StartFraction);
                a2.setCurrentFraction(a2StartFraction);

                assertFalse(a1.isStarted());
                assertFalse(a1.isRunning());
                assertFalse(a2.isStarted());
                assertFalse(a2.isRunning());
            }
        });
        Thread.sleep(POLL_INTERVAL);

        // Start animation and seek during the animation.
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(a1.isStarted());
                assertFalse(a1.isRunning());
                assertFalse(a2.isStarted());
                assertFalse(a2.isRunning());
                assertEquals(a1StartFraction, a1.getAnimatedFraction());
                assertEquals(a2StartFraction, a2.getAnimatedFraction());

                a1.start();
                a2.start();
            }
        });

        Thread.sleep(POLL_INTERVAL);
        final float halfwayFraction = 0.5f;
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(l1.startCalled);
                assertTrue(l2.startCalled);
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);

                // Check whether the animations start from the seeking fraction
                assertTrue(updateListener1.startFraction >= a1StartFraction);
                assertTrue(updateListener2.startFraction >= a2StartFraction);

                assertTrue(a1.isStarted());
                assertTrue(a1.isRunning());
                assertTrue(a2.isStarted());
                assertTrue(a2.isRunning());

                a1.setCurrentFraction(halfwayFraction);
                a2.setCurrentFraction(halfwayFraction);
            }
        });

        Thread.sleep(POLL_INTERVAL);

        // Check that seeking during running doesn't change animation's internal state
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(l1.startCalled);
                assertTrue(l2.startCalled);
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);

                assertTrue(a1.isStarted());
                assertTrue(a1.isRunning());
                assertTrue(a2.isStarted());
                assertTrue(a2.isRunning());
            }
        });

        // Wait until the animators finish successfully.
        long wait = Math.max(a1.getTotalDuration(), a2.getTotalDuration());
        Thread.sleep(wait);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Verify that the animators have finished.
                assertTrue(l1.endCalled);
                assertTrue(l2.endCalled);

                assertFalse(a1.isStarted());
                assertFalse(a2.isStarted());
                assertFalse(a1.isRunning());
                assertFalse(a2.isRunning());
            }
        });

        // Re-start animator a1 after it ends normally, and check that seek value from last run
        // does not affect the new run.
        updateListener1.reset();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                a1.start();
            }
        });

        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(updateListener1.wasRunning);
                assertTrue(updateListener1.startFraction >= 0);
                assertTrue(updateListener1.startFraction < halfwayFraction);
                a1.end();
            }
        });

    }

    @SmallTest
    public void testSeekWhileRunning() throws Throwable {
        // Seek one animator to the beginning and the other one to the end when they are running.
        final MyListener l1 = new MyListener();
        final MyListener l2 = new MyListener();
        a1.addListener(l1);
        a2.addListener(l2);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.startCalled);
                assertFalse(l2.startCalled);
                assertEquals(0f, a1.getAnimatedFraction());
                assertEquals(0f, a2.getAnimatedFraction());
                a1.start();
                a2.start();
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);
                assertTrue(a1.isRunning());
                assertTrue(a2.isRunning());
                // During the run, seek one to the beginning, the other to the end
                a1.setCurrentFraction(0f);
                a2.setCurrentFraction(1f);
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Check that a2 has finished due to the seeking, but a1 hasn't finished.
                assertFalse(l1.endCalled);
                assertTrue(l2.endCalled);
                assertEquals(1f, a2.getAnimatedFraction());
            }
        });

        Thread.sleep(a1.getTotalDuration());
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // By now a1 should finish also.
                assertTrue(l1.endCalled);
                assertEquals(1f, a1.getAnimatedFraction());
            }
        });
    }

    @SmallTest
    public void testEndBeforeStart() throws Throwable {
        // This test calls two animators that are not yet started. One animator has completed a
        // previous run but hasn't started since then, the other one has never run. When end() is
        // called on these two animators, we expected their animation listeners to receive both
        // onAnimationStarted(Animator) and onAnimationEnded(Animator) callbacks, in that sequence.

        a1.setStartDelay(20);

        // First start a1's first run.
        final MyListener normalEndingListener = new MyListener();
        a1.addListener(normalEndingListener);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(a1.isStarted());
                assertFalse(normalEndingListener.startCalled);
                assertFalse(normalEndingListener.endCalled);
                // Start normally
                a1.start();
            }
        });

        Thread.sleep(a1.getTotalDuration() + POLL_INTERVAL);

        // a1 should have finished by now.
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Call end() on both a1 and a2 without calling start()
                final MyListener l1 = new MyListener();
                a1.addListener(l1);
                final MyListener l2 = new MyListener();
                a2.addListener(l2);

                assertFalse(a1.isStarted());
                assertFalse(l1.startCalled);
                assertFalse(l1.endCalled);
                assertFalse(a2.isStarted());
                assertFalse(l2.startCalled);
                assertFalse(l1.endCalled);

                a1.end();
                a2.end();

                // Check that both animators' listeners have received the animation callbacks.
                assertTrue(l1.startCalled);
                assertTrue(l1.endCalled);
                assertFalse(a1.isStarted());
                assertTrue(l1.endTime >= l1.startTime);

                assertTrue(l2.startCalled);
                assertTrue(l2.endCalled);
                assertFalse(a2.isStarted());
                assertTrue(l2.endTime >= l1.startTime);
            }
        });
    }

    @SmallTest
    public void testZeroDuration() throws Throwable {
        // Run two animators with zero duration, with one running forward and the other one
        // backward. Check that the animations start and finish with the correct end fractions.
        a1.setDuration(0);
        a2.setDuration(0);

        // Set a fraction on an animation with 0-duration
        final ValueAnimator a3 = ValueAnimator.ofInt(0, 100);
        a3.setDuration(0);
        a3.setCurrentFraction(1.0f);
        assertEquals(1.0f, a3.getAnimatedFraction());

        final MyListener l1 = new MyListener();
        final MyListener l2 = new MyListener();
        final MyListener l3 = new MyListener();
        a1.addListener(l1);
        a2.addListener(l2);
        a3.addListener(l3);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.startCalled);
                assertFalse(l2.startCalled);
                assertFalse(l3.startCalled);
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);
                assertFalse(l3.endCalled);
                a1.start();
                a2.reverse();
                a3.start();
                // Check that the animators' values are immediately set to end value in the case of
                // 0-duration.
                assertEquals(A1_END_VALUE, a1.getAnimatedValue());
                assertEquals(A2_START_VALUE, a2.getAnimatedValue());
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Check that the animators have started and finished with the right values.
                assertTrue(l1.startCalled);
                assertTrue(l2.startCalled);
                assertTrue(l3.startCalled);
                assertTrue(l1.endCalled);
                assertTrue(l2.endCalled);
                assertTrue(l3.endCalled);
                assertEquals(1.0f, a1.getAnimatedFraction());
                assertEquals(0f, a2.getAnimatedFraction());
                assertEquals(1f, a3.getAnimatedFraction());
                assertEquals(A1_END_VALUE, a1.getAnimatedValue());
                assertEquals(A2_START_VALUE, a2.getAnimatedValue());
                assertEquals(100, a3.getAnimatedValue());
            }
        });
    }

    @SmallTest
    public void testZeroScale() throws Throwable {
        // Test whether animations would end properly when the scale is forced to be zero
        float scale = ValueAnimator.getDurationScale();
        ValueAnimator.setDurationScale(0f);

        // Run two animators, one of which has a start delay, after setting the duration scale to 0
        a1.setStartDelay(200);
        final MyListener l1 =  new MyListener();
        final MyListener l2 = new MyListener();
        a1.addListener(l1);
        a2.addListener(l2);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.startCalled);
                assertFalse(l2.startCalled);
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);

                a1.start();
                a2.start();

                // In the case of 0 duration scale applied to a non-0 duration, check that the
                // value is immediately set to the start value.
                assertEquals(A2_START_VALUE, a2.getAnimatedValue());
            }
        });
        Thread.sleep(POLL_INTERVAL);

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(l1.startCalled);
                assertTrue(l2.startCalled);
                assertTrue(l1.endCalled);
                assertTrue(l2.endCalled);
                assertEquals(A1_END_VALUE, a1.getAnimatedValue());
                assertEquals(A2_END_VALUE, a2.getAnimatedValue());
            }
        });

        // Restore duration scale
        ValueAnimator.setDurationScale(scale);
    }

    @SmallTest
    public void testReverse() throws Throwable {
        // Prolong animators duration so that we can do multiple checks during their run
        final ValueAnimator a3 = ValueAnimator.ofInt(0, 100);
        a1.setDuration(400);
        a2.setDuration(600);
        a3.setDuration(400);
        final MyListener l1 = new MyListener();
        final MyListener l2 = new MyListener();
        final MyListener l3 = new MyListener();
        a1.addListener(l1);
        a2.addListener(l2);
        a3.addListener(l3);

        // Reverse three animators, seek one to the beginning and another to the end, and force
        // to end the third one during reversing.
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertFalse(l1.startCalled);
                assertFalse(l2.startCalled);
                assertFalse(l3.startCalled);
                assertFalse(l1.endCalled);
                assertFalse(l2.endCalled);
                assertFalse(l3.endCalled);
                a1.reverse();
                a2.reverse();
                a3.reverse();
            }
        });
        Thread.sleep(POLL_INTERVAL);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                assertTrue(l1.startCalled);
                assertTrue(l2.startCalled);
                assertTrue(l3.startCalled);

                a1.setCurrentFraction(0f);
                a2.setCurrentFraction(1f);
                a3.end();

                // Check that the fraction has been set, and the getter returns the correct values.
                assertEquals(1f, a1.getAnimatedFraction());
                assertEquals(0f, a2.getAnimatedFraction());
            }
        });
        Thread.sleep(POLL_INTERVAL);

        // By now, a2 should have finished due to the seeking. It wouldn't have finished otherwise.
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Check that both animations have started, and a2 has finished.
                assertFalse(l1.endCalled);
                assertTrue(l2.endCalled);
                assertTrue(l3.endCalled);
            }
        });
        Thread.sleep(a1.getTotalDuration());

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                // Verify that a1 has finished as well.
                assertTrue(l1.endCalled);
                assertEquals(0f, a1.getAnimatedFraction());
                assertEquals(0f, a2.getAnimatedFraction());
                assertEquals(0f, a3.getAnimatedFraction());
            }
        });
    }

    class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener {
        boolean wasRunning = false;
        long firstRunningFrameTime = -1;
        long lastUpdateTime = -1;
        float startFraction = 0;

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            lastUpdateTime = SystemClock.uptimeMillis();
            if (animation.isRunning() && !wasRunning) {
                // Delay has passed
                firstRunningFrameTime = lastUpdateTime;
                startFraction = animation.getAnimatedFraction();
                wasRunning = animation.isRunning();
            }
        }

        void reset() {
            wasRunning = false;
            firstRunningFrameTime = -1;
            lastUpdateTime = -1;
            startFraction = 0;
        }
    }

    class MyListener implements Animator.AnimatorListener {
        boolean startCalled = false;
        boolean cancelCalled = false;
        boolean endCalled = false;
        long startTime = -1;
        long endTime = -1;

        @Override
        public void onAnimationStart(Animator animation) {
            startCalled = true;
            startTime = SystemClock.uptimeMillis();
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            endCalled = true;
            endTime = SystemClock.uptimeMillis();
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            cancelCalled = true;
        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    }

    class MyPauseListener implements Animator.AnimatorPauseListener {
        boolean pauseCalled = false;
        boolean resumeCalled = false;

        @Override
        public void onAnimationPause(Animator animation) {
            pauseCalled = true;
        }

        @Override
        public void onAnimationResume(Animator animation) {
            resumeCalled = true;
        }
    }

    class MyFrameCallbackProvider implements AnimationHandler.AnimationFrameCallbackProvider {

        Handler mHandler = null;
        private final static int MSG_FRAME = 0;
        private long mFrameDelay = DEFAULT_FRAME_INTERVAL;
        private ArrayList<Choreographer.FrameCallback> mFrameCallbacks = new ArrayList<>();

        final LooperThread mThread = new LooperThread();

        public MyFrameCallbackProvider() {
            mThread.start();
        }

        @Override
        public void postFrameCallback(Choreographer.FrameCallback callback) {
            mHandler.sendEmptyMessageDelayed(MSG_FRAME, mFrameDelay);
            if (!mFrameCallbacks.contains(callback)) {
                mFrameCallbacks.add(callback);
            }
        }

        @Override
        public void postCommitCallback(Runnable runnable) {
            // Run the runnable after a commit delay
            mHandler.postDelayed(runnable, COMMIT_DELAY);
        }

        @Override
        public long getFrameTime() {
            return SystemClock.uptimeMillis();
        }

        @Override
        public long getFrameDelay() {
            return mFrameDelay;
        }

        @Override
        public void setFrameDelay(long delay) {
            mFrameDelay = delay;
            if (mFrameCallbacks.size() != 0) {
                mHandler.removeMessages(MSG_FRAME);
                mHandler.sendEmptyMessageDelayed(MSG_FRAME, mFrameDelay);
            }
        }

        class LooperThread extends Thread {
            public void run() {
                Looper.prepare();
                mHandler = new Handler() {
                    public void handleMessage(Message msg) {
                        // Handle message here.
                        switch (msg.what) {
                            case MSG_FRAME:
                                for (int i = 0; i < mFrameCallbacks.size(); i++) {
                                    mFrameCallbacks.get(i).doFrame(SystemClock.uptimeMillis());
                                }
                                break;
                            default:
                                break;
                        }
                    }
                };
                Looper.loop();
            }
        }
    }
}
