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