Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2822a426 authored by Doris Liu's avatar Doris Liu
Browse files

Test ValueAnimator with custom timing pulse provider

This CL depends on ag/749611

With the custom timing pulse provider, we are free from the chaos that could
happen on UI thread and any delay that comes as a result. Therefore we can
count on more accurate timing for each frame, which enables us to test things
such as AnimatorUpdateListener, among other things.

Note that there will still be ~2ms delays in addition to the frame interval
in between frames, due to the MessageQueue processing time.

Change-Id: I6de0aa06125d1ecfae788f5dd22536a5fa52e651
parent 56d4ce51
Loading
Loading
Loading
Loading
+347 −1
Original line number Diff line number Diff line
@@ -15,13 +15,21 @@
*/
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;

@@ -34,6 +42,9 @@ public class ValueAnimatorTests extends ActivityInstrumentationTestCase2<BasicAn
    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);
    }
@@ -47,9 +58,9 @@ public class ValueAnimatorTests extends ActivityInstrumentationTestCase2<BasicAn

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

    @SmallTest
@@ -492,10 +503,265 @@ public class ValueAnimatorTests extends ActivityInstrumentationTestCase2<BasicAn
        });
    }

    @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 testASeek() 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();
            }
        });

    }

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

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
@@ -503,24 +769,36 @@ public class ValueAnimatorTests extends ActivityInstrumentationTestCase2<BasicAn
            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
@@ -548,4 +826,72 @@ public class ValueAnimatorTests extends ActivityInstrumentationTestCase2<BasicAn
            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();
            }
        }
    }
}