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