• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.animation;
18 
19 import android.animation.AnimationHandler.AnimationFrameCallback;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.os.Looper;
23 import android.os.SystemClock;
24 import android.testing.TestableLooper;
25 import android.testing.TestableLooper.RunnableWithException;
26 import android.util.AndroidRuntimeException;
27 import android.util.Singleton;
28 import android.view.Choreographer;
29 import android.view.animation.AnimationUtils;
30 
31 import androidx.test.platform.app.InstrumentationRegistry;
32 
33 import com.android.internal.util.Preconditions;
34 
35 import org.junit.AssumptionViolatedException;
36 import org.junit.rules.TestRule;
37 import org.junit.runner.Description;
38 import org.junit.runners.model.Statement;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.function.Consumer;
43 
44 /**
45  * JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the
46  * duration of the animation. This also helps the test to be written in a deterministic manner.
47  *
48  * Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule}
49  * of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started.
50  * Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to
51  * start the animator.
52  *
53  * <pre>
54  * {@literal @}SmallTest
55  * {@literal @}RunWith(AndroidJUnit4.class)
56  * public class SampleAnimatorTest {
57  *
58  *     {@literal @}Rule
59  *     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
60  *
61  *     {@literal @}UiThreadTest
62  *     {@literal @}Test
63  *     public void sample() {
64  *         final ValueAnimator animator = ValueAnimator.ofInt(0, 1000);
65  *         animator.setDuration(1000L);
66  *         assertThat(animator.getAnimatedValue(), is(0));
67  *         animator.start();
68  *         mAnimatorTestRule.advanceTimeBy(500L);
69  *         assertThat(animator.getAnimatedValue(), is(500));
70  *     }
71  * }
72  * </pre>
73  */
74 public final class AnimatorTestRule implements TestRule {
75 
76     private final Object mLock = new Object();
77     private final Singleton<TestHandler> mTestHandler = new Singleton<>() {
78         @Override
79         protected TestHandler create() {
80             return new TestHandler();
81         }
82     };
83     private final Object mTest;
84     private final long mStartTime;
85     private long mTotalTimeDelta = 0;
86     private volatile boolean mCanLockAnimationClock;
87     private Looper mLooperWithLockedAnimationClock;
88 
89     /**
90      * Construct an AnimatorTestRule with access to the test instance and a custom start time.
91      * @see #AnimatorTestRule(Object)
92      */
AnimatorTestRule(Object test, long startTime)93     public AnimatorTestRule(Object test, long startTime) {
94         mTest = test;
95         mStartTime = startTime;
96     }
97 
98     /**
99      * Construct an AnimatorTestRule for the given test instance with a start time of
100      * {@link SystemClock#uptimeMillis()}. Initializing the start time with this clock reduces the
101      * discrepancies with various internals of classes like ValueAnimator which can sometimes read
102      * that clock via {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
103      *
104      * @param test the test instance used to access the {@link TestableLooper} used by the class.
105      */
AnimatorTestRule(Object test)106     public AnimatorTestRule(Object test) {
107         this(test, SystemClock.uptimeMillis());
108     }
109 
110     @NonNull
111     @Override
apply(@onNull final Statement base, @NonNull Description description)112     public Statement apply(@NonNull final Statement base, @NonNull Description description) {
113         return new Statement() {
114             @Override
115             public void evaluate() throws Throwable {
116                 final TestHandler testHandler = mTestHandler.get();
117                 final AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler);
118                 final RunnableWithException lockClock =
119                         wrapWithRunBlocking(new LockAnimationClockRunnable());
120                 final RunnableWithException unlockClock =
121                         wrapWithRunBlocking(new UnlockAnimationClockRunnable());
122                 try {
123                     lockClock.run();
124                     base.evaluate();
125                 } finally {
126                     unlockClock.run();
127                     AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
128                     if (testHandler != objAtEnd) {
129                         // pass or fail, inner logic not restoring the handler needs to be reported.
130                         // noinspection ThrowFromFinallyBlock
131                         throw new IllegalStateException("Test handler was altered: expected="
132                                 + testHandler + " actual=" + objAtEnd);
133                     }
134                 }
135             }
136         };
137     }
138 
139     private RunnableWithException wrapWithRunBlocking(RunnableWithException runnable) {
140         RunnableWithException wrapped = TestableLooper.wrapWithRunBlocking(mTest, runnable);
141         if (wrapped != null) {
142             return wrapped;
143         }
144         return () -> runOnMainThrowing(runnable);
145     }
146 
147     private static void runOnMainThrowing(RunnableWithException runnable) throws Exception {
148         if (Looper.myLooper() == Looper.getMainLooper()) {
149             runnable.run();
150         } else {
151             final Throwable[] throwableBox = new Throwable[1];
152             InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
153                 try {
154                     runnable.run();
155                 } catch (Throwable t) {
156                     throwableBox[0] = t;
157                 }
158             });
159             if (throwableBox[0] == null) {
160                 return;
161             } else if (throwableBox[0] instanceof RuntimeException ex) {
162                 throw ex;
163             } else if (throwableBox[0] instanceof Error err) {
164                 throw err;
165             } else {
166                 throw new RuntimeException(throwableBox[0]);
167             }
168         }
169     }
170 
171     private class LockAnimationClockRunnable implements RunnableWithException {
172         @Override
173         public void run() {
174             mLooperWithLockedAnimationClock = Looper.myLooper();
175             mCanLockAnimationClock = true;
176             lockAnimationClockToCurrentTime();
177         }
178     }
179 
180     private class UnlockAnimationClockRunnable implements RunnableWithException {
181         @Override
182         public void run() {
183             mCanLockAnimationClock = false;
184             mLooperWithLockedAnimationClock = null;
185             AnimationUtils.unlockAnimationClock();
186         }
187     }
188 
189     private void lockAnimationClockToCurrentTime() {
190         if (!mCanLockAnimationClock) {
191             throw new AssertionError("Unable to lock the animation clock; "
192                     + "has the test started? already finished?");
193         }
194         if (mLooperWithLockedAnimationClock != Looper.myLooper()) {
195             throw new AssertionError("Animation clock being locked on " + Looper.myLooper()
196                     + " but should only be locked on " + mLooperWithLockedAnimationClock);
197         }
198         long desiredTime = getCurrentTime();
199         AnimationUtils.lockAnimationClock(desiredTime);
200         if (!mCanLockAnimationClock) {
201             AnimationUtils.unlockAnimationClock();
202             throw new AssertionError("Threading error when locking the animation clock");
203         }
204         long outputTime = AnimationUtils.currentAnimationTimeMillis();
205         if (outputTime != desiredTime) {
206             // Skip the test (rather than fail it) if there's a clock issue
207             throw new AssumptionViolatedException("currentAnimationTimeMillis() is " + outputTime
208                     + " after locking to " + desiredTime);
209         }
210     }
211 
212     /**
213      * If any new {@link Animator}s have been registered since the last time the frame time was
214      * advanced, initialize them with the current frame time.  Failing to do this will result in the
215      * animations beginning on the *next* advancement instead, so this is done automatically for
216      * test authors inside of {@link #advanceTimeBy}.  However this is exposed in case authors want
217      * to validate operations performed by onStart listeners.
218      * <p>
219      * NOTE: This is only required of the platform ValueAnimator because its start() method calls
220      * {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this
221      * rule can't synchronously trigger the callback at that time.
222      */
223     public void initNewAnimators() {
224         requireLooper("AnimationTestRule#initNewAnimators()");
225         long currentTime = getCurrentTime();
226         final TestHandler testHandler = mTestHandler.get();
227         List<AnimationFrameCallback> newCallbacks = new ArrayList<>(testHandler.mNewCallbacks);
228         testHandler.mNewCallbacks.clear();
229         for (AnimationFrameCallback newCallback : newCallbacks) {
230             newCallback.doAnimationFrame(currentTime);
231         }
232     }
233 
234     /**
235      * Advances the animation clock by the given amount of delta in milliseconds. This call will
236      * produce an animation frame to all the ongoing animations. This method needs to be
237      * called on the same thread as {@link Animator#start()}.
238      *
239      * @param timeDelta the amount of milliseconds to advance
240      */
241     public void advanceTimeBy(long timeDelta) {
242         advanceTimeBy(timeDelta, null);
243     }
244 
245     /**
246      * Advances the animation clock by the given amount of delta in milliseconds. This call will
247      * produce an animation frame to all the ongoing animations. This method needs to be
248      * called on the same thread as {@link Animator#start()}.
249      * <p>
250      * This method is not for test authors, but for rule authors to ensure that multiple animators
251      * can be advanced in sync.
252      *
253      * @param timeDelta      the amount of milliseconds to advance
254      * @param preFrameAction a consumer to be passed the timeDelta following the time advancement
255      *                       but prior to the frame production.
256      */
257     public void advanceTimeBy(long timeDelta, @Nullable Consumer<Long> preFrameAction) {
258         Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative");
259         requireLooper("AnimationTestRule#advanceTimeBy(long)");
260         final TestHandler testHandler = mTestHandler.get();
261         if (timeDelta == 0) {
262             // If time is not being advanced, all animators will get a tick; don't double tick these
263             testHandler.mNewCallbacks.clear();
264         } else {
265             // before advancing time, start new animators with the current time
266             initNewAnimators();
267         }
268         synchronized (mLock) {
269             // advance time
270             mTotalTimeDelta += timeDelta;
271         }
272         lockAnimationClockToCurrentTime();
273         if (preFrameAction != null) {
274             preFrameAction.accept(timeDelta);
275             // After letting other code run, clear any new callbacks to avoid double-ticking them
276             testHandler.mNewCallbacks.clear();
277         }
278         // produce a frame
279         testHandler.doFrame();
280     }
281 
282     /**
283      * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a
284      * different time than the time tracked by {@link SystemClock} This method needs to be called on
285      * the same thread as {@link Animator#start()}.
286      */
287     public long getCurrentTime() {
288         requireLooper("AnimationTestRule#getCurrentTime()");
289         synchronized (mLock) {
290             return mStartTime + mTotalTimeDelta;
291         }
292     }
293 
294     private static void requireLooper(String method) {
295         if (Looper.myLooper() == null) {
296             throw new AndroidRuntimeException(method + " may only be called on Looper threads");
297         }
298     }
299 
300     private class TestHandler extends AnimationHandler {
301         public final TestProvider mTestProvider = new TestProvider();
302         private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>();
303 
304         TestHandler() {
305             setProvider(mTestProvider);
306         }
307 
308         public void doFrame() {
309             mTestProvider.animateFrame();
310             mTestProvider.commitFrame();
311         }
312 
313         @Override
314         public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) {
315             // NOTE: using the delay is infeasible because the AnimationHandler uses
316             //  SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we
317             //  could fix this for tests.
318             super.addAnimationFrameCallback(callback, 0);
319             if (delay <= 0) {
320                 mNewCallbacks.add(callback);
321             }
322         }
323 
324         @Override
325         public void removeCallback(AnimationFrameCallback callback) {
326             super.removeCallback(callback);
327             mNewCallbacks.remove(callback);
328         }
329     }
330 
331     private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider {
332         private long mFrameDelay = 10;
333         private Choreographer.FrameCallback mFrameCallback = null;
334         private final List<Runnable> mCommitCallbacks = new ArrayList<>();
335 
336         public void animateFrame() {
337             Choreographer.FrameCallback frameCallback = mFrameCallback;
338             mFrameCallback = null;
339             if (frameCallback != null) {
340                 frameCallback.doFrame(getFrameTime());
341             }
342         }
343 
344         public void commitFrame() {
345             List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks);
346             mCommitCallbacks.clear();
347             for (Runnable commitCallback : commitCallbacks) {
348                 commitCallback.run();
349             }
350         }
351 
352         @Override
353         public void postFrameCallback(Choreographer.FrameCallback callback) {
354             assert mFrameCallback == null;
355             mFrameCallback = callback;
356         }
357 
358         @Override
359         public void postCommitCallback(Runnable runnable) {
360             mCommitCallbacks.add(runnable);
361         }
362 
363         @Override
364         public void setFrameDelay(long delay) {
365             mFrameDelay = delay;
366         }
367 
368         @Override
369         public long getFrameDelay() {
370             return mFrameDelay;
371         }
372 
373         @Override
374         public long getFrameTime() {
375             return getCurrentTime();
376         }
377     }
378 }
379