1 /* 2 * 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 17 package android.server.wm; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.server.wm.WindowInsetsAnimationControllerTests.ControlListener.Event.CANCELLED; 21 import static android.server.wm.WindowInsetsAnimationControllerTests.ControlListener.Event.FINISHED; 22 import static android.server.wm.WindowInsetsAnimationControllerTests.ControlListener.Event.READY; 23 import static android.server.wm.WindowInsetsAnimationUtils.INSETS_EVALUATOR; 24 import static android.view.WindowInsets.Type.ime; 25 import static android.view.WindowInsets.Type.navigationBars; 26 import static android.view.WindowInsets.Type.statusBars; 27 28 import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; 29 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 30 31 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 32 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 33 34 import static org.hamcrest.Matchers.equalTo; 35 import static org.hamcrest.Matchers.hasItem; 36 import static org.hamcrest.Matchers.hasSize; 37 import static org.hamcrest.Matchers.is; 38 import static org.hamcrest.Matchers.not; 39 import static org.hamcrest.Matchers.notNullValue; 40 import static org.hamcrest.Matchers.nullValue; 41 import static org.hamcrest.Matchers.sameInstance; 42 import static org.junit.Assert.assertEquals; 43 import static org.junit.Assert.assertThat; 44 import static org.junit.Assert.fail; 45 import static org.junit.Assume.assumeFalse; 46 import static org.junit.Assume.assumeThat; 47 import static org.junit.Assume.assumeTrue; 48 49 import android.animation.Animator; 50 import android.animation.AnimatorListenerAdapter; 51 import android.animation.ValueAnimator; 52 import android.app.Instrumentation; 53 import android.graphics.Insets; 54 import android.os.CancellationSignal; 55 import android.platform.test.annotations.Presubmit; 56 import android.server.wm.WindowInsetsAnimationTestBase.TestActivity; 57 import android.util.Log; 58 import android.view.View; 59 import android.view.WindowInsets; 60 import android.view.WindowInsetsAnimation; 61 import android.view.WindowInsetsAnimation.Callback; 62 import android.view.WindowInsetsAnimationControlListener; 63 import android.view.WindowInsetsAnimationController; 64 import android.view.WindowInsetsController.OnControllableInsetsChangedListener; 65 import android.view.animation.AccelerateInterpolator; 66 import android.view.animation.DecelerateInterpolator; 67 import android.view.animation.Interpolator; 68 import android.view.animation.LinearInterpolator; 69 import android.view.inputmethod.InputMethodManager; 70 71 import androidx.annotation.NonNull; 72 import androidx.annotation.Nullable; 73 74 import com.android.cts.mockime.ImeEventStream; 75 import com.android.cts.mockime.ImeSettings; 76 import com.android.cts.mockime.MockImeSession; 77 78 import org.junit.After; 79 import org.junit.Before; 80 import org.junit.Test; 81 import org.junit.rules.ErrorCollector; 82 import org.junit.runner.RunWith; 83 import org.junit.runners.Parameterized; 84 import org.junit.runners.Parameterized.Parameter; 85 import org.junit.runners.Parameterized.Parameters; 86 87 import java.util.ArrayList; 88 import java.util.Arrays; 89 import java.util.HashSet; 90 import java.util.List; 91 import java.util.Locale; 92 import java.util.Set; 93 import java.util.concurrent.CountDownLatch; 94 import java.util.concurrent.TimeUnit; 95 import java.util.stream.Collectors; 96 97 /** 98 * Test whether {@link android.view.WindowInsetsController#controlWindowInsetsAnimation} properly 99 * works. 100 * 101 * Build/Install/Run: 102 * atest CtsWindowManagerDeviceTestCases:WindowInsetsAnimationControllerTests 103 */ 104 //TODO(b/159167851) @Presubmit 105 @RunWith(Parameterized.class) 106 public class WindowInsetsAnimationControllerTests extends WindowManagerTestBase { 107 108 TestActivity mActivity; 109 View mRootView; 110 ControlListener mListener; 111 CancellationSignal mCancellationSignal = new CancellationSignal(); 112 Interpolator mInterpolator; 113 boolean mOnProgressCalled; 114 private ValueAnimator mAnimator; 115 List<VerifyingCallback> mCallbacks = new ArrayList<>(); 116 private boolean mLossOfControlExpected; 117 118 public LimitedErrorCollector mErrorCollector = new LimitedErrorCollector(); 119 120 /** 121 * {@link MockImeSession} used when {@link #mType} is 122 * {@link android.view.WindowInsets.Type#ime()}. 123 */ 124 @Nullable 125 private MockImeSession mMockImeSession; 126 127 @Parameter(0) 128 public int mType; 129 130 @Parameter(1) 131 public String mTypeDescription; 132 133 @Parameters(name= "{1}") types()134 public static Object[][] types() { 135 return new Object[][] { 136 { statusBars(), "statusBars" }, 137 { ime(), "ime" }, 138 { navigationBars(), "navigationBars" } 139 }; 140 } 141 142 @Before setUpWindowInsetsAnimationControllerTests()143 public void setUpWindowInsetsAnimationControllerTests() throws Throwable { 144 assumeFalse( 145 "In Automotive, auxiliary inset changes can happen when IME inset changes, so " 146 + "allow Automotive skip IME inset animation tests.", 147 isCar() && mType == ime()); 148 149 final ImeEventStream mockImeEventStream; 150 if (mType == ime()) { 151 final Instrumentation instrumentation = getInstrumentation(); 152 assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()), 153 nullValue()); 154 155 // For the best test stability MockIme should be selected before launching TestActivity. 156 mMockImeSession = MockImeSession.create( 157 instrumentation.getContext(), instrumentation.getUiAutomation(), 158 new ImeSettings.Builder()); 159 mockImeEventStream = mMockImeSession.openEventStream(); 160 } else { 161 mockImeEventStream = null; 162 } 163 164 mActivity = startActivityInWindowingMode(TestActivity.class, WINDOWING_MODE_FULLSCREEN); 165 mRootView = mActivity.getWindow().getDecorView(); 166 mListener = new ControlListener(mErrorCollector); 167 assumeTestCompatibility(); 168 169 if (mockImeEventStream != null) { 170 // TestActivity has a focused EditText. Hence MockIme should receive onStartInput() for 171 // that EditText within a reasonable time. 172 expectEvent(mockImeEventStream, 173 editorMatcher("onStartInput", mActivity.getEditTextMarker()), 174 TimeUnit.SECONDS.toMillis(10)); 175 } 176 awaitControl(mType); 177 } 178 179 @After tearDown()180 public void tearDown() throws Throwable { 181 runOnUiThread(() -> {}); // Fence to make sure we dispatched everything. 182 mCallbacks.forEach(VerifyingCallback::assertNoPendingAnimations); 183 184 // Unregistering VerifyingCallback as tearing down the MockIme also triggers UI events, 185 // which can trigger assertion failures in VerifyingCallback otherwise. 186 runOnUiThread(() -> { 187 mCallbacks.clear(); 188 if (mRootView != null) { 189 mRootView.setWindowInsetsAnimationCallback(null); 190 } 191 }); 192 193 // Now it should be safe to reset the IME to the default one. 194 if (mMockImeSession != null) { 195 mMockImeSession.close(); 196 mMockImeSession = null; 197 } 198 mErrorCollector.verify(); 199 } 200 assumeTestCompatibility()201 private void assumeTestCompatibility() { 202 if (mType == navigationBars() || mType == statusBars()) { 203 assumeTrue(Insets.NONE 204 != mRootView.getRootWindowInsets().getInsetsIgnoringVisibility(mType)); 205 } 206 } 207 awaitControl(int type)208 private void awaitControl(int type) throws Throwable { 209 CountDownLatch control = new CountDownLatch(1); 210 OnControllableInsetsChangedListener listener = (controller, controllableTypes) -> { 211 if ((controllableTypes & type) != 0) 212 control.countDown(); 213 }; 214 runOnUiThread(() -> mRootView.getWindowInsetsController() 215 .addOnControllableInsetsChangedListener(listener)); 216 try { 217 if (!control.await(10, TimeUnit.SECONDS)) { 218 fail("Timeout waiting for control of " + type); 219 } 220 } finally { 221 runOnUiThread(() -> mRootView.getWindowInsetsController() 222 .removeOnControllableInsetsChangedListener(listener) 223 ); 224 } 225 } 226 retryIfCancelled(ThrowableThrowingRunnable test)227 private void retryIfCancelled(ThrowableThrowingRunnable test) throws Throwable { 228 try { 229 mErrorCollector.verify(); 230 test.run(); 231 } catch (CancelledWhileWaitingForReadyException e) { 232 // Deflake cancellations waiting for ready - we'll reset state and try again. 233 runOnUiThread(() -> { 234 mCallbacks.clear(); 235 if (mRootView != null) { 236 mRootView.setWindowInsetsAnimationCallback(null); 237 } 238 }); 239 mErrorCollector = new LimitedErrorCollector(); 240 mListener = new ControlListener(mErrorCollector); 241 awaitControl(mType); 242 test.run(); 243 } 244 } 245 246 @Presubmit 247 @Test testControl_andCancel()248 public void testControl_andCancel() throws Throwable { 249 retryIfCancelled(() -> { 250 runOnUiThread(() -> { 251 setupAnimationListener(); 252 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 253 null, mCancellationSignal, mListener); 254 }); 255 256 mListener.awaitAndAssert(READY); 257 258 runOnUiThread(() -> { 259 mCancellationSignal.cancel(); 260 }); 261 262 mListener.awaitAndAssert(CANCELLED); 263 mListener.assertWasNotCalled(FINISHED); 264 }); 265 } 266 267 @Test testControl_andImmediatelyCancel()268 public void testControl_andImmediatelyCancel() throws Throwable { 269 retryIfCancelled(() -> { 270 runOnUiThread(() -> { 271 setupAnimationListener(); 272 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 273 null, mCancellationSignal, mListener); 274 mCancellationSignal.cancel(); 275 }); 276 277 mListener.assertWasCalled(CANCELLED); 278 mListener.assertWasNotCalled(READY); 279 mListener.assertWasNotCalled(FINISHED); 280 }); 281 } 282 283 @Presubmit 284 @Test testControl_immediately_show()285 public void testControl_immediately_show() throws Throwable { 286 retryIfCancelled(() -> { 287 setVisibilityAndWait(mType, false); 288 289 runOnUiThread(() -> { 290 setupAnimationListener(); 291 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 292 null, null, mListener); 293 }); 294 295 mListener.awaitAndAssert(READY); 296 297 runOnUiThread(() -> { 298 mListener.mController.finish(true); 299 }); 300 301 mListener.awaitAndAssert(FINISHED); 302 mListener.assertWasNotCalled(CANCELLED); 303 }); 304 } 305 306 @Presubmit 307 @Test testControl_immediately_hide()308 public void testControl_immediately_hide() throws Throwable { 309 retryIfCancelled(() -> { 310 setVisibilityAndWait(mType, true); 311 312 runOnUiThread(() -> { 313 setupAnimationListener(); 314 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 315 null, null, mListener); 316 }); 317 318 mListener.awaitAndAssert(READY); 319 320 runOnUiThread(() -> { 321 mListener.mController.finish(false); 322 }); 323 324 mListener.awaitAndAssert(FINISHED); 325 mListener.assertWasNotCalled(CANCELLED); 326 }); 327 } 328 329 @Presubmit 330 @Test testControl_transition_show()331 public void testControl_transition_show() throws Throwable { 332 retryIfCancelled(() -> { 333 setVisibilityAndWait(mType, false); 334 335 runOnUiThread(() -> { 336 setupAnimationListener(); 337 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 338 null, null, mListener); 339 }); 340 341 mListener.awaitAndAssert(READY); 342 343 runTransition(true); 344 345 mListener.awaitAndAssert(FINISHED); 346 mListener.assertWasNotCalled(CANCELLED); 347 }); 348 } 349 350 @Presubmit 351 @Test testControl_transition_hide()352 public void testControl_transition_hide() throws Throwable { 353 retryIfCancelled(() -> { 354 setVisibilityAndWait(mType, true); 355 356 runOnUiThread(() -> { 357 setupAnimationListener(); 358 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 359 null, null, mListener); 360 }); 361 362 mListener.awaitAndAssert(READY); 363 364 runTransition(false); 365 366 mListener.awaitAndAssert(FINISHED); 367 mListener.assertWasNotCalled(CANCELLED); 368 }); 369 } 370 371 @Presubmit 372 @Test testControl_transition_show_interpolator()373 public void testControl_transition_show_interpolator() throws Throwable { 374 retryIfCancelled(() -> { 375 mInterpolator = new DecelerateInterpolator(); 376 setVisibilityAndWait(mType, false); 377 378 runOnUiThread(() -> { 379 setupAnimationListener(); 380 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 381 mInterpolator, null, mListener); 382 }); 383 384 mListener.awaitAndAssert(READY); 385 386 runTransition(true); 387 388 mListener.awaitAndAssert(FINISHED); 389 mListener.assertWasNotCalled(CANCELLED); 390 }); 391 } 392 393 @Presubmit 394 @Test testControl_transition_hide_interpolator()395 public void testControl_transition_hide_interpolator() throws Throwable { 396 retryIfCancelled(() -> { 397 mInterpolator = new AccelerateInterpolator(); 398 setVisibilityAndWait(mType, true); 399 400 runOnUiThread(() -> { 401 setupAnimationListener(); 402 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 403 mInterpolator, null, mListener); 404 }); 405 406 mListener.awaitAndAssert(READY); 407 408 runTransition(false); 409 410 mListener.awaitAndAssert(FINISHED); 411 mListener.assertWasNotCalled(CANCELLED); 412 }); 413 } 414 415 @Test testControl_andLoseControl()416 public void testControl_andLoseControl() throws Throwable { 417 retryIfCancelled(() -> { 418 mInterpolator = new AccelerateInterpolator(); 419 setVisibilityAndWait(mType, true); 420 421 runOnUiThread(() -> { 422 setupAnimationListener(); 423 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 424 mInterpolator, null, mListener); 425 }); 426 427 mListener.awaitAndAssert(READY); 428 429 runTransition(false, TimeUnit.MINUTES.toMillis(5)); 430 runOnUiThread(() -> { 431 mLossOfControlExpected = true; 432 }); 433 launchHomeActivityNoWait(); 434 435 mListener.awaitAndAssert(CANCELLED); 436 mListener.assertWasNotCalled(FINISHED); 437 }); 438 } 439 440 @Presubmit 441 @Test testImeControl_isntInterruptedByStartingInput()442 public void testImeControl_isntInterruptedByStartingInput() throws Throwable { 443 if (mType != ime()) { 444 return; 445 } 446 447 retryIfCancelled(() -> { 448 setVisibilityAndWait(mType, false); 449 450 runOnUiThread(() -> { 451 setupAnimationListener(); 452 mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0, 453 null, null, mListener); 454 }); 455 456 mListener.awaitAndAssert(READY); 457 458 runTransition(true); 459 runOnUiThread(() -> { 460 mActivity.getSystemService(InputMethodManager.class).restartInput( 461 mActivity.mEditor); 462 }); 463 464 mListener.awaitAndAssert(FINISHED); 465 mListener.assertWasNotCalled(CANCELLED); 466 }); 467 } 468 setupAnimationListener()469 private void setupAnimationListener() { 470 WindowInsets initialInsets = mActivity.mLastWindowInsets; 471 VerifyingCallback callback = new VerifyingCallback( 472 new Callback(Callback.DISPATCH_MODE_STOP) { 473 @Override 474 public void onPrepare(@NonNull WindowInsetsAnimation animation) { 475 mErrorCollector.checkThat("onPrepare", 476 mActivity.mLastWindowInsets.getInsets(mType), 477 equalTo(initialInsets.getInsets(mType))); 478 } 479 480 @NonNull 481 @Override 482 public WindowInsetsAnimation.Bounds onStart( 483 @NonNull WindowInsetsAnimation animation, 484 @NonNull WindowInsetsAnimation.Bounds bounds) { 485 mErrorCollector.checkThat("onStart", 486 mActivity.mLastWindowInsets, not(equalTo(initialInsets))); 487 mErrorCollector.checkThat("onStart", 488 animation.getInterpolator(), sameInstance(mInterpolator)); 489 return bounds; 490 } 491 492 @NonNull 493 @Override 494 public WindowInsets onProgress(@NonNull WindowInsets insets, 495 @NonNull List<WindowInsetsAnimation> runningAnimations) { 496 mOnProgressCalled = true; 497 if (mAnimator != null) { 498 float fraction = runningAnimations.get(0).getFraction(); 499 mErrorCollector.checkThat( 500 String.format(Locale.US, "onProgress(%.2f)", fraction), 501 insets.getInsets(mType), equalTo(mAnimator.getAnimatedValue())); 502 mErrorCollector.checkThat("onProgress", 503 fraction, equalTo(mAnimator.getAnimatedFraction())); 504 505 Interpolator interpolator = 506 mInterpolator != null ? mInterpolator 507 : new LinearInterpolator(); 508 mErrorCollector.checkThat("onProgress", 509 runningAnimations.get(0).getInterpolatedFraction(), 510 equalTo(interpolator.getInterpolation( 511 mAnimator.getAnimatedFraction()))); 512 } 513 return insets; 514 } 515 516 @Override 517 public void onEnd(@NonNull WindowInsetsAnimation animation) { 518 mRootView.setWindowInsetsAnimationCallback(null); 519 } 520 }); 521 mCallbacks.add(callback); 522 mRootView.setWindowInsetsAnimationCallback(callback); 523 } 524 runTransition(boolean show)525 private void runTransition(boolean show) throws Throwable { 526 runTransition(show, 1000); 527 } 528 runTransition(boolean show, long durationMillis)529 private void runTransition(boolean show, long durationMillis) throws Throwable { 530 runOnUiThread(() -> { 531 mAnimator = ValueAnimator.ofObject( 532 INSETS_EVALUATOR, 533 show ? mListener.mController.getHiddenStateInsets() 534 : mListener.mController.getShownStateInsets(), 535 show ? mListener.mController.getShownStateInsets() 536 : mListener.mController.getHiddenStateInsets() 537 ); 538 mAnimator.setDuration(durationMillis); 539 mAnimator.addUpdateListener((animator1) -> { 540 if (!mListener.mController.isReady()) { 541 // Lost control - Don't crash the instrumentation below. 542 if (!mLossOfControlExpected) { 543 mErrorCollector.addError(new AssertionError("Unexpectedly lost control.")); 544 } 545 mAnimator.cancel(); 546 return; 547 } 548 Insets insets = (Insets) mAnimator.getAnimatedValue(); 549 mOnProgressCalled = false; 550 mListener.mController.setInsetsAndAlpha(insets, 1.0f, 551 mAnimator.getAnimatedFraction()); 552 mErrorCollector.checkThat( 553 "setInsetsAndAlpha() must synchronously call onProgress() but didn't", 554 mOnProgressCalled, is(true)); 555 }); 556 mAnimator.addListener(new AnimatorListenerAdapter() { 557 @Override 558 public void onAnimationEnd(Animator animation) { 559 if (!mListener.mController.isCancelled()) { 560 mListener.mController.finish(show); 561 } 562 } 563 }); 564 565 mAnimator.start(); 566 }); 567 } 568 setVisibilityAndWait(int type, boolean visible)569 private void setVisibilityAndWait(int type, boolean visible) throws Throwable { 570 assertThat("setVisibilityAndWait must only be called before any" 571 + " WindowInsetsAnimation.Callback was registered", mCallbacks, equalTo(List.of())); 572 573 574 final Set<WindowInsetsAnimation> runningAnimations = new HashSet<>(); 575 Callback callback = new Callback(Callback.DISPATCH_MODE_STOP) { 576 577 @NonNull 578 @Override 579 public void onPrepare(@NonNull WindowInsetsAnimation animation) { 580 synchronized (runningAnimations) { 581 runningAnimations.add(animation); 582 } 583 } 584 585 @NonNull 586 @Override 587 public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, 588 @NonNull WindowInsetsAnimation.Bounds bounds) { 589 synchronized (runningAnimations) { 590 runningAnimations.add(animation); 591 } 592 return bounds; 593 } 594 595 @NonNull 596 @Override 597 public WindowInsets onProgress(@NonNull WindowInsets insets, 598 @NonNull List<WindowInsetsAnimation> runningAnimations) { 599 return insets; 600 } 601 602 @Override 603 public void onEnd(@NonNull WindowInsetsAnimation animation) { 604 synchronized (runningAnimations) { 605 runningAnimations.remove(animation); 606 } 607 } 608 }; 609 runOnUiThread(() -> { 610 mRootView.setWindowInsetsAnimationCallback(callback); 611 if (visible) { 612 mRootView.getWindowInsetsController().show(type); 613 } else { 614 mRootView.getWindowInsetsController().hide(type); 615 } 616 }); 617 618 waitForOrFail("Timeout waiting for inset to become " + (visible ? "visible" : "invisible"), 619 () -> mActivity.mLastWindowInsets.isVisible(mType) == visible); 620 waitForOrFail("Timeout waiting for animations to end, running=" + runningAnimations, 621 () -> { 622 synchronized (runningAnimations) { 623 return runningAnimations.isEmpty(); 624 } 625 }); 626 627 runOnUiThread(() -> { 628 mRootView.setWindowInsetsAnimationCallback(null); 629 }); 630 } 631 632 static class ControlListener implements WindowInsetsAnimationControlListener { 633 private final ErrorCollector mErrorCollector; 634 635 WindowInsetsAnimationController mController = null; 636 int mTypes = -1; 637 RuntimeException mCancelledStack = null; 638 ControlListener(ErrorCollector errorCollector)639 ControlListener(ErrorCollector errorCollector) { 640 mErrorCollector = errorCollector; 641 } 642 643 enum Event { 644 READY, FINISHED, CANCELLED; 645 } 646 647 /** Latch for every callback event. */ 648 private CountDownLatch[] mLatches = { 649 new CountDownLatch(1), 650 new CountDownLatch(1), 651 new CountDownLatch(1), 652 }; 653 654 @Override onReady(@onNull WindowInsetsAnimationController controller, int types)655 public void onReady(@NonNull WindowInsetsAnimationController controller, int types) { 656 mController = controller; 657 mTypes = types; 658 659 // Collect errors here and below, so we don't crash the main thread. 660 mErrorCollector.checkThat(controller, notNullValue()); 661 mErrorCollector.checkThat(types, not(equalTo(0))); 662 mErrorCollector.checkThat("isReady", controller.isReady(), is(true)); 663 mErrorCollector.checkThat("isFinished", controller.isFinished(), is(false)); 664 mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(false)); 665 report(READY); 666 } 667 668 @Override onFinished(@onNull WindowInsetsAnimationController controller)669 public void onFinished(@NonNull WindowInsetsAnimationController controller) { 670 mErrorCollector.checkThat(controller, notNullValue()); 671 mErrorCollector.checkThat(controller, sameInstance(mController)); 672 mErrorCollector.checkThat("isReady", controller.isReady(), is(false)); 673 mErrorCollector.checkThat("isFinished", controller.isFinished(), is(true)); 674 mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(false)); 675 report(FINISHED); 676 } 677 678 @Override onCancelled(@ullable WindowInsetsAnimationController controller)679 public void onCancelled(@Nullable WindowInsetsAnimationController controller) { 680 mErrorCollector.checkThat(controller, sameInstance(mController)); 681 if (controller != null) { 682 mErrorCollector.checkThat("isReady", controller.isReady(), is(false)); 683 mErrorCollector.checkThat("isFinished", controller.isFinished(), is(false)); 684 mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(true)); 685 } 686 mCancelledStack = new RuntimeException("onCancelled called here"); 687 report(CANCELLED); 688 } 689 report(Event event)690 private void report(Event event) { 691 CountDownLatch latch = mLatches[event.ordinal()]; 692 mErrorCollector.checkThat(event + ": count", latch.getCount(), is(1L)); 693 latch.countDown(); 694 } 695 awaitAndAssert(Event event)696 void awaitAndAssert(Event event) { 697 CountDownLatch latch = mLatches[event.ordinal()]; 698 try { 699 if (!latch.await(10, TimeUnit.SECONDS)) { 700 if (event == READY && mCancelledStack != null) { 701 throw new CancelledWhileWaitingForReadyException( 702 "expected " + event + " but instead got " + CANCELLED, 703 mCancelledStack); 704 } 705 fail("Timeout waiting for " + event + "; reported events: " + reportedEvents()); 706 } 707 } catch (InterruptedException e) { 708 throw new AssertionError("Interrupted", e); 709 } 710 } 711 assertWasCalled(Event event)712 void assertWasCalled(Event event) { 713 CountDownLatch latch = mLatches[event.ordinal()]; 714 assertEquals(event + " expected, but never called; called: " + reportedEvents(), 715 0, latch.getCount()); 716 } 717 assertWasNotCalled(Event event)718 void assertWasNotCalled(Event event) { 719 CountDownLatch latch = mLatches[event.ordinal()]; 720 assertEquals(event + " not expected, but was called; called: " + reportedEvents(), 721 1, latch.getCount()); 722 } 723 reportedEvents()724 String reportedEvents() { 725 return Arrays.stream(Event.values()) 726 .filter((e) -> mLatches[e.ordinal()].getCount() == 0) 727 .map(Enum::toString) 728 .collect(Collectors.joining(",", "<", ">")); 729 } 730 } 731 732 733 private class VerifyingCallback extends Callback { 734 private final Callback mInner; 735 private final Set<WindowInsetsAnimation> mPreparedAnimations = new HashSet<>(); 736 private final Set<WindowInsetsAnimation> mRunningAnimations = new HashSet<>(); 737 private final Set<WindowInsetsAnimation> mEndedAnimations = new HashSet<>(); 738 VerifyingCallback(Callback callback)739 public VerifyingCallback(Callback callback) { 740 super(callback.getDispatchMode()); 741 mInner = callback; 742 } 743 744 @Override onPrepare(@onNull WindowInsetsAnimation animation)745 public void onPrepare(@NonNull WindowInsetsAnimation animation) { 746 mErrorCollector.checkThat("onPrepare: animation", animation, notNullValue()); 747 mErrorCollector.checkThat("onPrepare", mPreparedAnimations, not(hasItem(animation))); 748 mPreparedAnimations.add(animation); 749 mInner.onPrepare(animation); 750 } 751 752 @NonNull 753 @Override onStart(@onNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds)754 public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, 755 @NonNull WindowInsetsAnimation.Bounds bounds) { 756 mErrorCollector.checkThat("onStart: animation", animation, notNullValue()); 757 mErrorCollector.checkThat("onStart: bounds", bounds, notNullValue()); 758 759 mErrorCollector.checkThat("onStart: mPreparedAnimations", 760 mPreparedAnimations, hasItem(animation)); 761 mErrorCollector.checkThat("onStart: mRunningAnimations", 762 mRunningAnimations, not(hasItem(animation))); 763 mRunningAnimations.add(animation); 764 mPreparedAnimations.remove(animation); 765 return mInner.onStart(animation, bounds); 766 } 767 768 @NonNull 769 @Override onProgress(@onNull WindowInsets insets, @NonNull List<WindowInsetsAnimation> runningAnimations)770 public WindowInsets onProgress(@NonNull WindowInsets insets, 771 @NonNull List<WindowInsetsAnimation> runningAnimations) { 772 mErrorCollector.checkThat("onProgress: insets", insets, notNullValue()); 773 mErrorCollector.checkThat("onProgress: runningAnimations", 774 runningAnimations, notNullValue()); 775 776 mErrorCollector.checkThat("onProgress", new HashSet<>(runningAnimations), 777 is(equalTo(mRunningAnimations))); 778 return mInner.onProgress(insets, runningAnimations); 779 } 780 781 @Override onEnd(@onNull WindowInsetsAnimation animation)782 public void onEnd(@NonNull WindowInsetsAnimation animation) { 783 mErrorCollector.checkThat("onEnd: animation", animation, notNullValue()); 784 785 mErrorCollector.checkThat("onEnd for this animation was already dispatched", 786 mEndedAnimations, not(hasItem(animation))); 787 mErrorCollector.checkThat("onEnd: animation must be either running or prepared", 788 mRunningAnimations.contains(animation) 789 || mPreparedAnimations.contains(animation), 790 is(true)); 791 mRunningAnimations.remove(animation); 792 mPreparedAnimations.remove(animation); 793 mEndedAnimations.add(animation); 794 mInner.onEnd(animation); 795 } 796 assertNoPendingAnimations()797 public void assertNoPendingAnimations() { 798 mErrorCollector.checkThat("Animations with onStart but missing onEnd:", 799 mRunningAnimations, equalTo(Set.of())); 800 mErrorCollector.checkThat("Animations with onPrepare but missing onStart:", 801 mPreparedAnimations, equalTo(Set.of())); 802 } 803 } 804 805 public static final class LimitedErrorCollector extends ErrorCollector { 806 private static final int THROW_LIMIT = 1; 807 private static final int LOG_LIMIT = 10; 808 private static final boolean REPORT_SUPPRESSED_ERRORS_AS_THROWABLE = false; 809 private int mCount = 0; 810 private List<Throwable> mSuppressedErrors = new ArrayList<>(); 811 812 @Override addError(Throwable error)813 public void addError(Throwable error) { 814 if (mCount < THROW_LIMIT) { 815 super.addError(error); 816 } else if (mCount < LOG_LIMIT) { 817 mSuppressedErrors.add(error); 818 } 819 mCount++; 820 } 821 822 @Override verify()823 protected void verify() throws Throwable { 824 if (mCount > THROW_LIMIT) { 825 if (REPORT_SUPPRESSED_ERRORS_AS_THROWABLE) { 826 super.addError( 827 new AssertionError((mCount - THROW_LIMIT) + " errors suppressed.")); 828 } else { 829 Log.i("LimitedErrorCollector", (mCount - THROW_LIMIT) + " errors suppressed; " 830 + "additional errors:"); 831 for (Throwable t : mSuppressedErrors) { 832 Log.e("LimitedErrorCollector", "", t); 833 } 834 } 835 } 836 super.verify(); 837 } 838 } 839 840 private interface ThrowableThrowingRunnable { run()841 void run() throws Throwable; 842 } 843 844 private static class CancelledWhileWaitingForReadyException extends AssertionError { CancelledWhileWaitingForReadyException(String message, Throwable cause)845 public CancelledWhileWaitingForReadyException(String message, Throwable cause) { 846 super(message, cause); 847 } 848 }; 849 } 850