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