• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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