/* * Copyright (C) 2017 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 com.android.server.wm; import static com.android.server.wm.BoundsAnimationController.NO_PIP_MODE_CHANGED_CALLBACKS; import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_END; import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_START; import static com.android.server.wm.BoundsAnimationController.SchedulePipModeChangedState; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.Presubmit; import android.support.test.InstrumentationRegistry; import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.WindowManagerInternal.AppTransitionListener; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.android.server.wm.BoundsAnimationController.BoundsAnimator; /** * Test class for {@link BoundsAnimationController} to ensure that it sends the right callbacks * depending on the various interactions. * * We are really concerned about only three of the transition states [F = fullscreen, !F = floating] * F->!F, !F->!F, and !F->F. Each animation can only be cancelled from the target mid-transition, * or if a new animation starts on the same target. The tests below verifies that the target is * notified of all the cases where it is animating and cancelled so that it can respond * appropriately. * * Build/Install/Run: * bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests */ @SmallTest @Presubmit @RunWith(AndroidJUnit4.class) public class BoundsAnimationControllerTests extends WindowTestsBase { /** * Mock value animator to simulate updates with. */ private class MockValueAnimator extends ValueAnimator { private float mFraction; public MockValueAnimator getWithValue(float fraction) { mFraction = fraction; return this; } @Override public Object getAnimatedValue() { return mFraction; } } /** * Mock app transition to fire notifications to the bounds animator. */ private class MockAppTransition extends AppTransition { private AppTransitionListener mListener; MockAppTransition(Context context) { super(context, null); } @Override void registerListenerLocked(AppTransitionListener listener) { mListener = listener; } public void notifyTransitionPending() { mListener.onAppTransitionPendingLocked(); } public void notifyTransitionCancelled(int transit) { mListener.onAppTransitionCancelledLocked(transit); } public void notifyTransitionStarting(int transit) { mListener.onAppTransitionStartingLocked(transit, null, null, null, null); } public void notifyTransitionFinished() { mListener.onAppTransitionFinishedLocked(null); } } /** * A test animate bounds user to track callbacks from the bounds animation. */ private class TestBoundsAnimationTarget implements BoundsAnimationTarget { boolean mAwaitingAnimationStart; boolean mMovedToFullscreen; boolean mAnimationStarted; boolean mSchedulePipModeChangedOnStart; boolean mAnimationEnded; Rect mAnimationEndFinalStackBounds; boolean mSchedulePipModeChangedOnEnd; boolean mBoundsUpdated; boolean mCancelRequested; Rect mStackBounds; Rect mTaskBounds; void initialize(Rect from) { mAwaitingAnimationStart = true; mMovedToFullscreen = false; mAnimationStarted = false; mAnimationEnded = false; mAnimationEndFinalStackBounds = null; mSchedulePipModeChangedOnStart = false; mSchedulePipModeChangedOnEnd = false; mStackBounds = from; mTaskBounds = null; mBoundsUpdated = false; } @Override public void onAnimationStart(boolean schedulePipModeChangedCallback) { mAwaitingAnimationStart = false; mAnimationStarted = true; mSchedulePipModeChangedOnStart = schedulePipModeChangedCallback; } @Override public boolean setPinnedStackSize(Rect stackBounds, Rect taskBounds) { // TODO: Once we break the runs apart, we should fail() here if this is called outside // of onAnimationStart() and onAnimationEnd() if (mCancelRequested) { mCancelRequested = false; return false; } else { mBoundsUpdated = true; mStackBounds = stackBounds; mTaskBounds = taskBounds; return true; } } @Override public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackBounds, boolean moveToFullscreen) { mAnimationEnded = true; mAnimationEndFinalStackBounds = finalStackBounds; mSchedulePipModeChangedOnEnd = schedulePipModeChangedCallback; mMovedToFullscreen = moveToFullscreen; mTaskBounds = null; } } /** * Drives the animations, makes common assertions along the way. */ private class BoundsAnimationDriver { private BoundsAnimationController mController; private TestBoundsAnimationTarget mTarget; private BoundsAnimator mAnimator; private Rect mFrom; private Rect mTo; private Rect mLargerBounds; private Rect mExpectedFinalBounds; BoundsAnimationDriver(BoundsAnimationController controller, TestBoundsAnimationTarget target) { mController = controller; mTarget = target; } BoundsAnimationDriver start(Rect from, Rect to) { if (mAnimator != null) { throw new IllegalArgumentException("Call restart() to restart an animation"); } boolean fromFullscreen = from.equals(BOUNDS_FULL); boolean toFullscreen = to.equals(BOUNDS_FULL); mTarget.initialize(from); // Started, not running assertTrue(mTarget.mAwaitingAnimationStart); assertTrue(!mTarget.mAnimationStarted); startImpl(from, to); // Ensure that the animator is paused for the all windows drawn signal when animating // to/from fullscreen if (fromFullscreen || toFullscreen) { assertTrue(mAnimator.isPaused()); mController.onAllWindowsDrawn(); } else { assertTrue(!mAnimator.isPaused()); } // Started and running assertTrue(!mTarget.mAwaitingAnimationStart); assertTrue(mTarget.mAnimationStarted); return this; } BoundsAnimationDriver restart(Rect to) { if (mAnimator == null) { throw new IllegalArgumentException("Call start() to start a new animation"); } BoundsAnimator oldAnimator = mAnimator; boolean toSameBounds = mAnimator.isStarted() && to.equals(mTo); // Reset the animation start state mTarget.mAnimationStarted = false; // Start animation startImpl(mTarget.mStackBounds, to); if (toSameBounds) { // Same animator if same final bounds assertSame(oldAnimator, mAnimator); } // No animation start for replacing animation assertTrue(!mTarget.mAnimationStarted); mTarget.mAnimationStarted = true; return this; } private BoundsAnimationDriver startImpl(Rect from, Rect to) { boolean fromFullscreen = from.equals(BOUNDS_FULL); boolean toFullscreen = to.equals(BOUNDS_FULL); mFrom = new Rect(from); mTo = new Rect(to); mExpectedFinalBounds = new Rect(to); mLargerBounds = getLargerBounds(mFrom, mTo); // Start animation final @SchedulePipModeChangedState int schedulePipModeChangedState = toFullscreen ? SCHEDULE_PIP_MODE_CHANGED_ON_START : fromFullscreen ? SCHEDULE_PIP_MODE_CHANGED_ON_END : NO_PIP_MODE_CHANGED_CALLBACKS; mAnimator = mController.animateBoundsImpl(mTarget, from, to, DURATION, schedulePipModeChangedState, fromFullscreen, toFullscreen); // Original stack bounds, frozen task bounds assertEquals(mFrom, mTarget.mStackBounds); assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds); // Animating to larger size if (mFrom.equals(mLargerBounds)) { assertTrue(!mAnimator.animatingToLargerSize()); } else if (mTo.equals(mLargerBounds)) { assertTrue(mAnimator.animatingToLargerSize()); } return this; } BoundsAnimationDriver expectStarted(boolean schedulePipModeChanged) { // Callback made assertTrue(mTarget.mAnimationStarted); assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnStart); return this; } BoundsAnimationDriver update(float t) { mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(t)); // Temporary stack bounds, frozen task bounds if (t == 0f) { assertEquals(mFrom, mTarget.mStackBounds); } else if (t == 1f) { assertEquals(mTo, mTarget.mStackBounds); } else { assertNotEquals(mFrom, mTarget.mStackBounds); assertNotEquals(mTo, mTarget.mStackBounds); } assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds); return this; } BoundsAnimationDriver cancel() { // Cancel mTarget.mCancelRequested = true; mTarget.mBoundsUpdated = false; mExpectedFinalBounds = null; // Update mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(0.5f)); // Not started, not running, cancel reset assertTrue(!mTarget.mCancelRequested); // Stack/task bounds not updated assertTrue(!mTarget.mBoundsUpdated); // Callback made assertTrue(mTarget.mAnimationEnded); assertNull(mTarget.mAnimationEndFinalStackBounds); return this; } BoundsAnimationDriver end() { mAnimator.end(); // Final stack bounds assertEquals(mTo, mTarget.mStackBounds); assertEquals(mExpectedFinalBounds, mTarget.mAnimationEndFinalStackBounds); assertNull(mTarget.mTaskBounds); return this; } BoundsAnimationDriver expectEnded(boolean schedulePipModeChanged, boolean moveToFullscreen) { // Callback made assertTrue(mTarget.mAnimationEnded); assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnEnd); assertEquals(moveToFullscreen, mTarget.mMovedToFullscreen); return this; } private Rect getLargerBounds(Rect r1, Rect r2) { int r1Area = r1.width() * r1.height(); int r2Area = r2.width() * r2.height(); if (r1Area <= r2Area) { return r2; } else { return r1; } } } // Constants private static final boolean SCHEDULE_PIP_MODE_CHANGED = true; private static final boolean MOVE_TO_FULLSCREEN = true; private static final int DURATION = 100; // Some dummy bounds to represent fullscreen and floating bounds private static final Rect BOUNDS_FULL = new Rect(0, 0, 100, 100); private static final Rect BOUNDS_FLOATING = new Rect(60, 60, 95, 95); private static final Rect BOUNDS_SMALLER_FLOATING = new Rect(80, 80, 95, 95); // Common private MockAppTransition mMockAppTransition; private MockValueAnimator mMockAnimator; private TestBoundsAnimationTarget mTarget; private BoundsAnimationController mController; private BoundsAnimationDriver mDriver; // Temp private Rect mTmpRect = new Rect(); @Override public void setUp() throws Exception { super.setUp(); final Context context = InstrumentationRegistry.getTargetContext(); final Handler handler = new Handler(Looper.getMainLooper()); mMockAppTransition = new MockAppTransition(context); mMockAnimator = new MockValueAnimator(); mTarget = new TestBoundsAnimationTarget(); mController = new BoundsAnimationController(context, mMockAppTransition, handler, null); mDriver = new BoundsAnimationDriver(mController, mTarget); } /** BASE TRANSITIONS **/ @UiThreadTest @Test public void testFullscreenToFloatingTransition() throws Exception { mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0f) .update(0.5f) .update(1f) .end() .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToFullscreenTransition() throws Exception { mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) .expectStarted(SCHEDULE_PIP_MODE_CHANGED) .update(0f) .update(0.5f) .update(1f) .end() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToSmallerFloatingTransition() throws Exception { mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0f) .update(0.5f) .update(1f) .end() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToLargerFloatingTransition() throws Exception { mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0f) .update(0.5f) .update(1f) .end() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } /** F->!F w/ CANCEL **/ @UiThreadTest @Test public void testFullscreenToFloatingCancelFromTarget() throws Exception { mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .cancel() .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFullscreenToFloatingCancelFromAnimationToSameBounds() throws Exception { mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .restart(BOUNDS_FLOATING) .end() .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFullscreenToFloatingCancelFromAnimationToFloatingBounds() throws Exception { mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .restart(BOUNDS_SMALLER_FLOATING) .end() .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFullscreenToFloatingCancelFromAnimationToFullscreenBounds() throws Exception { mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .restart(BOUNDS_FULL) .end() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN); } /** !F->F w/ CANCEL **/ @UiThreadTest @Test public void testFloatingToFullscreenCancelFromTarget() throws Exception { mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) .expectStarted(SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .cancel() .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToFullscreenCancelFromAnimationToSameBounds() throws Exception { mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) .expectStarted(SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .restart(BOUNDS_FULL) .end() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToFullscreenCancelFromAnimationToFloatingBounds() throws Exception { mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) .expectStarted(SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .restart(BOUNDS_SMALLER_FLOATING) .end() .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } /** !F->!F w/ CANCEL **/ @UiThreadTest @Test public void testFloatingToSmallerFloatingCancelFromTarget() throws Exception { mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .cancel() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToLargerFloatingCancelFromTarget() throws Exception { mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING) .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) .update(0.25f) .cancel() .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } /** MISC **/ @UiThreadTest @Test public void testBoundsAreCopied() throws Exception { Rect from = new Rect(0, 0, 100, 100); Rect to = new Rect(25, 25, 75, 75); mDriver.start(from, to) .update(0.25f) .end(); assertEquals(new Rect(0, 0, 100, 100), from); assertEquals(new Rect(25, 25, 75, 75), to); } /** * @return whether the task and stack bounds would be the same if they were at the same offset. */ private boolean assertEqualSizeAtOffset(Rect stackBounds, Rect taskBounds) { mTmpRect.set(taskBounds); mTmpRect.offsetTo(stackBounds.left, stackBounds.top); return stackBounds.equals(mTmpRect); } }