Merge "Creating PinnedStackController."
diff --git a/Android.mk b/Android.mk
index 4b0587f..db63739 100644
--- a/Android.mk
+++ b/Android.mk
@@ -299,6 +299,8 @@
 	core/java/android/view/IInputFilter.aidl \
 	core/java/android/view/IInputFilterHost.aidl \
 	core/java/android/view/IOnKeyguardExitResult.aidl \
+	core/java/android/view/IPinnedStackController.aidl \
+	core/java/android/view/IPinnedStackListener.aidl \
 	core/java/android/view/IRotationWatcher.aidl \
 	core/java/android/view/IWindow.aidl \
 	core/java/android/view/IWindowFocusObserver.aidl \
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 82cea8a..623a11d 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -2940,22 +2940,6 @@
             reply.writeNoException();
             return true;
         }
-        case GET_DEFAULT_PICTURE_IN_PICTURE_BOUNDS_TRANSACTION: {
-            data.enforceInterface(IActivityManager.descriptor);
-            final int displayId = data.readInt();
-            Rect r = getDefaultPictureInPictureBounds(displayId);
-            reply.writeNoException();
-            r.writeToParcel(reply, 0);
-            return true;
-        }
-        case GET_PICTURE_IN_PICTURE_MOVEMENT_BOUNDS_TRANSACTION: {
-            data.enforceInterface(IActivityManager.descriptor);
-            final int displayId = data.readInt();
-            Rect r = getPictureInPictureMovementBounds(displayId);
-            reply.writeNoException();
-            r.writeToParcel(reply, 0);
-            return true;
-        }
         case SET_VR_MODE_TRANSACTION: {
             data.enforceInterface(IActivityManager.descriptor);
             final IBinder token = data.readStrongBinder();
@@ -7027,36 +7011,6 @@
     }
 
     @Override
-    public Rect getDefaultPictureInPictureBounds(int displayId) throws RemoteException
-    {
-        Parcel data = Parcel.obtain();
-        Parcel reply = Parcel.obtain();
-        data.writeInterfaceToken(IActivityManager.descriptor);
-        data.writeInt(displayId);
-        mRemote.transact(GET_DEFAULT_PICTURE_IN_PICTURE_BOUNDS_TRANSACTION, data, reply, 0);
-        reply.readException();
-        Rect rect = Rect.CREATOR.createFromParcel(reply);
-        data.recycle();
-        reply.recycle();
-        return rect;
-    }
-
-    @Override
-    public Rect getPictureInPictureMovementBounds(int displayId) throws RemoteException
-    {
-        Parcel data = Parcel.obtain();
-        Parcel reply = Parcel.obtain();
-        data.writeInterfaceToken(IActivityManager.descriptor);
-        data.writeInt(displayId);
-        mRemote.transact(GET_PICTURE_IN_PICTURE_MOVEMENT_BOUNDS_TRANSACTION, data, reply, 0);
-        reply.readException();
-        Rect rect = Rect.CREATOR.createFromParcel(reply);
-        data.recycle();
-        reply.recycle();
-        return rect;
-    }
-
-    @Override
     public boolean isAppForeground(int uid) throws RemoteException {
         Parcel data = Parcel.obtain();
         Parcel reply = Parcel.obtain();
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 3f11a7f..7b25c76 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -658,16 +658,6 @@
 
     public void enterPictureInPictureMode(IBinder token) throws RemoteException;
 
-    /**
-     * @return the default bounds of the PIP on the default display.
-     */
-    public Rect getDefaultPictureInPictureBounds(int displayId) throws RemoteException;
-
-    /**
-     * @return the movement bounds of the PIP on the default display.
-     */
-    public Rect getPictureInPictureMovementBounds(int displayId) throws RemoteException;
-
     public int setVrMode(IBinder token, boolean enabled, ComponentName packageName)
             throws RemoteException;
 
@@ -1112,8 +1102,6 @@
 
     // Start of O transactions
     int REQUEST_ACTIVITY_RELAUNCH = IBinder.FIRST_CALL_TRANSACTION+400;
-    int GET_DEFAULT_PICTURE_IN_PICTURE_BOUNDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 401;
-    int GET_PICTURE_IN_PICTURE_MOVEMENT_BOUNDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 402;
-    int UPDATE_DISPLAY_OVERRIDE_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 403;
-    int UNREGISTER_TASK_STACK_LISTENER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+404;
+    int UPDATE_DISPLAY_OVERRIDE_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 401;
+    int UNREGISTER_TASK_STACK_LISTENER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+402;
 }
diff --git a/core/java/android/view/IPinnedStackController.aidl b/core/java/android/view/IPinnedStackController.aidl
new file mode 100644
index 0000000..830591d
--- /dev/null
+++ b/core/java/android/view/IPinnedStackController.aidl
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+
+/**
+ * An interface to the PinnedStackController to update it of state changes, and to query
+ * information based on the current state.
+ *
+ * @hide
+ */
+interface IPinnedStackController {
+
+    /**
+     * Notifies the controller that the user is currently interacting with the PIP.
+     */
+    oneway void setInInteractiveMode(boolean inInteractiveMode);
+}
diff --git a/core/java/android/view/IPinnedStackListener.aidl b/core/java/android/view/IPinnedStackListener.aidl
new file mode 100644
index 0000000..3050dbb
--- /dev/null
+++ b/core/java/android/view/IPinnedStackListener.aidl
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.view.IPinnedStackController;
+
+/**
+  * Listener for changes to the pinned stack made by the WindowManager.
+  *
+  * @hide
+  */
+oneway interface IPinnedStackListener {
+
+    /**
+     * Called when the listener is registered and provides an interface to call back to the pinned
+     * stack controller to update the controller of the pinned stack state.
+     */
+    void onListenerRegistered(IPinnedStackController controller);
+
+    /**
+     * Called when window manager decides to adjust the pinned stack bounds, or when the listener
+     * is first registered to allow the listener to synchronized its state with the controller.
+     */
+    void onBoundsChanged(boolean adjustedForIme);
+}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index e42b42a..986ff46 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -34,6 +34,7 @@
 import android.view.IAppTransitionAnimationSpecsFuture;
 import android.view.IDockedStackListener;
 import android.view.IOnKeyguardExitResult;
+import android.view.IPinnedStackListener;
 import android.view.IRotationWatcher;
 import android.view.IWindowSession;
 import android.view.IWindowSessionCallback;
