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