blob: 1d9164d78b2edf9206d8e3ba5a6b384fc1b0c87b [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.server.wm.WindowInsetsAnimationControllerTests.ControlListener.Event.CANCELLED;
import static android.server.wm.WindowInsetsAnimationControllerTests.ControlListener.Event.FINISHED;
import static android.server.wm.WindowInsetsAnimationControllerTests.ControlListener.Event.READY;
import static android.server.wm.WindowInsetsAnimationUtils.INSETS_EVALUATOR;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeThat;
import static org.junit.Assume.assumeTrue;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Instrumentation;
import android.graphics.Insets;
import android.os.CancellationSignal;
import android.platform.test.annotations.Presubmit;
import android.server.wm.WindowInsetsAnimationTestBase.TestActivity;
import android.util.Log;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsAnimation.Callback;
import android.view.WindowInsetsAnimationControlListener;
import android.view.WindowInsetsAnimationController;
import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.cts.mockime.ImeEventStream;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Test whether {@link android.view.WindowInsetsController#controlWindowInsetsAnimation} properly
* works.
*
* Build/Install/Run:
* atest CtsWindowManagerDeviceTestCases:WindowInsetsAnimationControllerTests
*/
//TODO(b/159167851) @Presubmit
@RunWith(Parameterized.class)
public class WindowInsetsAnimationControllerTests extends WindowManagerTestBase {
TestActivity mActivity;
View mRootView;
ControlListener mListener;
CancellationSignal mCancellationSignal = new CancellationSignal();
Interpolator mInterpolator;
boolean mOnProgressCalled;
private ValueAnimator mAnimator;
List<VerifyingCallback> mCallbacks = new ArrayList<>();
private boolean mLossOfControlExpected;
public LimitedErrorCollector mErrorCollector = new LimitedErrorCollector();
/**
* {@link MockImeSession} used when {@link #mType} is
* {@link android.view.WindowInsets.Type#ime()}.
*/
@Nullable
private MockImeSession mMockImeSession;
@Parameter(0)
public int mType;
@Parameter(1)
public String mTypeDescription;
@Parameters(name= "{1}")
public static Object[][] types() {
return new Object[][] {
{ statusBars(), "statusBars" },
{ ime(), "ime" },
{ navigationBars(), "navigationBars" }
};
}
@Before
public void setUpWindowInsetsAnimationControllerTests() throws Throwable {
assumeFalse(
"In Automotive, auxiliary inset changes can happen when IME inset changes, so "
+ "allow Automotive skip IME inset animation tests.",
isCar() && mType == ime());
final ImeEventStream mockImeEventStream;
if (mType == ime()) {
final Instrumentation instrumentation = getInstrumentation();
assumeThat(MockImeSession.getUnavailabilityReason(instrumentation.getContext()),
nullValue());
// For the best test stability MockIme should be selected before launching TestActivity.
mMockImeSession = MockImeSession.create(
instrumentation.getContext(), instrumentation.getUiAutomation(),
new ImeSettings.Builder());
mockImeEventStream = mMockImeSession.openEventStream();
} else {
mockImeEventStream = null;
}
mActivity = startActivityInWindowingMode(TestActivity.class, WINDOWING_MODE_FULLSCREEN);
mRootView = mActivity.getWindow().getDecorView();
mListener = new ControlListener(mErrorCollector);
assumeTestCompatibility();
if (mockImeEventStream != null) {
// TestActivity has a focused EditText. Hence MockIme should receive onStartInput() for
// that EditText within a reasonable time.
expectEvent(mockImeEventStream,
editorMatcher("onStartInput", mActivity.getEditTextMarker()),
TimeUnit.SECONDS.toMillis(10));
}
awaitControl(mType);
}
@After
public void tearDown() throws Throwable {
runOnUiThread(() -> {}); // Fence to make sure we dispatched everything.
mCallbacks.forEach(VerifyingCallback::assertNoPendingAnimations);
// Unregistering VerifyingCallback as tearing down the MockIme also triggers UI events,
// which can trigger assertion failures in VerifyingCallback otherwise.
runOnUiThread(() -> {
mCallbacks.clear();
if (mRootView != null) {
mRootView.setWindowInsetsAnimationCallback(null);
}
});
// Now it should be safe to reset the IME to the default one.
if (mMockImeSession != null) {
mMockImeSession.close();
mMockImeSession = null;
}
mErrorCollector.verify();
}
private void assumeTestCompatibility() {
if (mType == navigationBars() || mType == statusBars()) {
assumeTrue(Insets.NONE
!= mRootView.getRootWindowInsets().getInsetsIgnoringVisibility(mType));
}
}
private void awaitControl(int type) throws Throwable {
CountDownLatch control = new CountDownLatch(1);
OnControllableInsetsChangedListener listener = (controller, controllableTypes) -> {
if ((controllableTypes & type) != 0)
control.countDown();
};
runOnUiThread(() -> mRootView.getWindowInsetsController()
.addOnControllableInsetsChangedListener(listener));
try {
if (!control.await(10, TimeUnit.SECONDS)) {
fail("Timeout waiting for control of " + type);
}
} finally {
runOnUiThread(() -> mRootView.getWindowInsetsController()
.removeOnControllableInsetsChangedListener(listener)
);
}
}
private void retryIfCancelled(ThrowableThrowingRunnable test) throws Throwable {
try {
mErrorCollector.verify();
test.run();
} catch (CancelledWhileWaitingForReadyException e) {
// Deflake cancellations waiting for ready - we'll reset state and try again.
runOnUiThread(() -> {
mCallbacks.clear();
if (mRootView != null) {
mRootView.setWindowInsetsAnimationCallback(null);
}
});
mErrorCollector = new LimitedErrorCollector();
mListener = new ControlListener(mErrorCollector);
awaitControl(mType);
test.run();
}
}
@Presubmit
@Test
public void testControl_andCancel() throws Throwable {
retryIfCancelled(() -> {
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, mCancellationSignal, mListener);
});
mListener.awaitAndAssert(READY);
runOnUiThread(() -> {
mCancellationSignal.cancel();
});
mListener.awaitAndAssert(CANCELLED);
mListener.assertWasNotCalled(FINISHED);
});
}
@Test
public void testControl_andImmediatelyCancel() throws Throwable {
retryIfCancelled(() -> {
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, mCancellationSignal, mListener);
mCancellationSignal.cancel();
});
mListener.assertWasCalled(CANCELLED);
mListener.assertWasNotCalled(READY);
mListener.assertWasNotCalled(FINISHED);
});
}
@Presubmit
@Test
public void testControl_immediately_show() throws Throwable {
retryIfCancelled(() -> {
setVisibilityAndWait(mType, false);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, null, mListener);
});
mListener.awaitAndAssert(READY);
runOnUiThread(() -> {
mListener.mController.finish(true);
});
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
@Presubmit
@Test
public void testControl_immediately_hide() throws Throwable {
retryIfCancelled(() -> {
setVisibilityAndWait(mType, true);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, null, mListener);
});
mListener.awaitAndAssert(READY);
runOnUiThread(() -> {
mListener.mController.finish(false);
});
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
@Presubmit
@Test
public void testControl_transition_show() throws Throwable {
retryIfCancelled(() -> {
setVisibilityAndWait(mType, false);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, null, mListener);
});
mListener.awaitAndAssert(READY);
runTransition(true);
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
@Presubmit
@Test
public void testControl_transition_hide() throws Throwable {
retryIfCancelled(() -> {
setVisibilityAndWait(mType, true);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, null, mListener);
});
mListener.awaitAndAssert(READY);
runTransition(false);
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
@Presubmit
@Test
public void testControl_transition_show_interpolator() throws Throwable {
retryIfCancelled(() -> {
mInterpolator = new DecelerateInterpolator();
setVisibilityAndWait(mType, false);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
mInterpolator, null, mListener);
});
mListener.awaitAndAssert(READY);
runTransition(true);
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
@Presubmit
@Test
public void testControl_transition_hide_interpolator() throws Throwable {
retryIfCancelled(() -> {
mInterpolator = new AccelerateInterpolator();
setVisibilityAndWait(mType, true);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
mInterpolator, null, mListener);
});
mListener.awaitAndAssert(READY);
runTransition(false);
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
@Test
public void testControl_andLoseControl() throws Throwable {
retryIfCancelled(() -> {
mInterpolator = new AccelerateInterpolator();
setVisibilityAndWait(mType, true);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
mInterpolator, null, mListener);
});
mListener.awaitAndAssert(READY);
runTransition(false, TimeUnit.MINUTES.toMillis(5));
runOnUiThread(() -> {
mLossOfControlExpected = true;
});
launchHomeActivityNoWait();
mListener.awaitAndAssert(CANCELLED);
mListener.assertWasNotCalled(FINISHED);
});
}
@Presubmit
@Test
public void testImeControl_isntInterruptedByStartingInput() throws Throwable {
if (mType != ime()) {
return;
}
retryIfCancelled(() -> {
setVisibilityAndWait(mType, false);
runOnUiThread(() -> {
setupAnimationListener();
mRootView.getWindowInsetsController().controlWindowInsetsAnimation(mType, 0,
null, null, mListener);
});
mListener.awaitAndAssert(READY);
runTransition(true);
runOnUiThread(() -> {
mActivity.getSystemService(InputMethodManager.class).restartInput(
mActivity.mEditor);
});
mListener.awaitAndAssert(FINISHED);
mListener.assertWasNotCalled(CANCELLED);
});
}
private void setupAnimationListener() {
WindowInsets initialInsets = mActivity.mLastWindowInsets;
VerifyingCallback callback = new VerifyingCallback(
new Callback(Callback.DISPATCH_MODE_STOP) {
@Override
public void onPrepare(@NonNull WindowInsetsAnimation animation) {
mErrorCollector.checkThat("onPrepare",
mActivity.mLastWindowInsets.getInsets(mType),
equalTo(initialInsets.getInsets(mType)));
}
@NonNull
@Override
public WindowInsetsAnimation.Bounds onStart(
@NonNull WindowInsetsAnimation animation,
@NonNull WindowInsetsAnimation.Bounds bounds) {
mErrorCollector.checkThat("onStart",
mActivity.mLastWindowInsets, not(equalTo(initialInsets)));
mErrorCollector.checkThat("onStart",
animation.getInterpolator(), sameInstance(mInterpolator));
return bounds;
}
@NonNull
@Override
public WindowInsets onProgress(@NonNull WindowInsets insets,
@NonNull List<WindowInsetsAnimation> runningAnimations) {
mOnProgressCalled = true;
if (mAnimator != null) {
float fraction = runningAnimations.get(0).getFraction();
mErrorCollector.checkThat(
String.format(Locale.US, "onProgress(%.2f)", fraction),
insets.getInsets(mType), equalTo(mAnimator.getAnimatedValue()));
mErrorCollector.checkThat("onProgress",
fraction, equalTo(mAnimator.getAnimatedFraction()));
Interpolator interpolator =
mInterpolator != null ? mInterpolator
: new LinearInterpolator();
mErrorCollector.checkThat("onProgress",
runningAnimations.get(0).getInterpolatedFraction(),
equalTo(interpolator.getInterpolation(
mAnimator.getAnimatedFraction())));
}
return insets;
}
@Override
public void onEnd(@NonNull WindowInsetsAnimation animation) {
mRootView.setWindowInsetsAnimationCallback(null);
}
});
mCallbacks.add(callback);
mRootView.setWindowInsetsAnimationCallback(callback);
}
private void runTransition(boolean show) throws Throwable {
runTransition(show, 1000);
}
private void runTransition(boolean show, long durationMillis) throws Throwable {
runOnUiThread(() -> {
mAnimator = ValueAnimator.ofObject(
INSETS_EVALUATOR,
show ? mListener.mController.getHiddenStateInsets()
: mListener.mController.getShownStateInsets(),
show ? mListener.mController.getShownStateInsets()
: mListener.mController.getHiddenStateInsets()
);
mAnimator.setDuration(durationMillis);
mAnimator.addUpdateListener((animator1) -> {
if (!mListener.mController.isReady()) {
// Lost control - Don't crash the instrumentation below.
if (!mLossOfControlExpected) {
mErrorCollector.addError(new AssertionError("Unexpectedly lost control."));
}
mAnimator.cancel();
return;
}
Insets insets = (Insets) mAnimator.getAnimatedValue();
mOnProgressCalled = false;
mListener.mController.setInsetsAndAlpha(insets, 1.0f,
mAnimator.getAnimatedFraction());
mErrorCollector.checkThat(
"setInsetsAndAlpha() must synchronously call onProgress() but didn't",
mOnProgressCalled, is(true));
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (!mListener.mController.isCancelled()) {
mListener.mController.finish(show);
}
}
});
mAnimator.start();
});
}
private void setVisibilityAndWait(int type, boolean visible) throws Throwable {
assertThat("setVisibilityAndWait must only be called before any"
+ " WindowInsetsAnimation.Callback was registered", mCallbacks, equalTo(List.of()));
final Set<WindowInsetsAnimation> runningAnimations = new HashSet<>();
Callback callback = new Callback(Callback.DISPATCH_MODE_STOP) {
@NonNull
@Override
public void onPrepare(@NonNull WindowInsetsAnimation animation) {
synchronized (runningAnimations) {
runningAnimations.add(animation);
}
}
@NonNull
@Override
public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation,
@NonNull WindowInsetsAnimation.Bounds bounds) {
synchronized (runningAnimations) {
runningAnimations.add(animation);
}
return bounds;
}
@NonNull
@Override
public WindowInsets onProgress(@NonNull WindowInsets insets,
@NonNull List<WindowInsetsAnimation> runningAnimations) {
return insets;
}
@Override
public void onEnd(@NonNull WindowInsetsAnimation animation) {
synchronized (runningAnimations) {
runningAnimations.remove(animation);
}
}
};
runOnUiThread(() -> {
mRootView.setWindowInsetsAnimationCallback(callback);
if (visible) {
mRootView.getWindowInsetsController().show(type);
} else {
mRootView.getWindowInsetsController().hide(type);
}
});
waitForOrFail("Timeout waiting for inset to become " + (visible ? "visible" : "invisible"),
() -> mActivity.mLastWindowInsets.isVisible(mType) == visible);
waitForOrFail("Timeout waiting for animations to end, running=" + runningAnimations,
() -> {
synchronized (runningAnimations) {
return runningAnimations.isEmpty();
}
});
runOnUiThread(() -> {
mRootView.setWindowInsetsAnimationCallback(null);
});
}
static class ControlListener implements WindowInsetsAnimationControlListener {
private final ErrorCollector mErrorCollector;
WindowInsetsAnimationController mController = null;
int mTypes = -1;
RuntimeException mCancelledStack = null;
ControlListener(ErrorCollector errorCollector) {
mErrorCollector = errorCollector;
}
enum Event {
READY, FINISHED, CANCELLED;
}
/** Latch for every callback event. */
private CountDownLatch[] mLatches = {
new CountDownLatch(1),
new CountDownLatch(1),
new CountDownLatch(1),
};
@Override
public void onReady(@NonNull WindowInsetsAnimationController controller, int types) {
mController = controller;
mTypes = types;
// Collect errors here and below, so we don't crash the main thread.
mErrorCollector.checkThat(controller, notNullValue());
mErrorCollector.checkThat(types, not(equalTo(0)));
mErrorCollector.checkThat("isReady", controller.isReady(), is(true));
mErrorCollector.checkThat("isFinished", controller.isFinished(), is(false));
mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(false));
report(READY);
}
@Override
public void onFinished(@NonNull WindowInsetsAnimationController controller) {
mErrorCollector.checkThat(controller, notNullValue());
mErrorCollector.checkThat(controller, sameInstance(mController));
mErrorCollector.checkThat("isReady", controller.isReady(), is(false));
mErrorCollector.checkThat("isFinished", controller.isFinished(), is(true));
mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(false));
report(FINISHED);
}
@Override
public void onCancelled(@Nullable WindowInsetsAnimationController controller) {
mErrorCollector.checkThat(controller, sameInstance(mController));
if (controller != null) {
mErrorCollector.checkThat("isReady", controller.isReady(), is(false));
mErrorCollector.checkThat("isFinished", controller.isFinished(), is(false));
mErrorCollector.checkThat("isCancelled", controller.isCancelled(), is(true));
}
mCancelledStack = new RuntimeException("onCancelled called here");
report(CANCELLED);
}
private void report(Event event) {
CountDownLatch latch = mLatches[event.ordinal()];
mErrorCollector.checkThat(event + ": count", latch.getCount(), is(1L));
latch.countDown();
}
void awaitAndAssert(Event event) {
CountDownLatch latch = mLatches[event.ordinal()];
try {
if (!latch.await(10, TimeUnit.SECONDS)) {
if (event == READY && mCancelledStack != null) {
throw new CancelledWhileWaitingForReadyException(
"expected " + event + " but instead got " + CANCELLED,
mCancelledStack);
}
fail("Timeout waiting for " + event + "; reported events: " + reportedEvents());
}
} catch (InterruptedException e) {
throw new AssertionError("Interrupted", e);
}
}
void assertWasCalled(Event event) {
CountDownLatch latch = mLatches[event.ordinal()];
assertEquals(event + " expected, but never called; called: " + reportedEvents(),
0, latch.getCount());
}
void assertWasNotCalled(Event event) {
CountDownLatch latch = mLatches[event.ordinal()];
assertEquals(event + " not expected, but was called; called: " + reportedEvents(),
1, latch.getCount());
}
String reportedEvents() {
return Arrays.stream(Event.values())
.filter((e) -> mLatches[e.ordinal()].getCount() == 0)
.map(Enum::toString)
.collect(Collectors.joining(",", "<", ">"));
}
}
private class VerifyingCallback extends Callback {
private final Callback mInner;
private final Set<WindowInsetsAnimation> mPreparedAnimations = new HashSet<>();
private final Set<WindowInsetsAnimation> mRunningAnimations = new HashSet<>();
private final Set<WindowInsetsAnimation> mEndedAnimations = new HashSet<>();
public VerifyingCallback(Callback callback) {
super(callback.getDispatchMode());
mInner = callback;
}
@Override
public void onPrepare(@NonNull WindowInsetsAnimation animation) {
mErrorCollector.checkThat("onPrepare: animation", animation, notNullValue());
mErrorCollector.checkThat("onPrepare", mPreparedAnimations, not(hasItem(animation)));
mPreparedAnimations.add(animation);
mInner.onPrepare(animation);
}
@NonNull
@Override
public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation,
@NonNull WindowInsetsAnimation.Bounds bounds) {
mErrorCollector.checkThat("onStart: animation", animation, notNullValue());
mErrorCollector.checkThat("onStart: bounds", bounds, notNullValue());
mErrorCollector.checkThat("onStart: mPreparedAnimations",
mPreparedAnimations, hasItem(animation));
mErrorCollector.checkThat("onStart: mRunningAnimations",
mRunningAnimations, not(hasItem(animation)));
mRunningAnimations.add(animation);
mPreparedAnimations.remove(animation);
return mInner.onStart(animation, bounds);
}
@NonNull
@Override
public WindowInsets onProgress(@NonNull WindowInsets insets,
@NonNull List<WindowInsetsAnimation> runningAnimations) {
mErrorCollector.checkThat("onProgress: insets", insets, notNullValue());
mErrorCollector.checkThat("onProgress: runningAnimations",
runningAnimations, notNullValue());
mErrorCollector.checkThat("onProgress", new HashSet<>(runningAnimations),
is(equalTo(mRunningAnimations)));
return mInner.onProgress(insets, runningAnimations);
}
@Override
public void onEnd(@NonNull WindowInsetsAnimation animation) {
mErrorCollector.checkThat("onEnd: animation", animation, notNullValue());
mErrorCollector.checkThat("onEnd for this animation was already dispatched",
mEndedAnimations, not(hasItem(animation)));
mErrorCollector.checkThat("onEnd: animation must be either running or prepared",
mRunningAnimations.contains(animation)
|| mPreparedAnimations.contains(animation),
is(true));
mRunningAnimations.remove(animation);
mPreparedAnimations.remove(animation);
mEndedAnimations.add(animation);
mInner.onEnd(animation);
}
public void assertNoPendingAnimations() {
mErrorCollector.checkThat("Animations with onStart but missing onEnd:",
mRunningAnimations, equalTo(Set.of()));
mErrorCollector.checkThat("Animations with onPrepare but missing onStart:",
mPreparedAnimations, equalTo(Set.of()));
}
}
public static final class LimitedErrorCollector extends ErrorCollector {
private static final int THROW_LIMIT = 1;
private static final int LOG_LIMIT = 10;
private static final boolean REPORT_SUPPRESSED_ERRORS_AS_THROWABLE = false;
private int mCount = 0;
private List<Throwable> mSuppressedErrors = new ArrayList<>();
@Override
public void addError(Throwable error) {
if (mCount < THROW_LIMIT) {
super.addError(error);
} else if (mCount < LOG_LIMIT) {
mSuppressedErrors.add(error);
}
mCount++;
}
@Override
protected void verify() throws Throwable {
if (mCount > THROW_LIMIT) {
if (REPORT_SUPPRESSED_ERRORS_AS_THROWABLE) {
super.addError(
new AssertionError((mCount - THROW_LIMIT) + " errors suppressed."));
} else {
Log.i("LimitedErrorCollector", (mCount - THROW_LIMIT) + " errors suppressed; "
+ "additional errors:");
for (Throwable t : mSuppressedErrors) {
Log.e("LimitedErrorCollector", "", t);
}
}
}
super.verify();
}
}
private interface ThrowableThrowingRunnable {
void run() throws Throwable;
}
private static class CancelledWhileWaitingForReadyException extends AssertionError {
public CancelledWhileWaitingForReadyException(String message, Throwable cause) {
super(message, cause);
}
};
}