@@ -426,6 +427,21 @@
     void registerDockedStackListener(IDockedStackListener listener);
 
     /**
+     * Registers a listener that will be called when the pinned stack state changes.
+     */
+    void registerPinnedStackListener(int displayId, IPinnedStackListener listener);
+
+    /**
+     * Returns the initial bounds that PIP will be shown when it is first started.
+     */
+    Rect getPictureInPictureDefaultBounds(int displayId);
+
+    /**
+     * Returns the bounds that the PIP can move on the screen in the current PIP state.
+     */
+    Rect getPictureInPictureMovementBounds(int displayId);
+
+    /**
      * Updates the dim layer used while resizing.
      *
      * @param visible Whether the dim layer should be visible.
diff --git a/core/java/com/android/internal/policy/PipMotionHelper.java b/core/java/com/android/internal/policy/PipMotionHelper.java
new file mode 100644
index 0000000..0543442
--- /dev/null
+++ b/core/java/com/android/internal/policy/PipMotionHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 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.internal.policy;
+
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.app.ActivityManagerNative;
+import android.app.IActivityManager;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * A helper to animate the PIP.
+ */
+public class PipMotionHelper {
+
+    private static final String TAG = "PipMotionHelper";
+
+    private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+    private static final int DEFAULT_DURATION = 225;
+
+    private IActivityManager mActivityManager;
+    private Handler mHandler;
+
+    public PipMotionHelper(Handler handler) {
+        mHandler = handler;
+    }
+
+    /**
+     * Moves the PIP to give given {@param bounds}.
+     */
+    public void resizeToBounds(Rect toBounds) {
+        mHandler.post(() -> {
+            if (mActivityManager == null) {
+                mActivityManager = ActivityManagerNative.getDefault();
+            }
+            try {
+                mActivityManager.resizePinnedStack(toBounds, null /* tempPinnedTaskBounds */);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Could not move pinned stack to bounds: " + toBounds, e);
+            }
+        });
+    }
+
+    /**
+     * Creates an animation to move the PIP to give given {@param toBounds} with the default
+     * animation properties.
+     */
+    public ValueAnimator createAnimationToBounds(Rect fromBounds, Rect toBounds) {
+        return createAnimationToBounds(fromBounds, toBounds, DEFAULT_DURATION, FAST_OUT_SLOW_IN,
+                null);
+    }
+
+    /**
+     * Creates an animation to move the PIP to give given {@param toBounds}.
+     */
+    public ValueAnimator createAnimationToBounds(Rect fromBounds, Rect toBounds, int duration,
+            Interpolator interpolator, ValueAnimator.AnimatorUpdateListener updateListener) {
+        ValueAnimator anim = ValueAnimator.ofObject(RECT_EVALUATOR, fromBounds, toBounds);
+        anim.setDuration(duration);
+        anim.setInterpolator(interpolator);
+        anim.addUpdateListener((ValueAnimator animation) -> {
+            resizeToBounds((Rect) animation.getAnimatedValue());
+        });
+        if (updateListener != null) {
+            anim.addUpdateListener(updateListener);
+        }
+        return anim;
+    }
+
+
+}
diff --git a/core/java/com/android/internal/policy/PipSnapAlgorithm.java b/core/java/com/android/internal/policy/PipSnapAlgorithm.java
index 793b228..1246c27 100644
--- a/core/java/com/android/internal/policy/PipSnapAlgorithm.java
+++ b/core/java/com/android/internal/policy/PipSnapAlgorithm.java
@@ -48,21 +48,12 @@
     private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
     private final int mSnapMode = SNAP_MODE_CORNERS_ONLY;
 
-    private final Scroller mScroller;
-    private final Rect mDisplayBounds = new Rect();
+    private Scroller mScroller;
     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
 
