Merge changes I789945c1,I532b0928,I62c6df8b,Ib2bd81ad into oc-dev

* changes:
  Fixing missing movement bounds notification to SystemUI.
  Fixing animating bounds regression.
  Tightening up rotation behavior for PIP (2/3)
  Tightening up rotation behavior for PIP (1/3)
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
index 114a594..c565373 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java
@@ -86,6 +86,7 @@
 
             ComponentName topPipActivity = PipUtils.getTopPinnedActivity(mContext,
                     mActivityManager);
+            mMenuController.hideMenu();
             mNotificationController.onActivityUnpinned(topPipActivity);
 
             SystemServicesProxy.getInstance(mContext).setPipVisibility(topPipActivity != null);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e65914d..e5212b7 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -7864,9 +7864,7 @@
                     final float aspectRatio = r.pictureInPictureArgs.getAspectRatio();
                     final List<RemoteAction> actions = r.pictureInPictureArgs.getActions();
                     final Rect sourceBounds = r.pictureInPictureArgs.getSourceRectHint();
-                    final Rect destBounds = mWindowManager.getPictureInPictureBounds(DEFAULT_DISPLAY,
-                            aspectRatio);
-                    mStackSupervisor.moveActivityToPinnedStackLocked(r, sourceBounds, destBounds,
+                    mStackSupervisor.moveActivityToPinnedStackLocked(r, sourceBounds, aspectRatio,
                             true /* moveHomeStackToFront */, "enterPictureInPictureMode");
                     final PinnedActivityStack stack = mStackSupervisor.getStack(PINNED_STACK_ID);
                     stack.setPictureInPictureAspectRatio(aspectRatio);
@@ -7927,7 +7925,7 @@
                     // if it is not already expanding to fullscreen. Otherwise, the arguments will
                     // be used the next time the activity enters PiP
                     final PinnedActivityStack stack = r.getStack();
-                    if (!stack.isBoundsAnimatingToFullscreen()) {
+                    if (!stack.isAnimatingBoundsToFullscreen()) {
                         stack.setPictureInPictureAspectRatio(
                                 r.pictureInPictureArgs.getAspectRatio());
                         stack.setPictureInPictureActions(r.pictureInPictureArgs.getActions());
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 68e25c3..4d16e33 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -174,6 +174,7 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
 import com.android.server.am.ActivityStack.ActivityState;
+import com.android.server.wm.PinnedStackWindowController;
 import com.android.server.wm.WindowManagerService;
 
 import java.io.FileDescriptor;
@@ -2492,11 +2493,21 @@
     }
 
     void resizePinnedStackLocked(Rect pinnedBounds, Rect tempPinnedTaskBounds) {
-        final ActivityStack stack = getStack(PINNED_STACK_ID);
+        final PinnedActivityStack stack = getStack(PINNED_STACK_ID);
         if (stack == null) {
             Slog.w(TAG, "resizePinnedStackLocked: pinned stack not found");
             return;
         }
+
+        // It is possible for the bounds animation from the WM to call this but be delayed by
+        // another AM call that is holding the AMS lock. In such a case, the pinnedBounds may be
+        // 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()) {
+            return;
+        }
+
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "am.resizePinnedStack");
         mWindowManager.deferSurfaceLayout();
         try {
@@ -2857,12 +2868,12 @@
             return false;
         }
 
-        moveActivityToPinnedStackLocked(r, null /* sourceBounds */, destBounds,
+        moveActivityToPinnedStackLocked(r, null /* sourceBounds */, 0f /* aspectRatio */,
                 true /* moveHomeStackToFront */, "moveTopActivityToPinnedStack");
         return true;
     }
 
-    void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceBounds, Rect destBounds,
+    void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceBounds, float aspectRatio,
             boolean moveHomeStackToFront, String reason) {
 
         mWindowManager.deferSurfaceLayout();
@@ -2932,6 +2943,11 @@
         ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
         resumeFocusedStackTopActivityLocked();
 
+        // Calculate the default bounds (don't use existing stack bounds as we may have just created
+        // the stack
+        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 */);
diff --git a/services/core/java/com/android/server/am/PinnedActivityStack.java b/services/core/java/com/android/server/am/PinnedActivityStack.java
index 394e902..cd9c42c 100644
--- a/services/core/java/com/android/server/am/PinnedActivityStack.java
+++ b/services/core/java/com/android/server/am/PinnedActivityStack.java
@@ -21,7 +21,7 @@
 
 import com.android.server.am.ActivityStackSupervisor.ActivityContainer;
 import com.android.server.wm.PinnedStackWindowController;
