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