-    public PipSnapAlgorithm(Context context, int displayId) {
-        final DisplayManager displayManager =
-                (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
-        final ViewConfiguration viewConfig = ViewConfiguration.get(context);
-        final Point displaySize = new Point();
-        displayManager.getDisplay(displayId).getRealSize(displaySize);
+    public PipSnapAlgorithm(Context context) {
         mContext = context;
-        mDisplayBounds.set(0, 0, displaySize.x, displaySize.y);
         mOrientation = context.getResources().getConfiguration().orientation;
-        mScroller = new Scroller(context);
-        mScroller.setFriction(viewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
         calculateSnapTargets();
     }
 
@@ -74,7 +65,12 @@
     public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
             float velocityY) {
         final Rect finalStackBounds = new Rect(stackBounds);
-        mScroller.fling(stackBounds.left, stackBounds.top,
+        if (mScroller == null) {
+            final ViewConfiguration viewConfig = ViewConfiguration.get(mContext);
+            mScroller = new Scroller(mContext);
+            mScroller.setFriction(viewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
+        }
+        mScroller.fling(sItackBounds.left, stackBounds.top,
                 (int) velocityX, (int) velocityY,
                 movementBounds.left, movementBounds.right,
                 movementBounds.top, movementBounds.bottom);
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
index ffe99c5..7f2d415 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -17,15 +17,16 @@
 package com.android.systemui.pip.phone;
 
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.INPUT_CONSUMER_PIP;
 
 import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN;
 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.systemui.recents.misc.Utilities.RECT_EVALUATOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.app.ActivityManager.StackInfo;
 import android.app.IActivityManager;
 import android.content.Context;
@@ -34,6 +35,8 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
 import android.view.IWindowManager;
 import android.view.InputChannel;
 import android.view.InputEvent;
@@ -41,9 +44,9 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
-import android.view.animation.Interpolator;
 
 import com.android.internal.os.BackgroundThread;
+import com.android.internal.policy.PipMotionHelper;
 import com.android.internal.policy.PipSnapAlgorithm;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 import com.android.systemui.tuner.TunerService;
@@ -65,12 +68,16 @@
 
     private final Context mContext;
     private final IActivityManager mActivityManager;
+    private final IWindowManager mWindowManager;
     private final ViewConfiguration mViewConfig;
     private final InputChannel mInputChannel = new InputChannel();
+    private final PinnedStackListener mPinnedStackListener = new PinnedStackListener();
+    private IPinnedStackController mPinnedStackController;
 
     private final PipInputEventReceiver mInputEventReceiver;
     private PipDismissViewController mDismissViewController;
     private PipSnapAlgorithm mSnapAlgorithm;
+    private PipMotionHelper mMotionHelper;
 
     private boolean mEnableSwipeToDismiss = true;
     private boolean mEnableDragToDismiss = true;
@@ -78,6 +85,13 @@
     private final Rect mPinnedStackBounds = new Rect();
     private final Rect mBoundedPinnedStackBounds = new Rect();
     private ValueAnimator mPinnedStackBoundsAnimator = null;
+    private ValueAnimator.AnimatorUpdateListener mUpdatePinnedStackBoundsListener =
+            new AnimatorUpdateListener() {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            mPinnedStackBounds.set((Rect) animation.getAnimatedValue());
+        }
+    };
 
     private final PointF mDownTouch = new PointF();
     private final PointF mLastTouch = new PointF();
@@ -88,10 +102,13 @@
     private final FlingAnimationUtils mFlingAnimationUtils;
     private VelocityTracker mVelocityTracker;
 
+    private final Rect mTmpBounds = new Rect();
+
     /**
      * Input handler used for Pip windows.
      */
     private final class PipInputEventReceiver extends InputEventReceiver {
+
         public PipInputEventReceiver(InputChannel inputChannel, Looper looper) {
             super(inputChannel, looper);
         }
@@ -111,6 +128,22 @@
         }
     }
 
+    /**
+     * Handler for messages from the PIP controller.
+     */
+    private class PinnedStackListener extends IPinnedStackListener.Stub {
+
+        @Override
+        public void onListenerRegistered(IPinnedStackController controller) {
+            mPinnedStackController = controller;
+        }
+
+        @Override
+        public void onBoundsChanged(boolean adjustedForIme) {
+            // Do nothing
+        }
+    }
+
     public PipTouchHandler(Context context, IActivityManager activityManager,
             IWindowManager windowManager) {
 
@@ -118,17 +151,20 @@
         try {
             windowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
             windowManager.createInputConsumer(INPUT_CONSUMER_PIP, mInputChannel);
+            windowManager.registerPinnedStackListener(DEFAULT_DISPLAY, mPinnedStackListener);
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to create PIP input consumer", e);
         }
         mContext = context;
         mActivityManager = activityManager;
+        mWindowManager = windowManager;
         mViewConfig = ViewConfiguration.get(context);
         mInputEventReceiver = new PipInputEventReceiver(mInputChannel, Looper.myLooper());
         if (mEnableDragToDismiss) {
             mDismissViewController = new PipDismissViewController(context);
         }
         mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
+        mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
 
         // Register any tuner settings changes
         TunerService.get(context).addTunable(this, TUNER_KEY_SWIPE_TO_DISMISS,
@@ -137,12 +173,15 @@
 
     @Override
     public void onTuningChanged(String key, String newValue) {
+        if (newValue == null) {
+            return;
+        }
         switch (key) {
             case TUNER_KEY_SWIPE_TO_DISMISS:
-                mEnableSwipeToDismiss = (newValue != null) && Integer.parseInt(newValue) != 0;
+                mEnableSwipeToDismiss = Integer.parseInt(newValue) != 0;
                 break;
             case TUNER_KEY_DRAG_TO_DISMISS:
-                mEnableDragToDismiss = (newValue != null) && Integer.parseInt(newValue) != 0;
+                mEnableDragToDismiss = Integer.parseInt(newValue) != 0;
                 break;
         }
     }
@@ -152,6 +191,11 @@
     }
 
     private void handleTouchEvent(MotionEvent ev) {
+        // Skip touch handling until we are bound to the controller
+        if (mPinnedStackController == null) {
+            return;
+        }
+
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN: {
                 // Cancel any existing animations on the pinned stack
@@ -166,6 +210,11 @@
                 mLastTouch.set(ev.getX(), ev.getY());
                 mDownTouch.set(mLastTouch);
                 mIsDragging = false;
+                try {
+                    mPinnedStackController.setInInteractiveMode(true);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not set dragging state", e);
+                }
                 if (mEnableDragToDismiss) {
                     // TODO: Consider setting a timer such at after X time, we show the dismiss
                     //       target if the user hasn't already dragged some distance
@@ -203,8 +252,12 @@
 
                 if (mIsSwipingToDismiss) {
                     // Ignore the vertical movement
-                    top = mPinnedStackBounds.top;
-                    movePinnedStack(left, top);
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
+                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
+                        mPinnedStackBounds.set(mTmpBounds);
+                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
+                    }
                 } else if (mIsDragging) {
                     // Move the pinned stack
                     if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) {
@@ -213,7 +266,12 @@
                         top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
                                 mBoundedPinnedStackBounds.bottom, top));
                     }
-                    movePinnedStack(left, top);
+                    mTmpBounds.set(mPinnedStackBounds);
+                    mTmpBounds.offsetTo((int) left, (int) top);
+                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
+                        mPinnedStackBounds.set(mTmpBounds);
+                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
+                    }
                 }
                 mLastTouch.set(ev.getX(), ev.getY());
                 break;
@@ -275,6 +333,11 @@
             case MotionEvent.ACTION_CANCEL: {
                 mIsDragging = false;
                 mIsSwipingToDismiss = false;
+                try {
+                    mPinnedStackController.setInInteractiveMode(false);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not set dragging state", e);
+                }
                 recycleVelocityTracker();
                 break;
             }
@@ -303,8 +366,8 @@
         Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
                 mPinnedStackBounds, velocityX, velocityY);
         if (!mPinnedStackBounds.equals(toBounds)) {
-            mPinnedStackBoundsAnimator = createResizePinnedStackAnimation(
-                toBounds, 0, FAST_OUT_SLOW_IN);
+            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
             mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0,
                 distanceBetweenRectOffsets(mPinnedStackBounds, toBounds),
                 velocity);
@@ -319,8 +382,8 @@
         Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
                 mPinnedStackBounds);
         if (!mPinnedStackBounds.equals(toBounds)) {
-            mPinnedStackBoundsAnimator = createResizePinnedStackAnimation(
-                toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN);
+            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
             mPinnedStackBoundsAnimator.start();
         }
     }
@@ -335,8 +398,8 @@
         Rect toBounds = new Rect(mPinnedStackBounds);
         toBounds.offsetTo((int) offsetX, toBounds.top);
         if (!mPinnedStackBounds.equals(toBounds)) {
-            mPinnedStackBoundsAnimator = createResizePinnedStackAnimation(
-                toBounds, 0, FAST_OUT_SLOW_IN);
+            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+                toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
             mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0,
                 distanceBetweenRectOffsets(mPinnedStackBounds, toBounds),
                 velocityX);