-import com.android.server.wm.StackWindowController;
+import com.android.server.wm.PinnedStackWindowListener;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -29,7 +29,8 @@
 /**
  * State and management of the pinned stack of activities.
  */
-class PinnedActivityStack extends ActivityStack<PinnedStackWindowController> {
+class PinnedActivityStack extends ActivityStack<PinnedStackWindowController>
+        implements PinnedStackWindowListener {
 
     PinnedActivityStack(ActivityContainer activityContainer,
             RecentTasks recentTasks, boolean onTop) {
@@ -55,8 +56,8 @@
         getWindowContainerController().setPictureInPictureActions(actions);
     }
 
-    boolean isBoundsAnimatingToFullscreen() {
-        return getWindowContainerController().isBoundsAnimatingToFullscreen();
+    boolean isAnimatingBoundsToFullscreen() {
+        return getWindowContainerController().isAnimatingBoundsToFullscreen();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/BoundsAnimationController.java b/services/core/java/com/android/server/wm/BoundsAnimationController.java
index 2811145..f41eed5 100644
--- a/services/core/java/com/android/server/wm/BoundsAnimationController.java
+++ b/services/core/java/com/android/server/wm/BoundsAnimationController.java
@@ -33,6 +33,8 @@
 import android.view.animation.Interpolator;
 import android.view.WindowManagerInternal;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Enables animating bounds of objects.
  *
@@ -103,7 +105,8 @@
                 com.android.internal.R.interpolator.fast_out_slow_in);
     }
 
-    private final class BoundsAnimator extends ValueAnimator
+    @VisibleForTesting
+    final class BoundsAnimator extends ValueAnimator
             implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
         private final AnimateBoundsUser mTarget;
         private final Rect mFrom = new Rect();
@@ -113,10 +116,12 @@
         private final boolean mMoveToFullScreen;
         // True if this this animation was cancelled and will be replaced the another animation from
         // the same {@link #AnimateBoundsUser} target.
-        private boolean mSkipAnimationEnd;
+        private boolean mSkipFinalResize;
         // True if this animation replaced a previous animation of the same
         // {@link #AnimateBoundsUser} target.
         private final boolean mSkipAnimationStart;
+        // True if this animation was cancelled 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
@@ -200,7 +205,11 @@
                 // Whoops, the target doesn't feel like animating anymore. Let's immediately finish
                 // any further animation.
                 if (DEBUG) Slog.d(TAG, "animateUpdate: cancelled");
-                animation.cancel();
+
+                // 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();
             }
         }
 
@@ -208,7 +217,7 @@
         public void onAnimationEnd(Animator animation) {
             if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
                     + " mMoveToFullScreen=" + mMoveToFullScreen
-                    + " mSkipAnimationEnd=" + mSkipAnimationEnd
+                    + " mSkipFinalResize=" + mSkipFinalResize
                     + " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition
                     + " mAppTransitionIsRunning=" + mAppTransition.isRunning());
 
@@ -222,10 +231,15 @@
                 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);
+            }
+
             finishAnimation();
 
-            mTarget.setPinnedStackSize(mTo, null);
-            if (mMoveToFullScreen && !mSkipAnimationEnd) {
+            if (mMoveToFullScreen && !mSkipFinalResize) {
                 mTarget.moveToFullscreen();
             }
         }
@@ -235,10 +249,16 @@
             finishAnimation();
         }
 
+        public void prepareCancel(boolean skipAnimationEnd, boolean skipFinalResize) {
+            if (DEBUG) Slog.d(TAG, "prepareCancel: skipAnimationEnd=" + skipAnimationEnd
+                    + " skipFinalResize=" + skipFinalResize);
+            mSkipAnimationEnd = skipAnimationEnd;
+            mSkipFinalResize = skipFinalResize;
+        }
+
         @Override
         public void cancel() {
-            mSkipAnimationEnd = true;
-            if (DEBUG) Slog.d(TAG, "cancel: willReplace mTarget=" + mTarget);
+            if (DEBUG) Slog.d(TAG, "cancel: mTarget=" + mTarget);
             super.cancel();
         }
 
