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.util.AndroidRuntimeException;
25 import android.view.Choreographer;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import org.junit.rules.TestRule;
30 import org.junit.runner.Description;
31 import org.junit.runners.model.Statement;
32 
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.function.Consumer;
36 
37 /**
38  * JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the
39  * duration of the animation. This also helps the test to be written in a deterministic manner.
40  *
41  * Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule}
42  * of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started.
43  * Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to
44  * start the animator.
45  *
46  * <pre>
47  * {@literal @}SmallTest
48  * {@literal @}RunWith(AndroidJUnit4.class)
49  * public class SampleAnimatorTest {
50  *
51  *     {@literal @}Rule
52  *     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
53  *
54  *     {@literal @}UiThreadTest
55  *     {@literal @}Test
56  *     public void sample() {
57  *         final ValueAnimator animator = ValueAnimator.ofInt(0, 1000);
58  *         animator.setDuration(1000L);
59  *         assertThat(animator.getAnimatedValue(), is(0));
60  *         animator.start();
61  *         mAnimatorTestRule.advanceTimeBy(500L);
62  *         assertThat(animator.getAnimatedValue(), is(500));
63  *     }
64  * }
65  * </pre>
66  */
67 public final class AnimatorTestRule implements TestRule {
68 
69     private final Object mLock = new Object();
70     private final TestHandler mTestHandler = new TestHandler();
71     /**
72      * initializing the start time with {@link SystemClock#uptimeMillis()} reduces the discrepancies
73      * with various internals of classes like ValueAnimator which can sometimes read that clock via
74      * {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
75      */
76     private final long mStartTime = SystemClock.uptimeMillis();
77     private long mTotalTimeDelta = 0;
78 
79     @NonNull
80     @Override
apply(@onNull final Statement base, @NonNull Description description)81     public Statement apply(@NonNull final Statement base, @NonNull Description description) {
82         return new Statement() {
83             @Override
84             public void evaluate() throws Throwable {
85                 AnimationHandler objAtStart = AnimationHandler.setTestHandler(mTestHandler);
86                 try {
87                     base.evaluate();
88                 } finally {
89                     AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
90                     if (mTestHandler != objAtEnd) {
91                         // pass or fail, inner logic not restoring the handler needs to be reported.
92                         // noinspection ThrowFromFinallyBlock
93                         throw new IllegalStateException("Test handler was altered: expected="
94                                 + mTestHandler + " actual=" + objAtEnd);
95                     }
96                 }
97             }
98         };
99     }
100 
101     /**
102      * If any new {@link Animator}s have been registered since the last time the frame time was
103      * advanced, initialize them with the current frame time.  Failing to do this will result in the
104      * animations beginning on the *next* advancement instead, so this is done automatically for
105      * test authors inside of {@link #advanceTimeBy}.  However this is exposed in case authors want
106      * to validate operations performed by onStart listeners.
107      * <p>
108      * NOTE: This is only required of the platform ValueAnimator because its start() method calls
109      * {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this
110      * rule can't synchronously trigger the callback at that time.
111      */
112     public void initNewAnimators() {
113         requireLooper("AnimationTestRule#initNewAnimators()");
114         long currentTime = getCurrentTime();
115         List<AnimationFrameCallback> newCallbacks = new ArrayList<>(mTestHandler.mNewCallbacks);
116         mTestHandler.mNewCallbacks.clear();
117         for (AnimationFrameCallback newCallback : newCallbacks) {
118             newCallback.doAnimationFrame(currentTime);
119         }
120     }
121 
122     /**
123      * Advances the animation clock by the given amount of delta in milliseconds. This call will
124      * produce an animation frame to all the ongoing animations. This method needs to be
125      * called on the same thread as {@link Animator#start()}.
126      *
127      * @param timeDelta the amount of milliseconds to advance
128      */
129     public void advanceTimeBy(long timeDelta) {
130         advanceTimeBy(timeDelta, null);
131     }
132 
133     /**
134      * Advances the animation clock by the given amount of delta in milliseconds. This call will
135      * produce an animation frame to all the ongoing animations. This method needs to be
136      * called on the same thread as {@link Animator#start()}.
137      * <p>
138      * This method is not for test authors, but for rule authors to ensure that multiple animators
139      * can be advanced in sync.
140      *
141      * @param timeDelta      the amount of milliseconds to advance
142      * @param preFrameAction a consumer to be passed the timeDelta following the time advancement
143      *                       but prior to the frame production.
144      */
145     public void advanceTimeBy(long timeDelta, @Nullable Consumer<Long> preFrameAction) {
146         Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative");
147         requireLooper("AnimationTestRule#advanceTimeBy(long)");
148         if (timeDelta == 0) {
149             // If time is not being advanced, all animators will get a tick; don't double tick these
150             mTestHandler.mNewCallbacks.clear();
151         } else {
152             // before advancing time, start new animators with the current time
153             initNewAnimators();
154         }
155         synchronized (mLock) {
156             // advance time
157             mTotalTimeDelta += timeDelta;
158         }
159         if (preFrameAction != null) {
160             preFrameAction.accept(timeDelta);
161             // After letting other code run, clear any new callbacks to avoid double-ticking them
162             mTestHandler.mNewCallbacks.clear();
163         }
164         // produce a frame
165         mTestHandler.doFrame();
166     }
167 
168     /**
169      * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a
170      * different time than the time tracked by {@link SystemClock} This method needs to be called on
171      * the same thread as {@link Animator#start()}.
172      */
173     public long getCurrentTime() {
174         requireLooper("AnimationTestRule#getCurrentTime()");
175         synchronized (mLock) {
176             return mStartTime + mTotalTimeDelta;
177         }
178     }
179 
180     private static void requireLooper(String method) {
181         if (Looper.myLooper() == null) {
182             throw new AndroidRuntimeException(method + " may only be called on Looper threads");
183         }
184     }
185 
186     private class TestHandler extends AnimationHandler {
187         public final TestProvider mTestProvider = new TestProvider();
188         private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>();
189 
190         TestHandler() {
191             setProvider(mTestProvider);
192         }
193 
194         public void doFrame() {
195             mTestProvider.animateFrame();
196             mTestProvider.commitFrame();
197         }
198 
199         @Override
200         public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) {
201             // NOTE: using the delay is infeasible because the AnimationHandler uses
202             //  SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we
203             //  could fix this for tests.
204             super.addAnimationFrameCallback(callback, 0);
205             if (delay <= 0) {
206                 mNewCallbacks.add(callback);
207             }
208         }
209 
210         @Override
211         public void removeCallback(AnimationFrameCallback callback) {
212             super.removeCallback(callback);
213             mNewCallbacks.remove(callback);
214         }
215     }
216 
217     private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider {
218         private long mFrameDelay = 10;
219         private Choreographer.FrameCallback mFrameCallback = null;
220         private final List<Runnable> mCommitCallbacks = new ArrayList<>();
221 
222         public void animateFrame() {
223             Choreographer.FrameCallback frameCallback = mFrameCallback;
224             mFrameCallback = null;
225             if (frameCallback != null) {
226                 frameCallback.doFrame(getFrameTime());
227             }
228         }
229 
230         public void commitFrame() {
231             List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks);
232             mCommitCallbacks.clear();
233             for (Runnable commitCallback : commitCallbacks) {
234                 commitCallback.run();
235             }
236         }
237 
238         @Override
239         public void postFrameCallback(Choreographer.FrameCallback callback) {
240             assert mFrameCallback == null;
241             mFrameCallback = callback;
242         }
243 
244         @Override
245         public void postCommitCallback(Runnable runnable) {
246             mCommitCallbacks.add(runnable);
247         }
248 
249         @Override
250         public void setFrameDelay(long delay) {
251             mFrameDelay = delay;
252         }
253 
254         @Override
255         public long getFrameDelay() {
256             return mFrameDelay;
257         }
258 
259         @Override
260         public long getFrameTime() {
261             return getCurrentTime();
262         }
263     }
264 }
265