@@ -364,8 +427,8 @@
             dismissBounds.centerY(),
             dismissBounds.centerX() + 1,
             dismissBounds.centerY() + 1);
-        mPinnedStackBoundsAnimator = createResizePinnedStackAnimation(
-            toBounds, DISMISS_STACK_DURATION, FAST_OUT_LINEAR_IN);
+        mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
+            toBounds, DISMISS_STACK_DURATION, FAST_OUT_LINEAR_IN, mUpdatePinnedStackBoundsListener);
         mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
@@ -404,9 +467,9 @@
             StackInfo info = mActivityManager.getStackInfo(PINNED_STACK_ID);
             if (info != null) {
                 mPinnedStackBounds.set(info.bounds);
-                mBoundedPinnedStackBounds.set(mActivityManager.getPictureInPictureMovementBounds(
+                mBoundedPinnedStackBounds.set(mWindowManager.getPictureInPictureMovementBounds(
                         info.displayId));
-                mSnapAlgorithm = new PipSnapAlgorithm(mContext, info.displayId);
+                mSnapAlgorithm = new PipSnapAlgorithm(mContext);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Could not fetch PIP movement bounds.", e);
@@ -414,59 +477,6 @@
     }
 
     /**
-     * Moves the pinned stack to the given {@param left} and {@param top} offsets.
-     */
-    private void movePinnedStack(float left, float top) {
-        if ((int) left != mPinnedStackBounds.left || (int) top != mPinnedStackBounds.top) {
-            mPinnedStackBounds.offsetTo((int) left, (int) top);
-            BackgroundThread.getHandler().post(() -> {
-                try {
-                    mActivityManager.resizePinnedStack(mPinnedStackBounds,
-                            null /* tempPinnedBounds */);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Could not move pinned stack to offset: (" + left + ", " + top + ")",
-                            e);
-                }
-            });
-        }
-    }
-
-    /**
-     * Resizes the pinned stack to the given {@param bounds}.
-     */
-    private void resizePinnedStack(Rect bounds) {
-        if (!mPinnedStackBounds.equals(bounds)) {
-            mPinnedStackBounds.set(bounds);
-            BackgroundThread.getHandler().post(() -> {
-                try {
-                    mActivityManager.resizePinnedStack(bounds, null);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Could not resize pinned stack to bounds: (" + bounds + ")");
-                }
-            });
-        }
-    }
-
-    /**
-     * Creates a resize-stack animation.
-     */
-    private ValueAnimator createResizePinnedStackAnimation(Rect toBounds, int duration,
-            Interpolator interpolator) {
-        ValueAnimator anim = ValueAnimator.ofObject(RECT_EVALUATOR,
-                mPinnedStackBounds, toBounds);
-        anim.setDuration(duration);
-        anim.setInterpolator(interpolator);
-        anim.addUpdateListener(
-                new ValueAnimator.AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimator animation) {
-                        resizePinnedStack((Rect) animation.getAnimatedValue());
-                    }
-                });
-        return anim;
-    }
-
-    /**
      * @return the distance between points {@param p1} and {@param p2}.
      */
     private float distanceBetweenRectOffsets(Rect r1, Rect r2) {
diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java
index 894bc53..c272ae6 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java
@@ -39,6 +39,8 @@
 import android.util.Pair;
 import android.view.Display;
 import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.recents.misc.SystemServicesProxy;
@@ -135,6 +137,7 @@
     private Context mContext;
     private PipRecentsOverlayManager mPipRecentsOverlayManager;
     private IActivityManager mActivityManager;
+    private IWindowManager mWindowManager;
     private MediaSessionManager mMediaSessionManager;
     private int mState = STATE_NO_PIP;
     private final Handler mHandler = new Handler();
@@ -205,6 +208,7 @@
         mContext = context;
 
         mActivityManager = ActivityManagerNative.getDefault();
+        mWindowManager = WindowManagerGlobal.getWindowManagerService();
         SystemServicesProxy.getInstance(context).registerTaskStackListener(mTaskStackListener);
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED);
@@ -221,7 +225,7 @@
     private void loadConfigurationsAndApply() {
         Resources res = mContext.getResources();
         try {
-            mDefaultPipBounds = mActivityManager.getDefaultPictureInPictureBounds(
+            mDefaultPipBounds = mWindowManager.getPictureInPictureDefaultBounds(
                     Display.DEFAULT_DISPLAY);
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to get default PIP bounds", e);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 41d07f2..40f0aae 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1392,12 +1392,6 @@
 
     final long[] mTmpLong = new long[2];
 
-    // The size and position information that describes where the pinned stack will go by default.
-    // In particular, the size is defined in DPs.
-    Size mDefaultPinnedStackSizeDp;
-    Size mDefaultPinnedStackScreenEdgeInsetsDp;
-    int mDefaultPinnedStackGravity;
-
     static final class ProcessChangeItem {
         static final int CHANGE_ACTIVITIES = 1<<0;
         static final int CHANGE_PROCESS_STATE = 1<<1;
@@ -7483,7 +7477,8 @@
                 // current bounds.
                 final ActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID);
                 final Rect bounds = (pinnedStack != null)
-                        ? pinnedStack.mBounds : getDefaultPictureInPictureBounds(DEFAULT_DISPLAY);
+                        ? pinnedStack.mBounds
+                        : mWindowManager.getPictureInPictureDefaultBounds(DEFAULT_DISPLAY);
 
                 mStackSupervisor.moveActivityToPinnedStackLocked(
                         r, "enterPictureInPictureMode", bounds);
@@ -7493,85 +7488,6 @@
         }
     }
 
-    @Override
-    public Rect getDefaultPictureInPictureBounds(int displayId) {
-        final long origId = Binder.clearCallingIdentity();
-        final Rect defaultBounds = new Rect();
-        try {
-            synchronized(this) {
-                if (!mSupportsPictureInPicture) {
-                    return new Rect();
-                }
-
-                // Convert the sizes to for the current display state
-                final DisplayMetrics dm = mStackSupervisor.getDisplayRealMetrics(displayId);
-                final int stackWidth = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
-                        mDefaultPinnedStackSizeDp.getWidth(), dm);
-                final int stackHeight = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
-                        mDefaultPinnedStackSizeDp.getHeight(), dm);
-                final Rect maxBounds = new Rect();
-                getPictureInPictureBounds(displayId, maxBounds);
-                Gravity.apply(mDefaultPinnedStackGravity, stackWidth, stackHeight,
-                        maxBounds, 0, 0, defaultBounds);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-        return defaultBounds;
-    }
-
-    @Override
-    public Rect getPictureInPictureMovementBounds(int displayId) {
-        final long origId = Binder.clearCallingIdentity();
-        final Rect maxBounds = new Rect();
-        try {
-            synchronized(this) {
-                if (!mSupportsPictureInPicture) {
-                    return new Rect();
-                }
-
-                getPictureInPictureBounds(displayId, maxBounds);
-
-                // Adjust the max bounds by the current stack dimensions
-                final StackInfo pinnedStackInfo = mStackSupervisor.getStackInfoLocked(
-                        PINNED_STACK_ID);
-                if (pinnedStackInfo != null) {
-                    maxBounds.right = Math.max(maxBounds.left, maxBounds.right -
-                            pinnedStackInfo.bounds.width());
-                    maxBounds.bottom = Math.max(maxBounds.top, maxBounds.bottom -
-                            pinnedStackInfo.bounds.height());
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-        return maxBounds;
-    }
-
-    /**
-     * Calculate the bounds where the pinned stack can move in the current display state.
-     */
-    private void getPictureInPictureBounds(int displayId, Rect outRect) {
-        // Convert the insets to for the current display state
-        final DisplayMetrics dm = mStackSupervisor.getDisplayRealMetrics(displayId);
-        final int insetsLR = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
-                mDefaultPinnedStackScreenEdgeInsetsDp.getWidth(), dm);
-        final int insetsTB = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
-                mDefaultPinnedStackScreenEdgeInsetsDp.getHeight(), dm);
-        try {
-            final Point displaySize = mStackSupervisor.getDisplayRealSize(displayId);
-            final Rect insets = new Rect();
-            mWindowManager.getStableInsets(displayId, insets);
-
-            // Calculate the insets from the system decorations and apply the gravity
-            outRect.set(insets.left + insetsLR, insets.top + insetsTB,
-                    displaySize.x - insets.right - insetsLR,
-                    displaySize.y - insets.bottom - insetsTB);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to calculate PIP movement bounds", e);
-        }
-    }
-
     // =========================================================
     // PROCESS INFO
     // =========================================================
@@ -13083,7 +12999,6 @@
             mLenientBackgroundCheck = lenientBackgroundCheck;
             mSupportsLeanbackOnly = supportsLeanbackOnly;
             mForceResizableActivities = forceResizable;
-            mWindowManager.setForceResizableTasks(mForceResizableActivities);
             if (supportsMultiWindow || forceResizable) {
                 mSupportsMultiWindow = true;
                 mSupportsFreeformWindowManagement = freeformWindowManagement || forceResizable;
@@ -13093,6 +13008,8 @@
                 mSupportsFreeformWindowManagement = false;
                 mSupportsPictureInPicture = false;
             }
+            mWindowManager.setForceResizableTasks(mForceResizableActivities);
+            mWindowManager.setSupportsPictureInPicture(mSupportsPictureInPicture);
             // This happens before any activities are started, so we can change global configuration
             // in-place.
             updateConfigurationLocked(configuration, null, true);
@@ -13106,12 +13023,6 @@
                     com.android.internal.R.dimen.thumbnail_width);
             mThumbnailHeight = res.getDimensionPixelSize(
                     com.android.internal.R.dimen.thumbnail_height);
-            mDefaultPinnedStackSizeDp = Size.parseSize(res.getString(
-                    com.android.internal.R.string.config_defaultPictureInPictureSize));
-            mDefaultPinnedStackScreenEdgeInsetsDp = Size.parseSize(res.getString(
-                    com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
-            mDefaultPinnedStackGravity = res.getInteger(
-                    com.android.internal.R.integer.config_defaultPictureInPictureGravity);
             mAppErrors.loadAppsNotReportingCrashesFromConfigLocked(res.getString(
                     com.android.internal.R.string.config_appsNotReportingCrashes));
             mUserController.mUserSwitchUiEnabled = !res.getBoolean(
@@ -14193,13 +14104,6 @@
                 }
             } else if ("locks".equals(cmd)) {
                 LockGuard.dump(fd, pw, args);
-            } else if ("pip".equals(cmd)) {
-                Rect bounds = getDefaultPictureInPictureBounds(DEFAULT_DISPLAY);
-                pw.print("defaultBounds="); bounds.printShortString(pw);
-                pw.println();
-                bounds = getPictureInPictureMovementBounds(DEFAULT_DISPLAY);
-                pw.print("movementBounds="); bounds.printShortString(pw);
-                pw.println();
             } else {
                 // Dumping a single activity?
                 if (!dumpActivity(fd, pw, cmd, args, opti, dumpAll, dumpVisibleStacks)) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 7a692b6..1484420 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -2498,7 +2498,6 @@
             pw.println("    p[rocesses] [PACKAGE_NAME]: process state");
             pw.println("    o[om]: out of memory management");
             pw.println("    perm[issions]: URI permission grant state");
-            pw.println("    pip: PIP state");
             pw.println("    prov[iders] [COMP_SPEC ...]: content provider state");
             pw.println("    provider [COMP_SPEC]: provider client-side state");
             pw.println("    s[ervices] [COMP_SPEC ...]: service state");
@@ -2702,8 +2701,6 @@
             pw.println("           Test command for sizing <TASK_ID> by <STEP_SIZE>");
             pw.println("           increments within the screen applying the optional [DELAY_MS] between");
             pw.println("           each step.");
-            pw.println("  pip");
-            pw.println("      Gets the current PIP state.");
             pw.println("  write");
             pw.println("      Write all pending state to storage.");
             pw.println();
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index e55c1e4..f0427e4 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -3420,18 +3420,6 @@
         mHandler.sendMessage(mHandler.obtainMessage(HANDLE_DISPLAY_CHANGED, displayId, 0));
     }
 
-    DisplayMetrics getDisplayRealMetrics(int displayId) {
-        final ActivityDisplay activityDisplay = mActivityDisplays.get(displayId);
-        activityDisplay.mDisplay.getRealMetrics(activityDisplay.mRealMetrics);
-        return activityDisplay.mRealMetrics;
-    }
-
-    Point getDisplayRealSize(int displayId) {
-        final ActivityDisplay activityDisplay = mActivityDisplays.get(displayId);
-        activityDisplay.mDisplay.getRealSize(activityDisplay.mRealSize);
-        return activityDisplay.mRealSize;
-    }
-
     private void handleDisplayAdded(int displayId) {
         boolean newDisplay;
         synchronized (mService) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 0b39d65..a99bad2 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -196,6 +196,7 @@
     private boolean mDeferredRemoval;
 
     final DockedStackDividerController mDividerControllerLocked;
+    final PinnedStackController mPinnedStackControllerLocked;
 
     final DimLayerController mDimLayerController;
 
@@ -237,6 +238,7 @@
         mService = service;
         initializeDisplayBaseInfo();
         mDividerControllerLocked = new DockedStackDividerController(service, this);
+        mPinnedStackControllerLocked = new PinnedStackController(service, this);
         mDimLayerController = new DimLayerController(this);
 
         // These are the only direct children we should ever have and they are permanent.
@@ -307,6 +309,10 @@
         return mDividerControllerLocked;
     }
 
+    PinnedStackController getPinnedStackController() {
+        return mPinnedStackControllerLocked;
+    }
+
     /**
      * Returns true if the specified UID has access to this display.
      */
@@ -345,6 +351,7 @@
         mService.reconfigureDisplayLocked(this);
 
         getDockedDividerController().onConfigurationChanged();
+        getPinnedStackController().onConfigurationChanged();
     }
 
     /**
@@ -788,6 +795,7 @@
             mDividerControllerLocked.setAdjustedForIme(
                     false /*ime*/, false /*divider*/, dockVisible /*animate*/, imeWin, imeHeight);
         }