@@ -273,19 +293,15 @@
 
     public interface AnimateBoundsUser {
         /**
-         * Asks the target to directly (without any intermediate steps, like scheduling animation)
-         * resize its bounds.
-         *
-         * @return Whether the target still wants to be animated and successfully finished the
-         * operation. If it returns false, the animation will immediately be cancelled. The target
-         * should return false when something abnormal happened, e.g. it was completely removed
-         * from the hierarchy and is not valid anymore.
-         */
-        boolean setSize(Rect bounds);
-        /**
-         * Behaves as setSize, but freezes the bounds of any tasks in the target at taskBounds,
+         * 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);
 
@@ -310,11 +326,20 @@
          */
         void onAnimationEnd();
 
+        /**
+         * Callback for the target to inform it to reparent to the fullscreen stack.
+         */
         void moveToFullscreen();
     }
 
-    void animateBounds(final AnimateBoundsUser target, Rect from, Rect to, int animationDuration,
-            boolean moveToFullscreen) {
+    public void animateBounds(final AnimateBoundsUser target, Rect from, Rect to,
+            int animationDuration, boolean moveToFullscreen) {
+        animateBoundsImpl(target, from, to, animationDuration, moveToFullscreen);
+    }
+
+    @VisibleForTesting
+    BoundsAnimator animateBoundsImpl(final AnimateBoundsUser target, Rect from, Rect to,
+            int animationDuration, boolean moveToFullscreen) {
         final BoundsAnimator existing = mRunningAnimations.get(target);
         final boolean replacing = existing != null;
         final boolean animatingToNewFullscreenState = (existing == null) ||
@@ -326,12 +351,15 @@
 
         if (replacing) {
             if (existing.isAnimatingTo(to)) {
-                // Just les the current animation complete if it has the same destination as the
+                // Just let the current animation complete if it has the same destination as the
                 // one we are trying to start.
                 if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing
                         + " ignoring...");
-                return;
+                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 */);
             existing.cancel();
         }
         final BoundsAnimator animator = new BoundsAnimator(target, from, to, moveToFullscreen,
@@ -342,5 +370,6 @@
                 : DEFAULT_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
         animator.setInterpolator(mFastOutSlowInInterpolator);
         animator.start();
+        return animator;
     }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2f64cd4..058fdae 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1420,6 +1420,13 @@
                 changedStackList.add(stack.mStackId);
             }
         }
+
+        // If there was no pinned stack, we still need to notify the controller of the display info
+        // update as a result of the config change.  We do this here to consolidate the flow between
+        // changes when there is and is not a stack.
+        if (getStackById(PINNED_STACK_ID) == null) {
+            mPinnedStackControllerLocked.onDisplayInfoChanged();
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
index 3ce61f0..1684878 100644
--- a/services/core/java/com/android/server/wm/PinnedStackController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -147,7 +147,6 @@
 
     void onConfigurationChanged() {
         reloadResources();
-        notifyMovementBoundsChanged(false /* fromImeAdjustment */);
     }
 
     /**
@@ -241,13 +240,31 @@
     }
 
     /**
+     * In the case where the display rotation is changed but there is no stack, we can't depend on
+     * onTaskStackBoundsChanged() to be called.  But we still should update our known display info
+     * with the new state so that we can update SystemUI.
+     */
+    synchronized void onDisplayInfoChanged() {
+        mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
+        notifyMovementBoundsChanged(false /* fromImeAdjustment */);
+    }
+
+    /**
      * Updates the display info, calculating and returning the new stack and movement bounds in the
      * new orientation of the device if necessary.
      */
-    void onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds) {
+    boolean onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds) {
         final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
         if (mDisplayInfo.equals(displayInfo)) {
-            return;
+            // We are already in the right orientation, ignore
+            outBounds.setEmpty();
+            return false;
+        } else if (targetBounds.isEmpty()) {
+            // The stack is null, we are just initializing the stack, so just store the display info
+            // and ignore
+            mDisplayInfo.copyFrom(displayInfo);
+            outBounds.setEmpty();
+            return false;
         }
 
         mTmpRect.set(targetBounds);
@@ -272,6 +289,7 @@
         notifyMovementBoundsChanged(false /* fromImeAdjustment */);
 
         outBounds.set(postChangeStackBounds);
+        return true;
     }
 
     /**
@@ -371,7 +389,7 @@
                 final Rect animatingBounds = mTmpAnimatingBoundsRect;
                 final TaskStack pinnedStack = mDisplayContent.getStackById(PINNED_STACK_ID);
                 if (pinnedStack != null) {
-                    pinnedStack.getAnimatingBounds(animatingBounds);
+                    pinnedStack.getAnimationOrCurrentBounds(animatingBounds);
                 } else {
                     animatingBounds.set(normalBounds);
                 }
diff --git a/services/core/java/com/android/server/wm/PinnedStackWindowController.java b/services/core/java/com/android/server/wm/PinnedStackWindowController.java
index 0145454..135832e 100644
--- a/services/core/java/com/android/server/wm/PinnedStackWindowController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackWindowController.java
@@ -32,8 +32,8 @@
 
     private Rect mTmpBoundsRect = new Rect();
 
-    public PinnedStackWindowController(int stackId, StackWindowListener listener, int displayId,
-            boolean onTop, Rect outBounds) {
+    public PinnedStackWindowController(int stackId, PinnedStackWindowListener listener,
+            int displayId, boolean onTop, Rect outBounds) {
         super(stackId, listener, displayId, onTop, outBounds, WindowManagerService.getInstance());
     }
 
@@ -63,7 +63,7 @@
 
             final Rect originalBounds = new Rect();
             mContainer.getBounds(originalBounds);
-            mContainer.setAnimatingBounds(sourceBounds, toBounds);
+            mContainer.setAnimationFinalBounds(sourceBounds, toBounds);
             UiThread.getHandler().post(() -> {
                 if (mContainer == null) {
                     return;
@@ -84,9 +84,10 @@
             }
 
             final int displayId = mContainer.getDisplayContent().getDisplayId();
-            final Rect toBounds = mService.getPictureInPictureBounds(displayId, aspectRatio);
+            final Rect toBounds = mService.getPictureInPictureBounds(displayId, aspectRatio,
+                    true /* useExistingStackBounds */);
             final Rect targetBounds = new Rect();
