Creating PinnedStackController.

- Creating a PinnedStackController to keep track of the state of the PIP
  to prevent changes in the system (ie. IME showing) and user interaction
  from clobbering each other.
- Refactoring calls in AM into WM/controller

Test: android.server.cts.ActivityManagerPinnedStackTests

Change-Id: Ie59dfd45d5c54764ba69a589b3b8148845e92cc3
Signed-off-by: Winson Chung <winsonc@google.com>
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");
         }