+        mPinnedStackControllerLocked.setAdjustedForIme(imeVisible, imeHeight);
     }
 
     void setInputMethodAnimLayerAdjustment(int adj) {
@@ -930,6 +938,8 @@
         mDimLayerController.dump(prefix + "  ", pw);
         pw.println();
         mDividerControllerLocked.dump(prefix + "  ", pw);
+        pw.println();
+        mPinnedStackControllerLocked.dump(prefix + "  ", pw);
 
         if (mInputMethodAnimLayerAdjustment != 0) {
             pw.println(subPrefix
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
new file mode 100644
index 0000000..a488d52
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Size;
+import android.util.Slog;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.IPinnedStackController;
+import android.view.IPinnedStackListener;
+
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.policy.PipMotionHelper;
+import com.android.internal.policy.PipSnapAlgorithm;
+
+import java.io.PrintWriter;
+
+/**
+ * Holds the common state of the pinned stack between the system and SystemUI.
+ */
+class PinnedStackController {
+
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
+
+    private final WindowManagerService mService;
+    private final DisplayContent mDisplayContent;
+    private final Handler mHandler = new Handler();
+
+    private IPinnedStackListener mPinnedStackListener;
+    private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler =
+            new PinnedStackListenerDeathHandler();
+
+    private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback();
+    private final PipSnapAlgorithm mSnapAlgorithm;
+    private final PipMotionHelper mMotionHelper;
+
+    // States that affect how the PIP can be manipulated
+    private boolean mInInteractiveMode;
+    private boolean mIsImeShowing;
+    private int mImeHeight;
+    private final Rect mPreImeShowingBounds = new Rect();
+    private ValueAnimator mBoundsAnimator = null;
+
+    // The size and position information that describes where the pinned stack will go by default.
+    private int mDefaultStackGravity;
+    private Size mDefaultStackSize;
+    private Point mScreenEdgeInsets;
+
+    // Temp vars for calculation
+    private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
+    private final Rect mTmpInsets = new Rect();
+
+    /**
+     * The callback object passed to listeners for them to notify the controller of state changes.
+     */
+    private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
+
+        @Override
+        public void setInInteractiveMode(final boolean inInteractiveMode) {
+            mHandler.post(() -> {
+                // Cancel any existing animations on the PIP once the user starts dragging it
+                if (mBoundsAnimator != null && inInteractiveMode) {
+                    mBoundsAnimator.cancel();
+                }
+                mInInteractiveMode = inInteractiveMode;
+                mPreImeShowingBounds.setEmpty();
+            });
+        }
+    }
+
+    /**
+     * Handler for the case where the listener dies.
+     */
+    private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
+
+        @Override
+        public void binderDied() {
+            // Clean up the state if the listener dies
+            mInInteractiveMode = false;
+            mPinnedStackListener = null;
+        }
+    }
+
+    PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
+        mService = service;
+        mDisplayContent = displayContent;
+        mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
+        mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
+        reloadResources();
+    }
+
+    void onConfigurationChanged() {
+        reloadResources();
+    }
+
+    /**
+     * Reloads all the resources for the current configuration.
+     */
+    void reloadResources() {
+        final Resources res = mService.mContext.getResources();
+        final Size defaultSizeDp = Size.parseSize(res.getString(
+                com.android.internal.R.string.config_defaultPictureInPictureSize));
+        final Size screenEdgeInsetsDp = Size.parseSize(res.getString(
+                com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
+        mDefaultStackGravity = res.getInteger(
+                com.android.internal.R.integer.config_defaultPictureInPictureGravity);
+        mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
+        mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics),
+                dpToPx(defaultSizeDp.getHeight(), mTmpMetrics));
+        mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
+                dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
+    }
+
+    /**
+     * Registers a pinned stack listener.
+     */
+    void registerPinnedStackListener(IPinnedStackListener listener) {
+        try {
+            listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
+            listener.onListenerRegistered(mCallbacks);
+            mPinnedStackListener = listener;
+            notifyBoundsChanged(mIsImeShowing);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to register pinned stack listener", e);
+        }
+    }
+
+    /**
+     * @return the default bounds to show the PIP when there is no active PIP.
+     */
+    Rect getDefaultBounds() {
+        final Display display = mDisplayContent.getDisplay();
+        final Rect insetBounds = new Rect();
+        final Point displaySize = new Point();
+        display.getRealSize(displaySize);
+        mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mTmpInsets);
+        getInsetBounds(displaySize, mTmpInsets, insetBounds);
+
+        final Rect defaultBounds = new Rect();
+        Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(),
+                mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds);
+        return defaultBounds;
+    }
+
+    /**
+     * @return the movement bounds for the given {@param stackBounds} and the current state of the
+     *         controller.
+     */
+    Rect getMovementBounds(Rect stackBounds) {
+        final Display display = mDisplayContent.getDisplay();
+        final Rect movementBounds = new Rect();
+        final Point displaySize = new Point();
+        display.getRealSize(displaySize);
+        mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mTmpInsets);
+        getInsetBounds(displaySize, mTmpInsets, movementBounds);
+
+        // Adjust the right/bottom to ensure the stack bounds never goes offscreen
+        movementBounds.right = Math.max(movementBounds.left, movementBounds.right -
+                stackBounds.width());
+        movementBounds.bottom = Math.max(movementBounds.top, movementBounds.bottom -
+                stackBounds.height());
+
+        // Adjust the top if the ime is open
+        if (mIsImeShowing) {
+            movementBounds.bottom -= mImeHeight;
+        }
+
+        return movementBounds;
+    }
+
+    /**
+     * @return the PIP bounds given it's bounds pre-rotation, and post-rotation (with as applied
+     * by the display content, which currently transposes the dimensions but keeps each stack in
+     * the same physical space on the device).
+     */
+    Rect getPostRotationBounds(Rect preRotationStackBounds, Rect postRotationStackBounds) {
+        // Keep the pinned stack in the same aspect ratio as in the old orientation, but
+        // move it into the position in the rotated space, and snap to the closest space
+        // in the new orientation.
+        final Rect movementBounds = getMovementBounds(preRotationStackBounds);
+        final int stackWidth = preRotationStackBounds.width();
+        final int stackHeight = preRotationStackBounds.height();
+        final int left = postRotationStackBounds.centerX() - (stackWidth / 2);
+        final int top = postRotationStackBounds.centerY() - (stackHeight / 2);
+        final Rect postRotBounds = new Rect(left, top, left + stackWidth, top + stackHeight);
+        return mSnapAlgorithm.findClosestSnapBounds(movementBounds, postRotBounds);
+    }
+
+    /**
+     * Sets the Ime state and height.
+     */
+    void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
+        // Return early if there is no state change
+        if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
+            return;
+        }
+
+        final Rect stackBounds = new Rect();
+        mService.getStackBounds(PINNED_STACK_ID, stackBounds);
+        final Rect prevMovementBounds = getMovementBounds(stackBounds);
+        final boolean wasAdjustedForIme = mIsImeShowing;
+        mIsImeShowing = adjustedForIme;
+        mImeHeight = imeHeight;
+        if (mInInteractiveMode) {
+            // If the user is currently interacting with the PIP and the ime state changes, then
+            // don't adjust the bounds and defer that to after the interaction
+            notifyBoundsChanged(adjustedForIme /* adjustedForIme */);
+        } else {
+            // Otherwise, we can move the PIP to a sane location to ensure that it does not block
+            // the user from interacting with the IME
+            Rect toBounds;
+            if (!wasAdjustedForIme && adjustedForIme) {
+                // If we are showing the IME, then store the previous bounds
+                mPreImeShowingBounds.set(stackBounds);
+                toBounds = adjustBoundsInMovementBounds(stackBounds);
+            } else if (wasAdjustedForIme && !adjustedForIme) {
+                if (!mPreImeShowingBounds.isEmpty()) {
+                    // If we are hiding the IME and the user is not interacting with the PIP, restore
+                    // the previous bounds
+                    toBounds = mPreImeShowingBounds;
+                } else {
+                    if (stackBounds.top == prevMovementBounds.bottom) {
+                        // If the PIP is resting on top of the IME, then adjust it with the hiding
+                        // of the IME
+                        final Rect movementBounds = getMovementBounds(stackBounds);
+                        toBounds = new Rect(stackBounds);
+                        toBounds.offsetTo(toBounds.left, movementBounds.bottom);
+                    } else {
+                        // Otherwise, leave the PIP in place
+                        toBounds = stackBounds;
+                    }
+                }
+            } else {
+                // Otherwise, the IME bounds have changed so we need to adjust the PIP bounds also
+                toBounds = adjustBoundsInMovementBounds(stackBounds);
+            }
+            if (!toBounds.equals(stackBounds)) {
+                if (mBoundsAnimator != null) {
+                    mBoundsAnimator.cancel();
+                }
+                mBoundsAnimator = mMotionHelper.createAnimationToBounds(stackBounds, toBounds);
+                mBoundsAnimator.start();
+            }
+        }
+    }
+
+    /**
+     * @return the adjusted {@param stackBounds} such that they are in the movement bounds.
+     */
+    private Rect adjustBoundsInMovementBounds(Rect stackBounds) {
+        final Rect movementBounds = getMovementBounds(stackBounds);
+        final Rect adjustedBounds = new Rect(stackBounds);
+        adjustedBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top));
+        return adjustedBounds;
+    }
+
+    /**
+     * Sends a broadcast that the PIP movement bounds have changed.
+     */
+    private void notifyBoundsChanged(boolean adjustedForIme) {
+        if (mPinnedStackListener != null) {
+            try {
+                mPinnedStackListener.onBoundsChanged(adjustedForIme);
+            } catch (RemoteException e) {
+                Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
+            }
+        }
+    }
+
+    /**
+     * @return the bounds on the screen that the PIP can be visible in.
+     */
+    private void getInsetBounds(Point displaySize, Rect insets, Rect outRect) {
+        outRect.set(insets.left + mScreenEdgeInsets.x, insets.top + mScreenEdgeInsets.y,
+                displaySize.x - insets.right - mScreenEdgeInsets.x,
+                displaySize.y - insets.bottom - mScreenEdgeInsets.y);
+    }
+
+    /**
+     * @return the pixels for a given dp value.
+     */
+    private int dpToPx(float dpValue, DisplayMetrics dm) {
+        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
+    }
+
+    void dump(String prefix, PrintWriter pw) {
+        pw.println(prefix + "PinnedStackController");
+        pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
+        pw.println(prefix + "  mInInteractiveMode=" + mInInteractiveMode);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 5637e51..4d8f29d 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -387,24 +387,8 @@
         mDisplayContent.rotateBounds(mRotation, newRotation, mTmpRect2);
         switch (mStackId) {
             case PINNED_STACK_ID:
-                // Keep the pinned stack in the same aspect ratio as in the old orientation, but
-                // move it into the position in the rotated space, and snap to the closest space
-                // in the new orientation.
-
-                try {
-                    final IActivityManager am = mService.mActivityManager;
-                    final Rect movementBounds = am.getPictureInPictureMovementBounds(
-                            mDisplayContent.getDisplayId());
-                    final int width = mBounds.width();
-                    final int height = mBounds.height();
-                    final int left = mTmpRect2.centerX() - (width / 2);
-                    final int top = mTmpRect2.centerY() - (height / 2);
-                    mTmpRect2.set(left, top, left + width, top + height);
-
-                    final PipSnapAlgorithm snapAlgorithm = new PipSnapAlgorithm(mService.mContext,
-                            mDisplayContent.getDisplayId());
-                    mTmpRect2.set(snapAlgorithm.findClosestSnapBounds(movementBounds, mTmpRect2));
-                } catch (RemoteException e) {}
+                mTmpRect2 = mDisplayContent.getPinnedStackController().getPostRotationBounds(
+                        mBounds, mTmpRect2);
                 break;
             case DOCKED_STACK_ID:
                 repositionDockedStackAfterRotation(mTmpRect2);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 1b08f16..70b0201 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -97,6 +97,7 @@
 import android.view.IDockedStackListener;
 import android.view.IInputFilter;
 import android.view.IOnKeyguardExitResult;
+import android.view.IPinnedStackListener;
 import android.view.IRotationWatcher;
 import android.view.IWindow;
 import android.view.IWindowId;
@@ -166,12 +167,14 @@
 import java.util.List;
 
 import static android.Manifest.permission.MANAGE_APP_TOKENS;
+import static android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS;
 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
 import static android.app.StatusBarManager.DISABLE_MASK;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.DOCKED_INVALID;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
@@ -529,6 +532,7 @@
     private final SparseIntArray mTmpTaskIds = new SparseIntArray();
 
     boolean mForceResizableTasks = false;
+    boolean mSupportsPictureInPicture = false;
 
     int getDragLayerLocked() {
         return mPolicy.windowTypeToLayerLw(TYPE_DRAG) * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
@@ -3388,6 +3392,36 @@
         mDockedStackCreateBounds = bounds;
     }
 
+    @Override
+    public Rect getPictureInPictureDefaultBounds(int displayId) {
+        synchronized (mWindowMap) {
+            if (!mSupportsPictureInPicture) {
+                return new Rect();
+            }
+
+            final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+            return displayContent.getPinnedStackController().getDefaultBounds();
+        }
+    }
+
+    @Override
+    public Rect getPictureInPictureMovementBounds(int displayId) {
+        synchronized (mWindowMap) {
+            if (!mSupportsPictureInPicture) {
+                return new Rect();
+            }
+
+            final Rect stackBounds = new Rect();
+            getStackBounds(PINNED_STACK_ID, stackBounds);
+            if (stackBounds.isEmpty()) {
+                return stackBounds;
+            }
+
+            final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+            return displayContent.getPinnedStackController().getMovementBounds(stackBounds);
+        }
+    }
+
     /**
      * Create a new TaskStack and place it on a DisplayContent.
      * @param stackId The unique identifier of the new stack.
@@ -8331,6 +8365,7 @@
                 pw.println("    a[animator]: animator state");
                 pw.println("    s[essions]: active sessions");
                 pw.println("    surfaces: active surfaces (debugging enabled only)");
+                pw.println("    pip: PIP state");
                 pw.println("    d[isplays]: active display contents");
                 pw.println("    t[okens]: token list");
                 pw.println("    w[indows]: window list");
@@ -8403,6 +8438,18 @@
                     pw.println(output.toString());
                 }
                 return;
+            } else if ("pip".equals(cmd)) {
+                synchronized(mWindowMap) {
+                    pw.print("defaultBounds=");
+                    getPictureInPictureDefaultBounds(DEFAULT_DISPLAY).printShortString(pw);
+                    pw.println();
+                    pw.print("movementBounds=");
+                    getPictureInPictureMovementBounds(DEFAULT_DISPLAY).printShortString(pw);
+                    pw.println();
+                    getDefaultDisplayContentLocked().getPinnedStackController().dump("", pw);
+                    pw.println();
+                }
+                return;
             } else {
                 // Dumping a single name?
                 if (!dumpWindows(pw, cmd, args, opti, dumpAll)) {
@@ -8677,13 +8724,19 @@
         }
     }
 
+    public void setSupportsPictureInPicture(boolean supportsPictureInPicture) {
+        synchronized (mWindowMap) {
+            mSupportsPictureInPicture = supportsPictureInPicture;
+        }
+    }
+
     static int dipToPixel(int dip, DisplayMetrics displayMetrics) {
         return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
     }
 
     @Override
     public void registerDockedStackListener(IDockedStackListener listener) {
-        if (!checkCallingPermission(android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS,
+        if (!checkCallingPermission(REGISTER_WINDOW_MANAGER_LISTENERS,
                 "registerDockedStackListener()")) {
             return;
         }
@@ -8693,6 +8746,21 @@
     }
 
     @Override
+    public void registerPinnedStackListener(int displayId, IPinnedStackListener listener) {
+        if (!checkCallingPermission(REGISTER_WINDOW_MANAGER_LISTENERS,
+                "registerPinnedStackListener()")) {
+            return;
+        }
+        if (!mSupportsPictureInPicture) {
+            return;
+        }
+        synchronized (mWindowMap) {
+            final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+            displayContent.getPinnedStackController().registerPinnedStackListener(listener);
+        }
+    }
+
+    @Override
     public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
         try {
             WindowState focusedWindow = getFocusedWindow();
@@ -8865,8 +8933,7 @@
     @Override
     public void registerShortcutKey(long shortcutCode, IShortcutService shortcutKeyReceiver)
             throws RemoteException {
-        if (!checkCallingPermission(Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS,
-                "registerShortcutKey")) {
+        if (!checkCallingPermission(REGISTER_WINDOW_MANAGER_LISTENERS, "registerShortcutKey")) {
             throw new SecurityException(
                     "Requires REGISTER_WINDOW_MANAGER_LISTENERS permission");
         }
diff --git a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java
index db5b119..9ec546e 100644
--- a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java
+++ b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java
@@ -591,6 +591,20 @@
     }
 
     @Override
+    public void registerPinnedStackListener(int displayId, IPinnedStackListener listener) throws RemoteException {
+    }
+
+    @Override
+    public Rect getPictureInPictureDefaultBounds(int displayId) {
+        return null;
+    }
+
+    @Override
+    public Rect getPictureInPictureMovementBounds(int displayId)  {
+        return null;
+    }
+
+    @Override
     public void setResizeDimLayer(boolean visible, int targetStackId, float alpha)
             throws RemoteException {
     }