-            mContainer.getAnimatingBounds(targetBounds);
+            mContainer.getAnimationOrCurrentBounds(targetBounds);
             final PinnedStackController pinnedStackController =
                     mContainer.getDisplayContent().getPinnedStackController();
 
@@ -117,8 +118,12 @@
     /**
      * @return whether the bounds are currently animating to fullscreen.
      */
-    public boolean isBoundsAnimatingToFullscreen() {
-        return mContainer.isBoundsAnimatingToFullscreen();
+    public boolean isAnimatingBoundsToFullscreen() {
+        return mContainer.isAnimatingBoundsToFullscreen();
+    }
+
+    public boolean pinnedStackResizeAllowed() {
+        return mContainer.pinnedStackResizeAllowed();
     }
 
     /**
@@ -132,4 +137,16 @@
         }
         return bounds;
     }
+
+    /**
+     * The following calls are made from WM to AM.
+     */
+
+    /** Calls directly into activity manager so window manager lock shouldn't held. */
+    public void updatePictureInPictureModeForPinnedStackAnimation(Rect targetStackBounds) {
+        if (mListener != null) {
+            PinnedStackWindowListener listener = (PinnedStackWindowListener) mListener;
+            listener.updatePictureInPictureModeForPinnedStackAnimation(targetStackBounds);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/PinnedStackWindowListener.java b/services/core/java/com/android/server/wm/PinnedStackWindowListener.java
new file mode 100644
index 0000000..12b9c1f
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PinnedStackWindowListener.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Interface used by the creator of {@link PinnedStackWindowController} to listen to changes with
+ * the stack container.
+ */
+public interface PinnedStackWindowListener extends StackWindowListener {
+
+    /**
+     * Called when the stack container pinned stack animation will change the picture-in-picture
+     * mode. This is a direct call into ActivityManager.
+     */
+    default void updatePictureInPictureModeForPinnedStackAnimation(Rect targetStackBounds) {}
+}
diff --git a/services/core/java/com/android/server/wm/StackWindowController.java b/services/core/java/com/android/server/wm/StackWindowController.java
index bf024cf..b927e67 100644
--- a/services/core/java/com/android/server/wm/StackWindowController.java
+++ b/services/core/java/com/android/server/wm/StackWindowController.java
@@ -368,13 +368,6 @@
         }
     }
 
-    /** Calls directly into activity manager so window manager lock shouldn't held. */
-    void updatePictureInPictureModeForPinnedStackAnimation(Rect targetStackBounds) {
-        if (mListener != null) {
-            mListener.updatePictureInPictureModeForPinnedStackAnimation(targetStackBounds);
-        }
-    }
-
     void requestResize(Rect bounds) {
         mHandler.obtainMessage(H.REQUEST_RESIZE, bounds).sendToTarget();
     }
diff --git a/services/core/java/com/android/server/wm/StackWindowListener.java b/services/core/java/com/android/server/wm/StackWindowListener.java
index a55f9df..c763c17 100644
--- a/services/core/java/com/android/server/wm/StackWindowListener.java
+++ b/services/core/java/com/android/server/wm/StackWindowListener.java
@@ -26,10 +26,4 @@
 
     /** Called when the stack container would like its controller to resize. */
     void requestResize(Rect bounds);
-
-    /**
-     * Called when the stack container pinned stack animation will change the picture-in-picture
-     * mode. This is a direct call into ActivityManager.
-     */
-    default void updatePictureInPictureModeForPinnedStackAnimation(Rect targetStackBounds) {}
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index b816d81..5c46ca9 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -595,8 +595,7 @@
      * we will have a jump at the end.
      */
     boolean isFloating() {
-        return StackId.tasksAreFloating(mStack.mStackId)
-            && !mStack.isBoundsAnimatingToFullscreen();
+        return StackId.tasksAreFloating(mStack.mStackId) && !mStack.isAnimatingBoundsToFullscreen();
     }
 
     WindowState getTopVisibleAppMainWindow() {
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 1a67ac7..d7c41d3 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -77,6 +77,7 @@
     /** For comparison with DisplayContent bounds. */
     private Rect mTmpRect = new Rect();
     private Rect mTmpRect2 = new Rect();
+    private Rect mTmpRect3 = new Rect();
 
     /** Content limits relative to the DisplayContent this sits in. */
     private Rect mBounds = new Rect();
@@ -125,7 +126,11 @@
     // perfectly fit the region it would have been cropped to. We may also avoid certain logic we
     // would otherwise apply while resizing, while resizing in the bounds animating mode.
     private boolean mBoundsAnimating = false;
+    // Set when an animation has been requested but has not yet started from the UI thread. This is
+    // cleared when the animation actually starts.
+    private boolean mBoundsAnimatingRequested = false;
     private boolean mBoundsAnimatingToFullscreen = false;
+    private boolean mCancelCurrentBoundsAnimation = false;
     private Rect mBoundsAnimationTarget = new Rect();
     private Rect mBoundsAnimationSourceBounds = new Rect();
 
@@ -262,12 +267,6 @@
 
         if (mDisplayContent != null) {
             mDisplayContent.mDimLayerController.updateDimLayer(this);
-            if (mStackId == PINNED_STACK_ID) {
-                // Update the bounds based on any changes to the display info
-                getAnimatingBounds(mTmpRect2);
-                mDisplayContent.mPinnedStackControllerLocked.onTaskStackBoundsChanged(mTmpRect2,
-                        bounds);
-            }
             mAnimationBackgroundSurface.setBounds(bounds);
         }
 
@@ -320,10 +319,11 @@
     }
 
     /**
-     * Sets the bounds animation target bounds.  This can't currently be done in onAnimationStart()
-     * since that is started on the UiThread.
+     * 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 setAnimatingBounds(Rect sourceBounds, Rect destBounds) {
+    void setAnimationFinalBounds(Rect sourceBounds, Rect destBounds) {
+        mBoundsAnimatingRequested = true;
         if (sourceBounds != null) {
             mBoundsAnimationSourceBounds.set(sourceBounds);
         } else {
@@ -337,23 +337,26 @@
     }
 
     /**
-     * @return the source bounds for the bounds animation.
+     * @return the final bounds for the bounds animation.
      */
-    void getAnimatingSourceBounds(Rect outBounds) {
-        if (!mBoundsAnimationSourceBounds.isEmpty()) {
-            outBounds.set(mBoundsAnimationSourceBounds);
-            return;
-        }
-        outBounds.setEmpty();
+    void getFinalAnimationBounds(Rect outBounds) {
+        outBounds.set(mBoundsAnimationTarget);
     }
 
     /**
-     * @return the bounds that the task stack is currently being animated towards, or the current
-     *         stack bounds if there is no animation in progress.
+     * @return the final source bounds for the bounds animation.
      */
-    void getAnimatingBounds(Rect outBounds) {
-        if (!mBoundsAnimationTarget.isEmpty()) {
-            outBounds.set(mBoundsAnimationTarget);
+    void getFinalAnimationSourceBounds(Rect outBounds) {
+        outBounds.set(mBoundsAnimationSourceBounds);
+    }
+
+    /**
+     * @return the final animation bounds if the task stack is currently being animated, or the
+     *         current stack bounds otherwise.
+     */
+    void getAnimationOrCurrentBounds(Rect outBounds) {
+        if ((mBoundsAnimatingRequested || mBoundsAnimating) && !mBoundsAnimationTarget.isEmpty()) {
+            getFinalAnimationBounds(outBounds);
             return;
         }
         getBounds(outBounds);
@@ -398,6 +401,24 @@
             // as it's going away soon anyway.
             return false;
         }
+
+        if (mStackId == PINNED_STACK_ID) {
+            getAnimationOrCurrentBounds(mTmpRect2);
+            boolean updated = mDisplayContent.mPinnedStackControllerLocked.onTaskStackBoundsChanged(
+                    mTmpRect2, mTmpRect3);
+            if (updated) {
+                mBoundsAfterRotation.set(mTmpRect3);
+
+                // Once we've set the bounds based on the rotation of the old bounds in the new
+                // orientation, clear the animation target bounds since they are obsolete, and
+                // cancel any currently running animations
+                mBoundsAnimationTarget.setEmpty();
+                mBoundsAnimationSourceBounds.setEmpty();
+                mCancelCurrentBoundsAnimation = true;
+                return true;
+            }
+        }
+
         final int newRotation = getDisplayInfo().rotation;
         final int newDensity = getDisplayInfo().logicalDensityDpi;
 
@@ -413,20 +434,6 @@
             return false;
         }
 
-        if (StackId.tasksAreFloating(mStackId)) {
-            // Update stack bounds again since the display info has changed since updateDisplayInfo,
-            // however, for floating tasks, we don't need to apply the new rotation to the bounds,
-            // we can just update and return them here
-            setBounds(mBounds);
-            mBoundsAfterRotation.set(mBounds);
-
-            // Once we've set the bounds based on the rotation of the old bounds in the new
-            // orientation, clear the animation target bounds since they are obsolete
-            mBoundsAnimationTarget.setEmpty();
-            mBoundsAnimationSourceBounds.setEmpty();
-            return true;
-        }
-
         mTmpRect2.set(mBounds);
         mDisplayContent.rotateBounds(mRotation, newRotation, mTmpRect2);
         switch (mStackId) {
@@ -692,6 +699,14 @@
             getStackDockedModeBounds(mTmpRect, bounds, mStackId, mTmpRect2,
                     mDisplayContent.mDividerControllerLocked.getContentWidth(),
                     dockedOnTopOrLeft);
+        } else if (mStackId == PINNED_STACK_ID) {
+            // Update the bounds based on any changes to the display info
+            getAnimationOrCurrentBounds(mTmpRect2);
+            boolean updated = mDisplayContent.mPinnedStackControllerLocked.onTaskStackBoundsChanged(
+                    mTmpRect2, mTmpRect3);
+            if (updated) {
+                bounds = new Rect(mTmpRect3);
+            }
         }
 
         updateDisplayInfo(bounds);
@@ -1443,21 +1458,11 @@
         }
     }
 
-    @Override  // AnimatesBounds
-    public boolean setSize(Rect bounds) {
-        synchronized (mService.mWindowMap) {
-            if (mDisplayContent == null) {
-                return false;
-            }
-        }
-        try {
-            mService.mActivityManager.resizeStack(mStackId, bounds, false, true, false, -1);
-        } catch (RemoteException e) {
-        }
-        return true;
-    }
-
     public boolean setPinnedStackSize(Rect bounds, Rect tempTaskBounds) {
+        if (mCancelCurrentBoundsAnimation) {
+            return false;
+        }
+
         try {
             mService.mActivityManager.resizePinnedStack(bounds, tempTaskBounds);
         } catch (RemoteException e) {
@@ -1469,8 +1474,10 @@
     @Override  // AnimatesBounds
     public void onAnimationStart(boolean toFullscreen) {
         synchronized (mService.mWindowMap) {
+            mBoundsAnimatingRequested = false;
             mBoundsAnimating = true;
             mBoundsAnimatingToFullscreen = toFullscreen;
+            mCancelCurrentBoundsAnimation = false;
         }
 
         if (mStackId == PINNED_STACK_ID) {
@@ -1484,7 +1491,8 @@
 
     @Override  // AnimatesBounds
     public void updatePictureInPictureMode(Rect targetStackBounds) {
-        final StackWindowController controller = getController();
+        final PinnedStackWindowController controller =
+                (PinnedStackWindowController) getController();
         if (controller != null) {
             controller.updatePictureInPictureModeForPinnedStackAnimation(targetStackBounds);
         }
@@ -1523,14 +1531,21 @@
         return mBoundsAnimating;
     }
 
-    public boolean getBoundsAnimating() {
+    public boolean isAnimatingBounds() {
         return mBoundsAnimating;
     }
 
-    public boolean isBoundsAnimatingToFullscreen() {
+    public boolean isAnimatingBoundsToFullscreen() {
         return mBoundsAnimating && mBoundsAnimatingToFullscreen;
     }
 
+    public boolean pinnedStackResizeAllowed() {
+        if (mBoundsAnimating && mCancelCurrentBoundsAnimation) {
+            return true;
+        }
+        return false;
+    }
+
     /** Returns true if a removal action is still being deferred. */
     boolean checkCompleteDeferredRemoval() {
         if (isAnimating()) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0f4707e..0049585 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -31,7 +31,6 @@
 import static android.os.Process.SYSTEM_UID;
 import static android.os.Process.THREAD_PRIORITY_DISPLAY;
 import static android.os.Process.myPid;
-import static android.os.Process.myTid;
 import static android.os.UserHandle.USER_NULL;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.DOCKED_INVALID;
@@ -148,7 +147,6 @@
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
 import android.os.PowerSaveState;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.StrictMode;
@@ -2758,7 +2756,12 @@
         mDockedStackCreateBounds = bounds;
     }
 
-    public Rect getPictureInPictureBounds(int displayId, float aspectRatio) {
+    /**
+     * @param useExistingStackBounds Apply {@param aspectRatio} to the existing target stack bounds
+     *                               if possible
+     */
+    public Rect getPictureInPictureBounds(int displayId, float aspectRatio,
+            boolean useExistingStackBounds) {
         synchronized (mWindowMap) {
             if (!mSupportsPictureInPicture) {
                 return null;
@@ -2773,11 +2776,11 @@
             final PinnedStackController pinnedStackController =
                     displayContent.getPinnedStackController();
             final TaskStack stack = displayContent.getStackById(PINNED_STACK_ID);
-            if (stack != null) {
+            if (stack != null && useExistingStackBounds) {
                 // If the stack exists, then use its final bounds to calculate the new aspect ratio
                 // bounds.
                 stackBounds = new Rect();
-                stack.getAnimatingBounds(stackBounds);
+                stack.getAnimationOrCurrentBounds(stackBounds);
             } else {
                 // Otherwise, just calculate the aspect ratio bounds from the default bounds
                 stackBounds = pinnedStackController.getDefaultBounds();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 979af7e..0b96f3f 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1090,7 +1090,7 @@
         // notify the client of frame changes in this case. Not only is it a lot of churn, but
         // the frame may not correspond to the surface size or the onscreen area at various
         // phases in the animation, and the client will become sad and confused.
-        if (task != null && task.mStack.getBoundsAnimating()) {
+        if (task != null && task.mStack.isAnimatingBounds()) {
             return;
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index ae17d08..fa35336 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -1360,11 +1360,11 @@
             int posX = mTmpSize.left;
             int posY = mTmpSize.top;
             task.mStack.getDimBounds(mTmpStackBounds);
-            task.mStack.getAnimatingSourceBounds(mTmpSourceBounds);
+            task.mStack.getFinalAnimationSourceBounds(mTmpSourceBounds);
             if (!mTmpSourceBounds.isEmpty()) {
                 // Get the final target stack bounds, if we are not animating, this is just the
                 // current stack bounds
-                task.mStack.getAnimatingBounds(mTmpAnimatingBounds);
+                task.mStack.getFinalAnimationBounds(mTmpAnimatingBounds);
 
                 // Calculate the current progress and interpolate the difference between the target
                 // and source bounds
diff --git a/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java
new file mode 100644
index 0000000..85dc712
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java
@@ -0,0 +1,348 @@
+/*
+ * 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.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.WindowManagerInternal.AppTransitionListener;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.server.wm.BoundsAnimationController.BoundsAnimator;
+
+/**
+ * Test class for {@link BoundsAnimationController} to ensure that it sends the right callbacks
+ * depending on the various interactions.
+ *
+ * Build/Install/Run:
+ *  bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BoundsAnimationControllerTests extends WindowTestsBase {
+
+    /**
+     * Mock value animator to simulate updates with.
+     */
+    private class MockValueAnimator extends ValueAnimator {
+
+        private float mFraction;
+
+        public MockValueAnimator getWithValue(float fraction) {
+            mFraction = fraction;
+            return this;
+        }
+
+        @Override
+        public Object getAnimatedValue() {
+            return mFraction;
+        }
+    }
+
+    /**
+     * Mock app transition to fire notifications to the bounds animator.
+     */
+    private class MockAppTransition extends AppTransition {
+
+        private AppTransitionListener mListener;
+
+        MockAppTransition(Context context) {
+            super(context, null);
+        }
+
+        @Override
+        void registerListenerLocked(AppTransitionListener listener) {
+            mListener = listener;
+        }
+
+        public void notifyTransitionPending() {
+            mListener.onAppTransitionPendingLocked();
+        }
+
+        public void notifyTransitionCancelled(int transit) {
+            mListener.onAppTransitionCancelledLocked(transit);
+        }
+
+        public void notifyTransitionStarting(int transit) {
+            mListener.onAppTransitionStartingLocked(transit, null, null, null, null);
+        }
+
+        public void notifyTransitionFinished() {
+            mListener.onAppTransitionFinishedLocked(null);
+        }
+    }
+
+    /**
+     * A test animate bounds user to track callbacks from the bounds animation.
+     */
+    private class TestAnimateBoundsUser implements BoundsAnimationController.AnimateBoundsUser {
+
+        boolean mMovedToFullscreen;
+        boolean mAnimationStarted;
+        boolean mAnimationStartedToFullscreen;
+        boolean mAnimationEnded;
+        boolean mUpdatedPictureInPictureModeWithBounds;
+        boolean mBoundsUpdated;
+        Rect mStackBounds;
+        Rect mTaskBounds;
+
+        boolean mRequestCancelAnimation = false;
+
+        void reinitialize(Rect stackBounds, Rect taskBounds) {
+            mMovedToFullscreen = false;
+            mAnimationStarted = false;
+            mAnimationStartedToFullscreen = false;
+            mAnimationEnded = false;
+            mUpdatedPictureInPictureModeWithBounds = false;
+            mStackBounds = stackBounds;
+            mTaskBounds = taskBounds;
+            mBoundsUpdated = false;
+            mRequestCancelAnimation = false;
+        }
+
+        @Override
+        public void onAnimationStart(boolean toFullscreen) {
+            mAnimationStarted = true;
+            mAnimationStartedToFullscreen = toFullscreen;
+        }
+
+        @Override
+        public void updatePictureInPictureMode(Rect targetStackBounds) {
+            mUpdatedPictureInPictureModeWithBounds = true;
+        }
+
+        @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) {
+                return false;
+            } else {
+                mBoundsUpdated = true;
+                mStackBounds = stackBounds;
+                mTaskBounds = taskBounds;
+                return true;
+            }
+        }
+
+        @Override
+        public void onAnimationEnd() {
+            mAnimationEnded = true;
+        }
+
+        @Override
+        public void moveToFullscreen() {
+            mMovedToFullscreen = true;
+        }
+    }
+
+    // Constants
+    private static final boolean MOVE_TO_FULLSCREEN = true;
+
+    // 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;
+
+    // Common
+    private MockAppTransition mAppTransition;
+    private MockValueAnimator mAnimator;
+    private TestAnimateBoundsUser mTarget;
+    private BoundsAnimationController mController;
+
+    // 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());
+        mAppTransition = new MockAppTransition(context);
+        mAnimator = new MockValueAnimator();
+        mTarget = new TestAnimateBoundsUser();
+        mController = new BoundsAnimationController(context, mAppTransition, handler);
+    }
+
+    @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);
+    }
+
+    @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);
+    }
+
+    @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);
+    }
+
+    @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);
+    }
+
+    @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);
+    }
+
+    /**
+     * @return the bounds offset to zero/zero.
+     */
+    private Rect offsetToZero(Rect bounds) {
+        mTmpRect.set(bounds);
+        mTmpRect.offsetTo(0, 0);
+        return mTmpRect;
+    }
+}