OSDN Git Service

Schedule PIP mode changes at the beginning/end of the transitions.
authorWinson Chung <winsonc@google.com>
Sun, 16 Apr 2017 22:59:43 +0000 (15:59 -0700)
committerWinson Chung <winsonc@google.com>
Mon, 17 Apr 2017 04:23:57 +0000 (21:23 -0700)
- When transitioning from fullscreen to PiP, ensure all PiP/MW/Config
  changes come after the transition completes, and inversely, from PiP to
  fullscreen, ensure that all changes come before the transition up starts
- Add a series of tests to verify the callback state when the animation
  is canceled
- Also fixes an issue where the surface is preserved when we don't want

Bug: 37169080
Bug: 37103000
Test: bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests
Test: android.server.cts.ActivityManagerPinnedStackTests
Change-Id: I6425c95df358358ed76d9cc8a130606c2124062e

services/core/java/com/android/server/am/ActivityManagerService.java
services/core/java/com/android/server/am/ActivityStack.java
services/core/java/com/android/server/am/ActivityStackSupervisor.java
services/core/java/com/android/server/am/PinnedActivityStack.java
services/core/java/com/android/server/wm/BoundsAnimationController.java
services/core/java/com/android/server/wm/BoundsAnimationTarget.java [new file with mode: 0644]
services/core/java/com/android/server/wm/PinnedStackWindowController.java
services/core/java/com/android/server/wm/TaskStack.java
services/core/java/com/android/server/wm/WindowState.java
services/core/java/com/android/server/wm/WindowStateAnimator.java
services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java

index 6b0a73b..1c2a6aa 100644 (file)
@@ -10535,8 +10535,9 @@ public class ActivityManagerService extends IActivityManager.Stub
                         final PinnedActivityStack pinnedStack =
                                 mStackSupervisor.getStack(PINNED_STACK_ID);
                         if (pinnedStack != null) {
-                            pinnedStack.animateResizePinnedStack(null /* sourceBounds */,
-                                    destBounds, animationDuration);
+                            pinnedStack.animateResizePinnedStack(null /* sourceHintBounds */,
+                                    destBounds, animationDuration,
+                                    false /* schedulePipModeChangedOnAnimationEnd */);
                         }
                     } else {
                         throw new IllegalArgumentException("Stack: " + stackId
index 4c84d98..825e8ac 100644 (file)
@@ -589,6 +589,13 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
     }
 
     /**
+     * Returns whether to defer the scheduling of the multi-window mode.
+     */
+    boolean deferScheduleMultiWindowModeChanged() {
+        return false;
+    }
+
+    /**
      * Defers updating the bounds of the stack. If the stack was resized/repositioned while
      * deferring, the bounds will update in {@link #continueUpdateBounds()}.
      */
index 4d16e33..43ae4b2 100644 (file)
@@ -2504,7 +2504,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
         // incorrect if AMS.resizeStackWithBoundsFromWindowManager() is already called while waiting
         // for the AMS lock to be freed. So check and make sure these bounds are still good.
         final PinnedStackWindowController stackController = stack.getWindowContainerController();
-        if (stackController.pinnedStackResizeAllowed()) {
+        if (stackController.pinnedStackResizeDisallowed()) {
             return;
         }
 
@@ -2873,7 +2873,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
         return true;
     }
 
-    void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceBounds, float aspectRatio,
+    void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceHintBounds, float aspectRatio,
             boolean moveHomeStackToFront, String reason) {
 
         mWindowManager.deferSurfaceLayout();
@@ -2948,11 +2948,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
         final Rect destBounds = mWindowManager.getPictureInPictureBounds(DEFAULT_DISPLAY,
                 aspectRatio, false /* useExistingStackBounds */);
 
-        // TODO(b/36099777): Schedule the PiP mode change here immediately until we can defer all
-        // callbacks until after the bounds animation
-        scheduleUpdatePictureInPictureModeIfNeeded(r.getTask(), destBounds, true /* immediate */);
-
-        stack.animateResizePinnedStack(sourceBounds, destBounds, -1 /* animationDuration */);
+        stack.animateResizePinnedStack(sourceHintBounds, destBounds, -1 /* animationDuration */,
+                true /* schedulePipModeChangedOnAnimationEnd */);
         mService.mTaskChangeNotificationController.notifyActivityPinned(r.packageName);
     }
 
