/*
 * 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 com.android.server.wm.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, 0, 0, 0);
        }

        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 mForcePipModeChangedCallback;
        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;
            mForcePipModeChangedCallback = false;
            mSchedulePipModeChangedOnStart = false;
            mSchedulePipModeChangedOnEnd = false;
            mStackBounds = from;
            mTaskBounds = null;
            mBoundsUpdated = false;
        }

        @Override
        public void onAnimationStart(boolean schedulePipModeChangedCallback, boolean forceUpdate) {
            mAwaitingAnimationStart = false;
            mAnimationStarted = true;
            mSchedulePipModeChangedOnStart = schedulePipModeChangedCallback;
            mForcePipModeChangedCallback = forceUpdate;
        }

        @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, boolean expectStartedAndPipModeChangedCallback) {
            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);
            }

            if (expectStartedAndPipModeChangedCallback) {
                // Replacing animation with pending pip mode changed callback, ensure we update
                assertTrue(mTarget.mAnimationStarted);
                assertTrue(mTarget.mSchedulePipModeChangedOnStart);
                assertTrue(mTarget.mForcePipModeChangedCallback);
            } else {
                // 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, false /* expectStartedAndPipModeChangedCallback */)
                .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,
                        false /* expectStartedAndPipModeChangedCallback */)
                .end()
                .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN);
    }

    @UiThreadTest
    @Test
    public void testFullscreenToFloatingCancelFromAnimationToFullscreenBounds() throws Exception {
        // When animating from fullscreen and the animation is interruped, we expect the animation
        // start callback to be made, with a forced pip mode change callback
        mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING)
                .expectStarted(!SCHEDULE_PIP_MODE_CHANGED)
                .update(0.25f)
                .restart(BOUNDS_FULL, true /* expectStartedAndPipModeChangedCallback */)
                .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, false /* expectStartedAndPipModeChangedCallback */)
                .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,
                        false /* expectStartedAndPipModeChangedCallback */)
                .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);
    }
}
