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