@@ -4179,6 +4176,12 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
     }
 
     void scheduleUpdateMultiWindowMode(TaskRecord task) {
+        // If the stack is animating in a way where we will be forcing a multi-mode change at the
+        // end, then ensure that we defer all in between multi-window mode changes
+        if (task.getStack().deferScheduleMultiWindowModeChanged()) {
+            return;
+        }
+
         for (int i = task.mActivities.size() - 1; i >= 0; i--) {
             final ActivityRecord r = task.mActivities.get(i);
             if (r.app != null && r.app.thread != null) {
index cd9c42c..a4932bb 100644 (file)
@@ -43,9 +43,10 @@ class PinnedActivityStack extends ActivityStack<PinnedStackWindowController>
         return new PinnedStackWindowController(mStackId, this, displayId, onTop, outBounds);
     }
 
-    void animateResizePinnedStack(Rect sourceBounds, Rect destBounds, int animationDuration) {
-        getWindowContainerController().animateResizePinnedStack(sourceBounds, destBounds,
-                animationDuration);
+    void animateResizePinnedStack(Rect sourceHintBounds, Rect toBounds, int animationDuration,
+            boolean schedulePipModeChangedOnAnimationEnd) {
+        getWindowContainerController().animateResizePinnedStack(toBounds, sourceHintBounds,
+                animationDuration, schedulePipModeChangedOnAnimationEnd);
     }
 
     void setPictureInPictureAspectRatio(float aspectRatio) {
@@ -60,7 +61,18 @@ class PinnedActivityStack extends ActivityStack<PinnedStackWindowController>
         return getWindowContainerController().isAnimatingBoundsToFullscreen();
     }
 
-    @Override
+    /**
+     * Returns whether to defer the scheduling of the multi-window mode.
+     */
+    boolean deferScheduleMultiWindowModeChanged() {
+        // For the pinned stack, the deferring of the multi-window mode changed is tied to the
+        // transition animation into picture-in-picture, and is called once the animation completes,
+        // or is interrupted in a way that would leave the stack in a non-fullscreen state.
+        // @see BoundsAnimationController
+        // @see BoundsAnimationControllerTests
+        return mWindowContainerController.deferScheduleMultiWindowModeChanged();
+    }
+
     public void updatePictureInPictureModeForPinnedStackAnimation(Rect targetStackBounds) {
         // It is guaranteed that the activities requiring the update will be in the pinned stack at
         // this point (either reparented before the animation into PiP, or before reparenting after
index f41eed5..e634552 100644 (file)
@@ -22,6 +22,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.animation.Animator;
 import android.animation.ValueAnimator;
+import android.annotation.IntDef;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Handler;
@@ -35,6 +36,9 @@ import android.view.WindowManagerInternal;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Enables animating bounds of objects.
  *
@@ -43,7 +47,7 @@ import com.android.internal.annotations.VisibleForTesting;
  * relaunching it would cause poorer experience), these class provides a way to directly animate
  * the bounds of the resized object.
  *
- * The object that is resized needs to implement {@link AnimateBoundsUser} interface.
+ * The object that is resized needs to implement {@link BoundsAnimationTarget} interface.
  *
  * NOTE: All calls to methods in this class should be done on the UI thread
  */
@@ -56,8 +60,19 @@ public class BoundsAnimationController {
 
     private static final int DEFAULT_TRANSITION_DURATION = 425;
 
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({NO_PIP_MODE_CHANGED_CALLBACKS, SCHEDULE_PIP_MODE_CHANGED_ON_START,
+        SCHEDULE_PIP_MODE_CHANGED_ON_END})
+    public @interface SchedulePipModeChangedState {}
+    /** Do not schedule any PiP mode changed callbacks as a part of this animation. */
+    public static final int NO_PIP_MODE_CHANGED_CALLBACKS = 0;
+    /** Schedule a PiP mode changed callback when this animation starts. */
+    public static final int SCHEDULE_PIP_MODE_CHANGED_ON_START = 1;
+    /** Schedule a PiP mode changed callback when this animation ends. */
+    public static final int SCHEDULE_PIP_MODE_CHANGED_ON_END = 2;
+
     // Only accessed on UI thread.
-    private ArrayMap<AnimateBoundsUser, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
+    private ArrayMap<BoundsAnimationTarget, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
 
     private final class AppTransitionNotifier
             extends WindowManagerInternal.AppTransitionListener implements Runnable {
@@ -108,40 +123,42 @@ public class BoundsAnimationController {
     @VisibleForTesting
     final class BoundsAnimator extends ValueAnimator
             implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
-        private final AnimateBoundsUser mTarget;
+        private final BoundsAnimationTarget mTarget;
         private final Rect mFrom = new Rect();
         private final Rect mTo = new Rect();
         private final Rect mTmpRect = new Rect();
         private final Rect mTmpTaskBounds = new Rect();
-        private final boolean mMoveToFullScreen;
-        // True if this this animation was cancelled and will be replaced the another animation from
-        // the same {@link #AnimateBoundsUser} target.
+
+        // True if this this animation was canceled and will be replaced the another animation from
+        // the same {@link #BoundsAnimationTarget} target.
         private boolean mSkipFinalResize;
         // True if this animation replaced a previous animation of the same
-        // {@link #AnimateBoundsUser} target.
+        // {@link #BoundsAnimationTarget} target.
         private final boolean mSkipAnimationStart;
-        // True if this animation was cancelled by the user, not as a part of a replacing animation
+        // True if this animation was canceled by the user, not as a part of a replacing animation
         private boolean mSkipAnimationEnd;
-        // True if this animation is not replacing a previous animation, or if the previous
-        // animation is animating to a different fullscreen state than the current animation.
-        // We use this to ensure that we always provide a consistent set/order of callbacks when we
-        // transition to/from PiP.
-        private final boolean mAnimatingToNewFullscreenState;
+        // True if the animation target should be moved to the fullscreen stack at the end of this
+        // animation
+        private boolean mMoveToFullscreen;
+
+        // Whether to schedule PiP mode changes on animation start/end
+        private @SchedulePipModeChangedState int mSchedulePipModeChangedState;
 
         // Depending on whether we are animating from
         // a smaller to a larger size
         private final int mFrozenTaskWidth;
         private final int mFrozenTaskHeight;
 
-        BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to, boolean moveToFullScreen,
-                boolean replacingExistingAnimation, boolean animatingToNewFullscreenState) {
+        BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to,
+                @SchedulePipModeChangedState int schedulePipModeChangedState,
+                boolean moveToFullscreen, boolean replacingExistingAnimation) {
             super();
             mTarget = target;
             mFrom.set(from);
             mTo.set(to);
-            mMoveToFullScreen = moveToFullScreen;
             mSkipAnimationStart = replacingExistingAnimation;
-            mAnimatingToNewFullscreenState = animatingToNewFullscreenState;
+            mSchedulePipModeChangedState = schedulePipModeChangedState;
+            mMoveToFullscreen = moveToFullscreen;
             addUpdateListener(this);
             addListener(this);
 
@@ -161,7 +178,8 @@ public class BoundsAnimationController {
         @Override
         public void onAnimationStart(Animator animation) {
             if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget
-                    + " mSkipAnimationStart=" + mSkipAnimationStart);
+                    + " mSkipAnimationStart=" + mSkipAnimationStart
+                    + " mSchedulePipModeChangedState=" + mSchedulePipModeChangedState);
             mFinishAnimationAfterTransition = false;
             mTmpRect.set(mFrom.left, mFrom.top, mFrom.left + mFrozenTaskWidth,
                     mFrom.top + mFrozenTaskHeight);
@@ -170,13 +188,8 @@ public class BoundsAnimationController {
             // we trigger any size changes, so it can swap surfaces
             // in to appropriate modes, or do as it wishes otherwise.
             if (!mSkipAnimationStart) {
-                mTarget.onAnimationStart(mMoveToFullScreen);
-            }
-
-            // If we are animating to a new fullscreen state (either to/from fullscreen), then
-            // notify the target of the change with the new frozen task bounds
-            if (mAnimatingToNewFullscreenState && mMoveToFullScreen) {
-                mTarget.updatePictureInPictureMode(null);
+                mTarget.onAnimationStart(mSchedulePipModeChangedState ==
+                        SCHEDULE_PIP_MODE_CHANGED_ON_START);
             }
 
             // Immediately update the task bounds if they have to become larger, but preserve
@@ -206,20 +219,26 @@ public class BoundsAnimationController {
                 // any further animation.
                 if (DEBUG) Slog.d(TAG, "animateUpdate: cancelled");
 
+                // If we have already scheduled a PiP mode changed at the start of the animation,
+                // then we need to clean up and schedule one at the end, since we have canceled the
+                // animation to the final state.
+                if (mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+                    mSchedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
+                }
+
                 // Since we are cancelling immediately without a replacement animation, send the
                 // animation end to maintain callback parity, but also skip any further resizes
-                prepareCancel(false /* skipAnimationEnd */, true /* skipFinalResize */);
-                cancel();
+                cancelAndCallAnimationEnd();
             }
         }
 
         @Override
         public void onAnimationEnd(Animator animation) {
             if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
-                    + " mMoveToFullScreen=" + mMoveToFullScreen
                     + " mSkipFinalResize=" + mSkipFinalResize
                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition
-                    + " mAppTransitionIsRunning=" + mAppTransition.isRunning());
+                    + " mAppTransitionIsRunning=" + mAppTransition.isRunning()
+                    + " callers=" + Debug.getCallers(2));
 
             // There could be another animation running. For example in the
             // move to fullscreen case, recents will also be closing while the
@@ -231,58 +250,57 @@ public class BoundsAnimationController {
                 return;
             }
 
-            if (!mSkipFinalResize) {
-                // If not cancelled, resize the pinned stack to the final size. All calls to
-                // setPinnedStackSize() must be done between onAnimationStart() and onAnimationEnd()
-                mTarget.setPinnedStackSize(mTo, null);
+            if (!mSkipAnimationEnd) {
+                // If this animation has already scheduled the picture-in-picture mode on start, and
+                // we are not skipping the final resize due to being canceled, then move the PiP to
+                // fullscreen once the animation ends
+                if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
+                        + " moveToFullscreen=" + mMoveToFullscreen);
+                mTarget.onAnimationEnd(mSchedulePipModeChangedState ==
+                        SCHEDULE_PIP_MODE_CHANGED_ON_END, !mSkipFinalResize ? mTo : null,
+                                mMoveToFullscreen);
             }
 
-            finishAnimation();
-
-            if (mMoveToFullScreen && !mSkipFinalResize) {
-                mTarget.moveToFullscreen();
-            }
+            // Clean up this animation
+            removeListener(this);
+            removeUpdateListener(this);
+            mRunningAnimations.remove(mTarget);
         }
 
         @Override
         public void onAnimationCancel(Animator animation) {
-            finishAnimation();
+            // Always skip the final resize when the animation is canceled
+            mSkipFinalResize = true;
+            mMoveToFullscreen = false;
         }
 
-        public void prepareCancel(boolean skipAnimationEnd, boolean skipFinalResize) {
-            if (DEBUG) Slog.d(TAG, "prepareCancel: skipAnimationEnd=" + skipAnimationEnd
-                    + " skipFinalResize=" + skipFinalResize);
-            mSkipAnimationEnd = skipAnimationEnd;
-            mSkipFinalResize = skipFinalResize;
+        private void cancelAndCallAnimationEnd() {
+            if (DEBUG) Slog.d(TAG, "cancelAndCallAnimationEnd: mTarget=" + mTarget);
+            mSkipAnimationEnd = false;
+            super.cancel();
         }
 
         @Override
         public void cancel() {
             if (DEBUG) Slog.d(TAG, "cancel: mTarget=" + mTarget);
+            mSkipAnimationEnd = true;
             super.cancel();
         }
 
-        /** Returns true if the animation target is the same as the input bounds. */
+        /**
+         * @return true if the animation target is the same as the input bounds.
+         */
         boolean isAnimatingTo(Rect bounds) {
             return mTo.equals(bounds);
         }
 
-        private boolean animatingToLargerSize() {
-            if (mFrom.width() * mFrom.height() > mTo.width() * mTo.height()) {
-                return false;
-            }
-            return true;
-        }
-
-        private void finishAnimation() {
-            if (DEBUG) Slog.d(TAG, "finishAnimation: mTarget=" + mTarget
-                    + " callers" + Debug.getCallers(2));
-            if (!mSkipAnimationEnd) {
-                mTarget.onAnimationEnd();
-            }
-            removeListener(this);
-            removeUpdateListener(this);
-            mRunningAnimations.remove(mTarget);
+        /**
+         * @return true if we are animating to a larger surface size
+         */
+        @VisibleForTesting
+        boolean animatingToLargerSize() {
+            // TODO: Fix this check for aspect ratio changes
+            return (mFrom.width() * mFrom.height() <= mTo.width() * mTo.height());
         }
 
         @Override
@@ -291,63 +309,23 @@ public class BoundsAnimationController {
         }
     }
 
-    public interface AnimateBoundsUser {
-        /**
-         * Sets the size of the target (without any intermediate steps, like scheduling animation)
-         * but freezes the bounds of any tasks in the target at taskBounds,
-         * to allow for more flexibility during resizing. Only works for the pinned stack at the
-         * moment.
-         *
-         * @return Whether the target should continue to be animated and this call was successful.
-         * If false, the animation will be cancelled because the user has determined that the
-         * animation is now invalid and not required. In such a case, the cancel will trigger the
-         * animation end callback as well, but will not send any further size changes.
-         */
-        boolean setPinnedStackSize(Rect bounds, Rect taskBounds);
-
-        /**
-         * Callback for the target to inform it that the animation has started, so it can do some
-         * necessary preparation.
-         */
-        void onAnimationStart(boolean toFullscreen);
-
-        /**
-         * Callback for the target to inform it that the animation is going to a new fullscreen
-         * state and should update the picture-in-picture mode accordingly.
-         *
-         * @param targetStackBounds the target stack bounds we are animating to, can be null if
-         *                          we are animating to fullscreen
-         */
-        void updatePictureInPictureMode(Rect targetStackBounds);
-
-        /**
-         * Callback for the target to inform it that the animation has ended, so it can do some
-         * necessary cleanup.
-         */
-        void onAnimationEnd();
-
-        /**
-         * Callback for the target to inform it to reparent to the fullscreen stack.
-         */
-        void moveToFullscreen();
-    }
-
-    public void animateBounds(final AnimateBoundsUser target, Rect from, Rect to,
-            int animationDuration, boolean moveToFullscreen) {
-        animateBoundsImpl(target, from, to, animationDuration, moveToFullscreen);
+    public void animateBounds(final BoundsAnimationTarget target, Rect from, Rect to,
+            int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
+            boolean moveToFullscreen) {
+        animateBoundsImpl(target, from, to, animationDuration, schedulePipModeChangedState,
+                moveToFullscreen);
     }
 
     @VisibleForTesting
-    BoundsAnimator animateBoundsImpl(final AnimateBoundsUser target, Rect from, Rect to,
-            int animationDuration, boolean moveToFullscreen) {
+    BoundsAnimator animateBoundsImpl(final BoundsAnimationTarget target, Rect from, Rect to,
+            int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
+            boolean moveToFullscreen) {
         final BoundsAnimator existing = mRunningAnimations.get(target);
         final boolean replacing = existing != null;
-        final boolean animatingToNewFullscreenState = (existing == null) ||
-                (existing.mMoveToFullScreen != moveToFullscreen);
 
         if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to
-                + " moveToFullscreen=" + moveToFullscreen + " replacing=" + replacing
-                + " animatingToNewFullscreenState=" + animatingToNewFullscreenState);
+                + " schedulePipModeChangedState=" + schedulePipModeChangedState
+                + " replacing=" + replacing);
 
         if (replacing) {
             if (existing.isAnimatingTo(to)) {
@@ -355,15 +333,36 @@ public class BoundsAnimationController {
                 // one we are trying to start.
                 if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing
                         + " ignoring...");
+
                 return existing;
             }
-            // Since we are replacing, we skip both animation start and end callbacks, and don't
-            // animate to the final bounds when cancelling
-            existing.prepareCancel(true /* skipAnimationEnd */, true /* skipFinalResize */);
+
+            // Update the PiP callback states if we are replacing the animation
+            if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+                if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+                    if (DEBUG) Slog.d(TAG, "animateBounds: still animating to fullscreen, keep"
+                            + " existing deferred state");
+                } else {
+                    if (DEBUG) Slog.d(TAG, "animateBounds: fullscreen animation canceled, callback"
+                            + " on start already processed, schedule deferred update on end");
+                    schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
+                }
+            } else if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END) {
+                if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+                    if (DEBUG) Slog.d(TAG, "animateBounds: non-fullscreen animation canceled,"
+                            + " callback on start will be processed");
+                } else {
+                    if (DEBUG) Slog.d(TAG, "animateBounds: still animating from fullscreen, keep"
+                            + " existing deferred state");
+                    schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
+                }
+            }
+
+            // Since we are replacing, we skip both animation start and end callbacks
             existing.cancel();
         }
-        final BoundsAnimator animator = new BoundsAnimator(target, from, to, moveToFullscreen,
-                replacing, animatingToNewFullscreenState);
+        final BoundsAnimator animator = new BoundsAnimator(target, from, to,
+                schedulePipModeChangedState, moveToFullscreen, replacing);
         mRunningAnimations.put(target, animator);
         animator.setFloatValues(0f, 1f);
         animator.setDuration((animationDuration != -1 ? animationDuration
diff --git a/services/core/java/com/android/server/wm/BoundsAnimationTarget.java b/services/core/java/com/android/server/wm/BoundsAnimationTarget.java
new file mode 100644 (file)
index 0000000..8b1bf7b
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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 android.graphics.Rect;
+
+/**
+ * The target for a BoundsAnimation.
+ * @see BoundsAnimationController
+ */
+interface BoundsAnimationTarget {
+
+    /**
+     * Callback for the target to inform it that the animation has started, so it can do some
+     * necessary preparation.
+     *
+     * @param schedulePipModeChangedCallback whether or not to schedule the PiP mode changed
+     * callbacks
+     */
+    void onAnimationStart(boolean schedulePipModeChangedCallback);
+
+    /**
+     * Sets the size of the target (without any intermediate steps, like scheduling animation)
+     * but freezes the bounds of any tasks in the target at taskBounds, to allow for more
+     * flexibility during resizing. Only works for the pinned stack at the moment.  This will
+     * only be called between onAnimationStart() and onAnimationEnd().
+     *
+     * @return Whether the target should continue to be animated and this call was successful.
+     * If false, the animation will be cancelled because the user has determined that the
+     * animation is now invalid and not required. In such a case, the cancel will trigger the
+     * animation end callback as well, but will not send any further size changes.
+     */
+    boolean setPinnedStackSize(Rect stackBounds, Rect taskBounds);
+
+    /**
+     * Callback for the target to inform it that the animation has ended, so it can do some
+     * necessary cleanup.
+     *
+     * @param schedulePipModeChangedCallback whether or not to schedule the PiP mode changed
+     * callbacks
+     * @param finalStackSize the final stack bounds to set on the target (can be to indicate that
+     * the animation was cancelled and the target does not need to update to the final stack bounds)
+     * @param moveToFullscreen whether or the target should reparent itself to the fullscreen stack
+     * when the animation completes
+     */
+    void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackSize,
+            boolean moveToFullscreen);
+}
index 135832e..5b9c16c 100644 (file)
 package com.android.server.wm;
 
 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
+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.app.RemoteAction;
 import android.graphics.Rect;
@@ -40,36 +44,53 @@ public class PinnedStackWindowController extends StackWindowController {
     /**
      * Animates the pinned stack.
      */
-    public void animateResizePinnedStack(Rect sourceBounds, Rect destBounds,
-            int animationDuration) {
+    public void animateResizePinnedStack(Rect toBounds, Rect sourceHintBounds,
+            int animationDuration, boolean schedulePipModeChangedOnAnimationEnd) {
         synchronized (mWindowMap) {
             if (mContainer == null) {
                 throw new IllegalArgumentException("Pinned stack container not found :(");
             }
 
-            // Get non-null fullscreen bounds if the bounds are null
-            final boolean moveToFullscreen = destBounds == null;
-            destBounds = getPinnedStackAnimationBounds(destBounds);
-
-            // If the bounds are truly null, then there was no fullscreen stack at this time, so
-            // animate this to the full display bounds
-            final Rect toBounds;
-            if (destBounds == null) {
-                toBounds = new Rect();
-                mContainer.getDisplayContent().getLogicalDisplayRect(toBounds);
-            } else {
-                toBounds = destBounds;
+            // Get the from-bounds
+            final Rect fromBounds = new Rect();
+            mContainer.getBounds(fromBounds);
+
+            // Get non-null fullscreen to-bounds for animating if the bounds are null
+            @SchedulePipModeChangedState int schedulePipModeChangedState =
+                NO_PIP_MODE_CHANGED_CALLBACKS;
+            final boolean toFullscreen = toBounds == null;
+            if (toFullscreen) {
+                if (schedulePipModeChangedOnAnimationEnd) {
+                    throw new IllegalArgumentException("Should not defer scheduling PiP mode"
+                            + " change on animation to fullscreen.");
+                }
+                schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_START;
+
+                mService.getStackBounds(FULLSCREEN_WORKSPACE_STACK_ID, mTmpBoundsRect);
+                if (!mTmpBoundsRect.isEmpty()) {
+                    // If there is a fullscreen bounds, use that
+                    toBounds = new Rect(mTmpBoundsRect);
+                } else {
+                    // Otherwise, use the display bounds
+                    toBounds = new Rect();
+                    mContainer.getDisplayContent().getLogicalDisplayRect(toBounds);
+                }
+            } else if (schedulePipModeChangedOnAnimationEnd) {
+                schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
             }
 
-            final Rect originalBounds = new Rect();
-            mContainer.getBounds(originalBounds);
-            mContainer.setAnimationFinalBounds(sourceBounds, toBounds);
+            mContainer.setAnimationFinalBounds(sourceHintBounds, toBounds, toFullscreen);
+
+            final Rect finalToBounds = toBounds;
+            final @SchedulePipModeChangedState int finalSchedulePipModeChangedState =
+                schedulePipModeChangedState;
             UiThread.getHandler().post(() -> {
                 if (mContainer == null) {
                     return;
                 }
-                mService.mBoundsAnimationController.animateBounds(mContainer, originalBounds,
-                        toBounds, animationDuration, moveToFullscreen);
+                mService.mBoundsAnimationController.animateBounds(mContainer, fromBounds,
+                        finalToBounds, animationDuration, finalSchedulePipModeChangedState,
+                        toFullscreen);
             });
         }
     }
@@ -93,7 +114,8 @@ public class PinnedStackWindowController extends StackWindowController {
 
             if (Float.compare(aspectRatio, pinnedStackController.getAspectRatio()) != 0) {
                 if (!toBounds.equals(targetBounds)) {
-                    animateResizePinnedStack(null /* sourceBounds */, toBounds, -1 /* duration */);
+                    animateResizePinnedStack(toBounds, null /* sourceHintBounds */,
+                            -1 /* duration */, false /* schedulePipModeChangedOnAnimationEnd */);
                 }
                 pinnedStackController.setAspectRatio(
                         pinnedStackController.isValidPictureInPictureAspectRatio(aspectRatio)
@@ -116,26 +138,31 @@ public class PinnedStackWindowController extends StackWindowController {
     }
 
     /**
-     * @return whether the bounds are currently animating to fullscreen.
+     * @return whether the multi-window mode change should be deferred as a part of a transition
+     * from fullscreen to non-fullscreen bounds.
      */
-    public boolean isAnimatingBoundsToFullscreen() {
-        return mContainer.isAnimatingBoundsToFullscreen();
+    public boolean deferScheduleMultiWindowModeChanged() {
+        synchronized(mWindowMap) {
+            return mContainer.deferScheduleMultiWindowModeChanged();
+        }
     }
 
-    public boolean pinnedStackResizeAllowed() {
-        return mContainer.pinnedStackResizeAllowed();
+    /**
+     * @return whether the bounds are currently animating to fullscreen.
+     */
+    public boolean isAnimatingBoundsToFullscreen() {
+        synchronized (mWindowMap) {
+            return mContainer.isAnimatingBoundsToFullscreen();
+        }
     }
 
     /**
-     * Checks the {@param bounds} and retirms non-null fullscreen bounds for the pinned stack
-     * animation if necessary.
+     * @return whether the stack can be resized from the bounds animation.
      */
-    private Rect getPinnedStackAnimationBounds(Rect bounds) {
-        mService.getStackBounds(FULLSCREEN_WORKSPACE_STACK_ID, mTmpBoundsRect);
-        if (bounds == null && !mTmpBoundsRect.isEmpty()) {
-            bounds = new Rect(mTmpBoundsRect);
+    public boolean pinnedStackResizeDisallowed() {
+        synchronized (mWindowMap) {
+            return mContainer.pinnedStackResizeDisallowed();
         }
-        return bounds;
     }
 
     /**
index d7c41d3..d141f7c 100644 (file)
@@ -56,7 +56,7 @@ import com.android.server.EventLogTags;
 import java.io.PrintWriter;
 
 public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLayerUser,
-        BoundsAnimationController.AnimateBoundsUser {
+        BoundsAnimationTarget {
     /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to
      * restrict IME adjustment so that a min portion of top stack remains visible.*/
     private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f;
@@ -132,7 +132,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
     private boolean mBoundsAnimatingToFullscreen = false;
     private boolean mCancelCurrentBoundsAnimation = false;
     private Rect mBoundsAnimationTarget = new Rect();
-    private Rect mBoundsAnimationSourceBounds = new Rect();
+    private Rect mBoundsAnimationSourceHintBounds = new Rect();
 
     // Temporary storage for the new bounds that should be used after the configuration change.
     // Will be cleared once the client retrieves the new bounds via getBoundsForNewConfiguration().
@@ -322,18 +322,19 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
      * Sets the bounds animation target bounds ahead of an animation.  This can't currently be done
      * in onAnimationStart() since that is started on the UiThread.
      */
-    void setAnimationFinalBounds(Rect sourceBounds, Rect destBounds) {
+    void setAnimationFinalBounds(Rect sourceHintBounds, Rect destBounds, boolean toFullscreen) {
         mBoundsAnimatingRequested = true;
-        if (sourceBounds != null) {
-            mBoundsAnimationSourceBounds.set(sourceBounds);
-        } else {
-            mBoundsAnimationSourceBounds.setEmpty();
-        }
+        mBoundsAnimatingToFullscreen = toFullscreen;
         if (destBounds != null) {
             mBoundsAnimationTarget.set(destBounds);
         } else {
             mBoundsAnimationTarget.setEmpty();
         }
+        if (sourceHintBounds != null) {
+            mBoundsAnimationSourceHintBounds.set(sourceHintBounds);
+        } else {
+            mBoundsAnimationSourceHintBounds.setEmpty();
+        }
     }
 
     /**
@@ -346,8 +347,8 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
     /**
      * @return the final source bounds for the bounds animation.
      */
-    void getFinalAnimationSourceBounds(Rect outBounds) {
-        outBounds.set(mBoundsAnimationSourceBounds);
+    void getFinalAnimationSourceHintBounds(Rect outBounds) {
+        outBounds.set(mBoundsAnimationSourceHintBounds);
     }
 
     /**
@@ -413,7 +414,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
                 // orientation, clear the animation target bounds since they are obsolete, and
                 // cancel any currently running animations
                 mBoundsAnimationTarget.setEmpty();
-                mBoundsAnimationSourceBounds.setEmpty();
+                mBoundsAnimationSourceHintBounds.setEmpty();
                 mCancelCurrentBoundsAnimation = true;
                 return true;
             }
@@ -1458,13 +1459,16 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
         }
     }
 
-    public boolean setPinnedStackSize(Rect bounds, Rect tempTaskBounds) {
-        if (mCancelCurrentBoundsAnimation) {
-            return false;
+    public boolean setPinnedStackSize(Rect stackBounds, Rect tempTaskBounds) {
+        // Hold the lock since this is called from the BoundsAnimator running on the UiThread
+        synchronized (mService.mWindowMap) {
+            if (mCancelCurrentBoundsAnimation) {
+                return false;
+            }
         }
 
         try {
-            mService.mActivityManager.resizePinnedStack(bounds, tempTaskBounds);
+            mService.mActivityManager.resizePinnedStack(stackBounds, tempTaskBounds);
         } catch (RemoteException e) {
             // I don't believe you.
         }
@@ -1472,11 +1476,11 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
     }
 
     @Override  // AnimatesBounds
-    public void onAnimationStart(boolean toFullscreen) {
+    public void onAnimationStart(boolean schedulePipModeChangedCallback) {
+        // Hold the lock since this is called from the BoundsAnimator running on the UiThread
         synchronized (mService.mWindowMap) {
             mBoundsAnimatingRequested = false;
             mBoundsAnimating = true;
-            mBoundsAnimatingToFullscreen = toFullscreen;
             mCancelCurrentBoundsAnimation = false;
         }
 
@@ -1486,41 +1490,63 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
             } catch (RemoteException e) {
                 // I don't believe you...
             }
-        }
-    }
 
-    @Override  // AnimatesBounds
-    public void updatePictureInPictureMode(Rect targetStackBounds) {
-        final PinnedStackWindowController controller =
-                (PinnedStackWindowController) getController();
-        if (controller != null) {
-            controller.updatePictureInPictureModeForPinnedStackAnimation(targetStackBounds);
+            final PinnedStackWindowController controller =
+                    (PinnedStackWindowController) getController();
+            if (schedulePipModeChangedCallback && controller != null) {
+                // We need to schedule the PiP mode change after the animation down, so use the
+                // final bounds
+                controller.updatePictureInPictureModeForPinnedStackAnimation(null);
+            }
         }
     }
 
     @Override  // AnimatesBounds
-    public void onAnimationEnd() {
+    public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackSize,
+            boolean moveToFullscreen) {
+        // Hold the lock since this is called from the BoundsAnimator running on the UiThread
         synchronized (mService.mWindowMap) {
             mBoundsAnimating = false;
             mService.requestTraversal();
         }
 
         if (mStackId == PINNED_STACK_ID) {
+            final PinnedStackWindowController controller =
+                    (PinnedStackWindowController) getController();
+            if (schedulePipModeChangedCallback && controller != null) {
+                // We need to schedule the PiP mode change after the animation down, so use the
+                // final bounds
+                controller.updatePictureInPictureModeForPinnedStackAnimation(
+                        mBoundsAnimationTarget);
+            }
+
+            // Update to the final bounds if requested. This is done here instead of in the bounds
+            // animator to allow us to coordinate this after we notify the PiP mode changed
+            if (finalStackSize != null) {
+                setPinnedStackSize(finalStackSize, null);
+            }
+
             try {
                 mService.mActivityManager.notifyPinnedStackAnimationEnded();
+                if (moveToFullscreen) {
+                    mService.mActivityManager.moveTasksToFullscreenStack(mStackId,
+                            true /* onTop */);
+                }
             } catch (RemoteException e) {
                 // I don't believe you...
             }
         }
     }
 
-    @Override
-    public void moveToFullscreen() {
-        try {
-            mService.mActivityManager.moveTasksToFullscreenStack(mStackId, true);
-        } catch (RemoteException e) {
-            e.printStackTrace();
+    /**
+     * @return True if we are currently animating the pinned stack from fullscreen to non-fullscreen
+     *         bounds and we have a deferred PiP mode changed callback set with the animation.
+     */
+    public boolean deferScheduleMultiWindowModeChanged() {
+        if (mStackId == PINNED_STACK_ID) {
+            return (mBoundsAnimatingRequested || mBoundsAnimating);
         }
+        return false;
     }
 
     public boolean hasMovementAnimations() {
@@ -1539,7 +1565,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
         return mBoundsAnimating && mBoundsAnimatingToFullscreen;
     }
 
-    public boolean pinnedStackResizeAllowed() {
+    public boolean pinnedStackResizeDisallowed() {
         if (mBoundsAnimating && mCancelCurrentBoundsAnimation) {
             return true;
         }
index 0b96f3f..9555c8d 100644 (file)
@@ -4365,9 +4365,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
         // When we change the Surface size, in scenarios which may require changing
         // the surface position in sync with the resize, we use a preserved surface
         // so we can freeze it while waiting for the client to report draw on the newly
-        // sized surface.
+        // sized surface.  Don't preserve surfaces if the insets change while animating the pinned
+        // stack since it can lead to issues if a new surface is created while calculating the
+        // scale for the animation using the source hint rect
+        // (see WindowStateAnimator#setSurfaceBoundariesLocked()).
         if (isDragResizeChanged() || isResizedWhileNotDragResizing()
-                || surfaceInsetsChanging()) {
+                || (surfaceInsetsChanging() && !inPinnedWorkspace())) {
             mLastSurfaceInsets.set(mAttrs.surfaceInsets);
 
             setDragResizing();
index fa35336..a2889b1 100644 (file)
@@ -1360,7 +1360,7 @@ class WindowStateAnimator {
             int posX = mTmpSize.left;
             int posY = mTmpSize.top;
             task.mStack.getDimBounds(mTmpStackBounds);
-            task.mStack.getFinalAnimationSourceBounds(mTmpSourceBounds);
+            task.mStack.getFinalAnimationSourceHintBounds(mTmpSourceBounds);
             if (!mTmpSourceBounds.isEmpty()) {
                 // Get the final target stack bounds, if we are not animating, this is just the
                 // current stack bounds
index 85dc712..7f150a2 100644 (file)
 
 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;
@@ -47,6 +52,12 @@ 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
  */
@@ -109,47 +120,46 @@ public class BoundsAnimationControllerTests extends WindowTestsBase {
     /**
      * A test animate bounds user to track callbacks from the bounds animation.
      */
-    private class TestAnimateBoundsUser implements BoundsAnimationController.AnimateBoundsUser {
+    private class TestBoundsAnimationTarget implements BoundsAnimationTarget {
 
+        boolean mAwaitingAnimationStart;
         boolean mMovedToFullscreen;
         boolean mAnimationStarted;
-        boolean mAnimationStartedToFullscreen;
+        boolean mSchedulePipModeChangedOnStart;
         boolean mAnimationEnded;
-        boolean mUpdatedPictureInPictureModeWithBounds;
+        Rect mAnimationEndFinalStackBounds;
+        boolean mSchedulePipModeChangedOnEnd;
         boolean mBoundsUpdated;
+        boolean mCancelRequested;
         Rect mStackBounds;
         Rect mTaskBounds;
 
-        boolean mRequestCancelAnimation = false;
-
-        void reinitialize(Rect stackBounds, Rect taskBounds) {
+        void initialize(Rect from) {
+            mAwaitingAnimationStart = true;
             mMovedToFullscreen = false;
             mAnimationStarted = false;
-            mAnimationStartedToFullscreen = false;
             mAnimationEnded = false;
-            mUpdatedPictureInPictureModeWithBounds = false;
-            mStackBounds = stackBounds;
-            mTaskBounds = taskBounds;
+            mAnimationEndFinalStackBounds = null;
+            mSchedulePipModeChangedOnStart = false;
+            mSchedulePipModeChangedOnEnd = false;
+            mStackBounds = from;
+            mTaskBounds = null;
             mBoundsUpdated = false;
-            mRequestCancelAnimation = false;
         }
 
         @Override
-        public void onAnimationStart(boolean toFullscreen) {
+        public void onAnimationStart(boolean schedulePipModeChangedCallback) {
+            mAwaitingAnimationStart = false;
             mAnimationStarted = true;
-            mAnimationStartedToFullscreen = toFullscreen;
-        }
-
-        @Override
-        public void updatePictureInPictureMode(Rect targetStackBounds) {
-            mUpdatedPictureInPictureModeWithBounds = 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 (mRequestCancelAnimation) {
+            if (mCancelRequested) {
+                mCancelRequested = false;
                 return false;
             } else {
                 mBoundsUpdated = true;
@@ -160,32 +170,206 @@ public class BoundsAnimationControllerTests extends WindowTestsBase {
         }
 
         @Override
-        public void onAnimationEnd() {
+        public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackBounds,
+                boolean moveToFullscreen) {
             mAnimationEnded = true;
+            mAnimationEndFinalStackBounds = finalStackBounds;
+            mSchedulePipModeChangedOnEnd = schedulePipModeChangedCallback;
+            mMovedToFullscreen = moveToFullscreen;
+            mTaskBounds = null;
         }
+    }
 
-        @Override
-        public void moveToFullscreen() {
-            mMovedToFullscreen = true;
+    /**
+     * 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");
+            }
+
+            mTarget.initialize(from);
+
+            // Started, not running
+            assertTrue(mTarget.mAwaitingAnimationStart);
+            assertTrue(!mTarget.mAnimationStarted);
+
+            startImpl(from, to);
+
+            // 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, 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(80, 80, 95, 95);
-    private static final Rect BOUNDS_ALT_FLOATING = new Rect(60, 60, 95, 95);
-
-    // Some dummy duration
-    private static final int DURATION = 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 mAppTransition;
-    private MockValueAnimator mAnimator;
-    private TestAnimateBoundsUser mTarget;
+    private MockAppTransition mMockAppTransition;
+    private MockValueAnimator mMockAnimator;
+    private TestBoundsAnimationTarget mTarget;
     private BoundsAnimationController mController;
+    private BoundsAnimationDriver mDriver;
 
     // Temp
     private Rect mTmpRect = new Rect();
@@ -196,153 +380,184 @@ public class BoundsAnimationControllerTests extends WindowTestsBase {
 
         final Context context = InstrumentationRegistry.getTargetContext();
         final Handler handler = new Handler(Looper.getMainLooper());
-        mAppTransition = new MockAppTransition(context);
-        mAnimator = new MockValueAnimator();
-        mTarget = new TestAnimateBoundsUser();
-        mController = new BoundsAnimationController(context, mAppTransition, handler);
+        mMockAppTransition = new MockAppTransition(context);
+        mMockAnimator = new MockValueAnimator();
+        mTarget = new TestBoundsAnimationTarget();
+        mController = new BoundsAnimationController(context, mMockAppTransition, handler);
+        mDriver = new BoundsAnimationDriver(mController, mTarget);
     }
 
+    /** BASE TRANSITIONS **/
+
     @UiThreadTest
     @Test
     public void testFullscreenToFloatingTransition() throws Exception {
-        // Create and start the animation
-        mTarget.reinitialize(BOUNDS_FULL, null);
-        final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL,
-                BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN);
-
-        // Assert that when we are started, and that we are not going to fullscreen
-        assertTrue(mTarget.mAnimationStarted);
-        assertFalse(mTarget.mAnimationStartedToFullscreen);
-        // Ensure we are not triggering a PiP mode change
-        assertFalse(mTarget.mUpdatedPictureInPictureModeWithBounds);
-        // Ensure that the task stack bounds are already frozen to the larger source stack bounds
-        assertEquals(BOUNDS_FULL, mTarget.mStackBounds);
-        assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds));
-
-        // Drive some animation updates, ensure that only the stack bounds change and the task
-        // bounds are frozen to the original stack bounds (adjusted for the offset)
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f));
-        assertNotEquals(BOUNDS_FULL, mTarget.mStackBounds);
-        assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds));
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(1f));
-        assertNotEquals(BOUNDS_FULL, mTarget.mStackBounds);
-        assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds));
-
-        // Finish the animation, ensure that it reaches the final bounds with the given state
-        boundsAnimator.end();
-        assertTrue(mTarget.mAnimationEnded);
-        assertEquals(BOUNDS_FLOATING, mTarget.mStackBounds);
-        assertNull(mTarget.mTaskBounds);
+        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 {
-        // Create and start the animation
-        mTarget.reinitialize(BOUNDS_FULL, null);
-        final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FLOATING,
-                BOUNDS_FULL, DURATION, MOVE_TO_FULLSCREEN);
-
-        // Assert that when we are started, and that we are going to fullscreen
-        assertTrue(mTarget.mAnimationStarted);
-        assertTrue(mTarget.mAnimationStartedToFullscreen);
-        // Ensure that we update the PiP mode change with the new fullscreen bounds
-        assertTrue(mTarget.mUpdatedPictureInPictureModeWithBounds);
-        // Ensure that the task stack bounds are already frozen to the larger target stack bounds
-        assertEquals(BOUNDS_FLOATING, mTarget.mStackBounds);
-        assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds));
-
-        // Drive some animation updates, ensure that only the stack bounds change and the task
-        // bounds are frozen to the original stack bounds (adjusted for the offset)
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f));
-        assertNotEquals(BOUNDS_FLOATING, mTarget.mStackBounds);
-        assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds));
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(1f));
-        assertNotEquals(BOUNDS_FLOATING, mTarget.mStackBounds);
-        assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds));
-
-        // Finish the animation, ensure that it reaches the final bounds with the given state
-        boundsAnimator.end();
-        assertTrue(mTarget.mAnimationEnded);
-        assertEquals(BOUNDS_FULL, mTarget.mStackBounds);
-        assertNull(mTarget.mTaskBounds);
+        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 testInterruptAnimationFromUser() throws Exception {
-        // Create and start the animation
-        mTarget.reinitialize(BOUNDS_FULL, null);
-        final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL,
-                BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN);
-
-        // Cancel the animation on the next update from the user
-        mTarget.mRequestCancelAnimation = true;
-        mTarget.mBoundsUpdated = false;
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f));
-        // Ensure that we got no more updates after returning false and the bounds are not updated
-        // to the end value
-        assertFalse(mTarget.mBoundsUpdated);
-        assertNotEquals(BOUNDS_FLOATING, mTarget.mStackBounds);
-        assertNotEquals(BOUNDS_FLOATING, mTarget.mTaskBounds);
-        // Ensure that we received the animation end call
-        assertTrue(mTarget.mAnimationEnded);
+    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 testCancelAnimationFromNewAnimationToExistingBounds() throws Exception {
-        // Create and start the animation
-        mTarget.reinitialize(BOUNDS_FULL, null);
-        final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL,
-                BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN);
-
-        // Drive some animation updates
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f));
-
-        // Cancel the animation as a restart to the same bounds
-        mTarget.reinitialize(null, null);
-        final BoundsAnimator altBoundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL,
-                BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN);
-        // Ensure the animator is the same
-        assertSame(boundsAnimator, altBoundsAnimator);
-        // Ensure we haven't restarted or finished the animation
-        assertFalse(mTarget.mAnimationStarted);
-        assertFalse(mTarget.mAnimationEnded);
-        // Ensure that we haven't tried to update the PiP mode
-        assertFalse(mTarget.mUpdatedPictureInPictureModeWithBounds);
+    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 testCancelAnimationFromNewAnimationToNewBounds() throws Exception {
-        // Create and start the animation
-        mTarget.reinitialize(BOUNDS_FULL, null);
-        final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL,
-                BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN);
-
-        // Drive some animation updates
-        boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f));
-
-        // Cancel the animation as a restart to new bounds
-        mTarget.reinitialize(null, null);
-        final BoundsAnimator altBoundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL,
-                BOUNDS_ALT_FLOATING, DURATION, !MOVE_TO_FULLSCREEN);
-        // Ensure the animator is not the same
-        assertNotSame(boundsAnimator, altBoundsAnimator);
-        // Ensure that we did not get an animation start/end callback
-        assertFalse(mTarget.mAnimationStarted);
-        assertFalse(mTarget.mAnimationEnded);
-        // Ensure that we haven't tried to update the PiP mode
-        assertFalse(mTarget.mUpdatedPictureInPictureModeWithBounds);
+    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 the bounds offset to zero/zero.
+     * @return whether the task and stack bounds would be the same if they were at the same offset.
      */
-    private Rect offsetToZero(Rect bounds) {
-        mTmpRect.set(bounds);
-        mTmpRect.offsetTo(0, 0);
-        return mTmpRect;
+    private boolean assertEqualSizeAtOffset(Rect stackBounds, Rect taskBounds) {
+        mTmpRect.set(taskBounds);
+        mTmpRect.offsetTo(stackBounds.left, stackBounds.top);
+        return stackBounds.equals(mTmpRect);
     }
 }