1 /* <lambda>null2 * Copyright (C) 2020 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 package com.android.wm.shell.shared.animation 17 18 import android.os.Handler 19 import android.os.Looper 20 import android.util.ArrayMap 21 import androidx.dynamicanimation.animation.FloatPropertyCompat 22 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.prepareForTest 23 import java.util.ArrayDeque 24 import java.util.concurrent.CountDownLatch 25 import java.util.concurrent.TimeUnit 26 import kotlin.collections.ArrayList 27 import kotlin.collections.HashMap 28 import kotlin.collections.HashSet 29 import kotlin.collections.Set 30 import kotlin.collections.component1 31 import kotlin.collections.component2 32 import kotlin.collections.drop 33 import kotlin.collections.forEach 34 import kotlin.collections.getOrPut 35 import kotlin.collections.set 36 import kotlin.collections.toList 37 import kotlin.collections.toTypedArray 38 39 typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean 40 typealias UpdateFramesPerProperty<T> = 41 ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>> 42 43 /** 44 * Utilities for testing code that uses [PhysicsAnimator]. 45 * 46 * Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior 47 * of all PhysicsAnimator instances so that they post animations to the main thread (so they don't 48 * crash). It'll also enable the use of the other static helper methods in this class, which you can 49 * use to do things like block the test until animations complete (so you can test end states), or 50 * verify keyframes. 51 */ 52 object PhysicsAnimatorTestUtils { 53 var timeoutMs: Long = 2000 54 private var startBlocksUntilAnimationsEnd = false 55 private val animationThreadHandler = Handler(Looper.getMainLooper()) 56 private val allAnimatedObjects = HashSet<Any>() 57 private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>() 58 59 /** 60 * Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the 61 * main thread, and report all of their 62 */ 63 @JvmStatic 64 fun prepareForTest() { 65 PhysicsAnimator.onAnimatorCreated = { animator, target -> 66 allAnimatedObjects.add(target) 67 animatorTestHelpers[animator] = AnimatorTestHelper(animator) 68 } 69 70 timeoutMs = 2000 71 startBlocksUntilAnimationsEnd = false 72 allAnimatedObjects.clear() 73 } 74 75 @JvmStatic 76 fun tearDown() { 77 if (Looper.myLooper() == animationThreadHandler.looper) { 78 animatorTestHelpers.keys.forEach { it.cancel() } 79 } else { 80 val latch = CountDownLatch(1) 81 animationThreadHandler.post { 82 animatorTestHelpers.keys.forEach { it.cancel() } 83 latch.countDown() 84 } 85 latch.await(5, TimeUnit.SECONDS) 86 } 87 88 animatorTestHelpers.clear() 89 animators.clear() 90 allAnimatedObjects.clear() 91 } 92 93 /** 94 * Sets the maximum time (in milliseconds) to block the test thread while waiting for animations 95 * before throwing an exception. 96 */ 97 @JvmStatic 98 fun setBlockTimeout(timeoutMs: Long) { 99 PhysicsAnimatorTestUtils.timeoutMs = timeoutMs 100 } 101 102 /** 103 * Sets whether all animations should block the test thread until they end. This is typically 104 * the desired behavior, since you can invoke code that runs an animation and then assert things 105 * about its end state. 106 */ 107 @JvmStatic 108 fun setAllAnimationsBlock(block: Boolean) { 109 startBlocksUntilAnimationsEnd = block 110 } 111 112 /** 113 * Blocks the calling thread until animations of the given property on the target object end. 114 */ 115 @JvmStatic 116 @Throws(InterruptedException::class) 117 fun <T : Any> blockUntilAnimationsEnd( 118 animator: PhysicsAnimator<T>, 119 vararg properties: FloatPropertyCompat<in T> 120 ) { 121 val animatingProperties = HashSet<FloatPropertyCompat<in T>>() 122 for (property in properties) { 123 if (animator.isPropertyAnimating(property)) { 124 animatingProperties.add(property) 125 } 126 } 127 128 if (animatingProperties.size > 0) { 129 val latch = CountDownLatch(animatingProperties.size) 130 getAnimationTestHelper(animator).addTestEndListener( 131 object : PhysicsAnimator.EndListener<T> { 132 override fun onAnimationEnd( 133 target: T, 134 property: FloatPropertyCompat<in T>, 135 wasFling: Boolean, 136 canceled: Boolean, 137 finalValue: Float, 138 finalVelocity: Float, 139 allRelevantPropertyAnimsEnded: Boolean 140 ) { 141 if (animatingProperties.contains(property)) { 142 latch.countDown() 143 } 144 } 145 }) 146 147 latch.await(timeoutMs, TimeUnit.MILLISECONDS) 148 } 149 } 150 151 /** 152 * Blocks the calling thread until all animations of the given property (on all target objects) 153 * have ended. Useful when you don't have access to the objects being animated, but still need 154 * to wait for them to end so that other testable side effects occur (such as update/end 155 * listeners). 156 */ 157 @JvmStatic 158 @Throws(InterruptedException::class) 159 @Suppress("UNCHECKED_CAST") 160 fun <T : Any> blockUntilAnimationsEnd( 161 vararg properties: FloatPropertyCompat<in T> 162 ) { 163 for (target in allAnimatedObjects) { 164 try { 165 blockUntilAnimationsEnd( 166 PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, *properties) 167 } catch (e: ClassCastException) { 168 // Keep checking the other objects for ones whose types match the provided 169 // properties. 170 } 171 } 172 } 173 174 /** Whether any animation is currently running. */ 175 @JvmStatic 176 fun isAnyAnimationRunning(): Boolean { 177 for (target in allAnimatedObjects) { 178 val animator = PhysicsAnimator.getInstance(target) 179 if (animator.isRunning()) return true 180 } 181 return false 182 } 183 184 /** 185 * Blocks the calling thread until the first animation frame in which predicate returns true. If 186 * the given object isn't animating, returns without blocking. 187 */ 188 @JvmStatic 189 @Throws(InterruptedException::class) 190 fun <T : Any> blockUntilFirstAnimationFrameWhereTrue( 191 animator: PhysicsAnimator<T>, 192 predicate: (T) -> Boolean 193 ) { 194 if (animator.isRunning()) { 195 val latch = CountDownLatch(1) 196 getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator 197 .UpdateListener<T> { 198 override fun onAnimationUpdateForProperty( 199 target: T, 200 values: UpdateMap<T> 201 ) { 202 if (predicate(target)) { 203 latch.countDown() 204 } 205 } 206 }) 207 208 latch.await(timeoutMs, TimeUnit.MILLISECONDS) 209 } 210 } 211 212 /** 213 * Verifies that the animator reported animation frame values to update listeners that satisfy 214 * the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through 215 * all animation frames, and check them against the current predicate. If it returns false, we 216 * continue through the frames until it returns true, and then move on to the next matcher. 217 * Verification fails if we run out of frames while unsatisfied matchers remain. 218 * 219 * If verification is successful, all frames to this point are considered 'verified' and will be 220 * cleared. Subsequent calls to this method will start verification at the next animation frame. 221 * 222 * Example: Verify that an animation surpassed x = 50f before going negative. 223 * verifyAnimationUpdateFrames( 224 * animator, TRANSLATION_X, 225 * { u -> u.value > 50f }, 226 * { u -> u.value < 0f }) 227 * 228 * Example: verify that an animation went backwards at some point while still being on-screen. 229 * verifyAnimationUpdateFrames( 230 * animator, TRANSLATION_X, 231 * { u -> u.velocity < 0f && u.value >= 0f }) 232 * 233 * This method is intended to help you test longer, more complicated animations where it's 234 * critical that certain values were reached. Using this method to test short animations can 235 * fail due to the animation having fewer frames than provided matchers. For example, an 236 * animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The 237 * following would then fail despite it seeming logically sound: 238 * 239 * verifyAnimationUpdateFrames( 240 * animator, TRANSLATION_X, 241 * { u -> u.value > 1f }, 242 * { u -> u.value > 2f }, 243 * { u -> u.value > 3f }) 244 * 245 * Tests might also fail if your matchers are too granular, such as this example test after an 246 * animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f 247 * and 3f. 248 * 249 * verifyAnimationUpdateFrames( 250 * animator, TRANSLATION_X, 251 * { u -> u.value > 2f && u.value < 3f }, 252 * { u -> u.value >= 50f }) 253 * 254 * Failures will print a helpful log of all animation frames so you can see what caused the test 255 * to fail. 256 */ 257 fun <T : Any> verifyAnimationUpdateFrames( 258 animator: PhysicsAnimator<T>, 259 property: FloatPropertyCompat<in T>, 260 firstUpdateMatcher: UpdateMatcher, 261 vararg additionalUpdateMatchers: UpdateMatcher 262 ) { 263 val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator) 264 265 if (!updateFrames.containsKey(property)) { 266 error("No frames for given target object and property.") 267 } 268 269 // Copy the frames to avoid a ConcurrentModificationException if the animation update 270 // listeners attempt to add a new frame while we're verifying these. 271 val framesForProperty = ArrayList(updateFrames[property]!!) 272 val matchers = ArrayDeque<UpdateMatcher>( 273 additionalUpdateMatchers.toList()) 274 val frameTraceMessage = StringBuilder() 275 276 var curMatcher = firstUpdateMatcher 277 278 // Loop through the updates from the testable animator. 279 for (update in framesForProperty) { 280 // Check whether this frame satisfies the current matcher. 281 if (curMatcher(update)) { 282 // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining 283 // frames and return without failing. 284 if (matchers.size == 0) { 285 getAnimationUpdateFrames(animator).remove(property) 286 return 287 } 288 289 frameTraceMessage.append("$update\t(satisfied matcher)\n") 290 curMatcher = matchers.pop() // Get the next matcher and keep going. 291 } else { 292 frameTraceMessage.append("${update}\n") 293 } 294 } 295 296 val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property) 297 getAnimationUpdateFrames(animator).remove(property) 298 299 throw RuntimeException( 300 "Failed to verify animation frames for property $readablePropertyName: " + 301 "Provided ${additionalUpdateMatchers.size + 1} matchers, " + 302 "however ${matchers.size + 1} remained unsatisfied.\n\n" + 303 "All frames:\n$frameTraceMessage") 304 } 305 306 /** 307 * Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float 308 * values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f: 309 * 310 * verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f) 311 * 312 * This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and 313 * <= 50f. 314 * 315 * The same caveats apply: short animations might not have enough frames to satisfy all of the 316 * matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from 317 * x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and 318 * 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames 319 * so you can see what caused the test to fail. 320 */ 321 fun <T : Any> verifyAnimationUpdateFrames( 322 animator: PhysicsAnimator<T>, 323 property: FloatPropertyCompat<in T>, 324 startValue: Float, 325 firstTargetValue: Float, 326 vararg additionalTargetValues: Float 327 ) { 328 val matchers = ArrayList<UpdateMatcher>() 329 330 val values = ArrayList<Float>().also { 331 it.add(firstTargetValue) 332 it.addAll(additionalTargetValues.toList()) 333 } 334 335 var prevVal = startValue 336 for (value in values) { 337 if (value > prevVal) { 338 matchers.add { update -> update.value >= value } 339 } else { 340 matchers.add { update -> update.value <= value } 341 } 342 343 prevVal = value 344 } 345 346 verifyAnimationUpdateFrames( 347 animator, property, matchers[0], *matchers.drop(0).toTypedArray()) 348 } 349 350 /** 351 * Returns all of the values that have ever been reported to update listeners, per property. 352 */ 353 @Suppress("UNCHECKED_CAST") 354 fun <T : Any> getAnimationUpdateFrames( 355 animator: PhysicsAnimator<T> 356 ): UpdateFramesPerProperty<T> { 357 return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T> 358 } 359 360 /** 361 * Clears animation frame updates from the given animator so they aren't used the next time its 362 * passed to [verifyAnimationUpdateFrames]. 363 */ 364 fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) { 365 animatorTestHelpers[animator]?.clearUpdates() 366 } 367 368 @Suppress("UNCHECKED_CAST") 369 private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> { 370 return animatorTestHelpers[animator] as AnimatorTestHelper<T> 371 } 372 373 /** 374 * Helper class for testing an animator. This replaces the animator's start action with 375 * [startForTest] and adds test listeners to enable other test utility behaviors. We build one 376 * these for each Animator and keep them around so we can access the updates. 377 */ 378 class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) { 379 380 /** All updates received for each property animation. */ 381 private val allUpdates = 382 ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>() 383 384 private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>() 385 private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>() 386 387 /** Whether we're currently in the middle of executing startInternal(). */ 388 private var currentlyRunningStartInternal = false 389 390 init { 391 animator.startAction = ::startForTest 392 animator.cancelAction = ::cancelForTest 393 } 394 395 internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) { 396 testEndListeners.add(listener) 397 } 398 399 internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) { 400 testUpdateListeners.add(listener) 401 } 402 403 internal fun getUpdates(): UpdateFramesPerProperty<T> { 404 return allUpdates 405 } 406 407 internal fun clearUpdates() { 408 allUpdates.clear() 409 } 410 411 private fun startForTest() { 412 // The testable animator needs to block the main thread until super.start() has been 413 // called, since callers expect .start() to be synchronous but we're posting it to a 414 // handler here. We may also continue blocking until all animations end, if 415 // startBlocksUntilAnimationsEnd = true. 416 val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1) 417 418 animationThreadHandler.post { 419 // Add an update listener that dispatches to any test update listeners added by 420 // tests. 421 animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> { 422 override fun onAnimationUpdateForProperty( 423 target: T, 424 values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> 425 ) { 426 values.forEach { (property, value) -> 427 allUpdates.getOrPut(property, { ArrayList() }).add(value) 428 } 429 430 for (listener in testUpdateListeners) { 431 listener.onAnimationUpdateForProperty(target, values) 432 } 433 } 434 }) 435 436 // Add an end listener that dispatches to any test end listeners added by tests, and 437 // unblocks the main thread if required. 438 animator.addEndListener(object : PhysicsAnimator.EndListener<T> { 439 override fun onAnimationEnd( 440 target: T, 441 property: FloatPropertyCompat<in T>, 442 wasFling: Boolean, 443 canceled: Boolean, 444 finalValue: Float, 445 finalVelocity: Float, 446 allRelevantPropertyAnimsEnded: Boolean 447 ) { 448 for (listener in testEndListeners) { 449 listener.onAnimationEnd( 450 target, property, wasFling, canceled, finalValue, finalVelocity, 451 allRelevantPropertyAnimsEnded) 452 } 453 454 if (allRelevantPropertyAnimsEnded) { 455 testEndListeners.clear() 456 testUpdateListeners.clear() 457 458 if (startBlocksUntilAnimationsEnd) { 459 unblockLatch.countDown() 460 } 461 } 462 } 463 }) 464 465 currentlyRunningStartInternal = true 466 animator.startInternal() 467 currentlyRunningStartInternal = false 468 unblockLatch.countDown() 469 } 470 471 unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) 472 } 473 474 private fun cancelForTest(properties: Set<FloatPropertyCompat<in T>>) { 475 // If this was called from startInternal, we are already on the animation thread, and 476 // should just call cancelInternal rather than posting it. If we post it, the 477 // cancellation will occur after the rest of startInternal() and we'll immediately 478 // cancel the animation we worked so hard to start! 479 if (currentlyRunningStartInternal) { 480 animator.cancelInternal(properties) 481 return 482 } 483 484 val unblockLatch = CountDownLatch(1) 485 486 animationThreadHandler.post { 487 animator.cancelInternal(properties) 488 unblockLatch.countDown() 489 } 490 491 unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) 492 } 493 } 494 } 495