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