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