Adding API for apps to specify their aspect ratio when entering PIP.

Test: android.server.cts.ActivityManagerPinnedStackTests
Test: #testEnterPipAspectRatio
Test: #testEnterPipExtremeAspectRatios

Change-Id: I9efba942b9a6451dec07428fe1e428ef4a896867
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 42d16fb..989977a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -21,6 +21,7 @@
 import android.app.ContentProviderHolder;
 import android.app.IActivityManager;
 import android.app.WaitResult;
+import android.graphics.PointF;
 import android.os.IDeviceIdentifiersPolicyService;
 import com.android.internal.telephony.TelephonyIntents;
 import com.google.android.collect.Lists;
@@ -293,7 +294,6 @@
 import static android.provider.Settings.Global.LENIENT_BACKGROUND_CHECK;
 import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
 import static android.provider.Settings.System.FONT_SCALE;
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.internal.util.XmlUtils.readBooleanAttribute;
@@ -1575,6 +1575,10 @@
     int mThumbnailHeight;
     float mFullscreenThumbnailScale;
 
+    /** The aspect ratio bounds of the PIP. */
+    float mMinPipAspectRatio;
+    float mMaxPipAspectRatio;
+
     final ServiceThread mHandlerThread;
     final MainHandler mHandler;
     final UiHandler mUiHandler;
@@ -7469,6 +7473,15 @@
 
     @Override
     public void enterPictureInPictureMode(IBinder token) {
+        enterPictureInPictureMode(token, DEFAULT_DISPLAY, null /* aspectRatio */);
+    }
+
+    @Override
+    public void enterPictureInPictureModeWithAspectRatio(IBinder token, float aspectRatio) {
+        enterPictureInPictureMode(token, DEFAULT_DISPLAY, aspectRatio);
+    }
+
+    public void enterPictureInPictureMode(IBinder token, int displayId, Float aspectRatio) {
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized(this) {
@@ -7478,7 +7491,6 @@
                 }
 
                 final ActivityRecord r = ActivityRecord.forTokenLocked(token);
-
                 if (r == null) {
                     throw new IllegalStateException("enterPictureInPictureMode: "
                             + "Can't find activity for token=" + token);
@@ -7489,21 +7501,55 @@
                             + "Picture-In-Picture not supported for r=" + r);
                 }
 
-                // Use the default launch bounds for pinned stack if it doesn't exist yet or use the
-                // current bounds.
-                final ActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID);
-                final Rect bounds = (pinnedStack != null)
-                        ? pinnedStack.mBounds
-                        : mWindowManager.getPictureInPictureDefaultBounds(DEFAULT_DISPLAY);
+                if (aspectRatio != null && !isValidPictureInPictureAspectRatio(aspectRatio)) {
+                    throw new IllegalArgumentException(String.format("enterPictureInPictureMode: "
+                            + "Aspect ratio is too extreme (must be between %f and %f).",
+                                    mMinPipAspectRatio, mMaxPipAspectRatio));
+                }
 
-                mStackSupervisor.moveActivityToPinnedStackLocked(
-                        r, "enterPictureInPictureMode", bounds);
+                final Rect bounds = isValidPictureInPictureAspectRatio(aspectRatio)
+                        ? mWindowManager.getPictureInPictureBounds(displayId, aspectRatio)
+                        : mWindowManager.getPictureInPictureDefaultBounds(displayId);
+                mStackSupervisor.moveActivityToPinnedStackLocked(r, "enterPictureInPictureMode",
+                        bounds);
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
         }
     }
 
+    @Override
+    public void setPictureInPictureAspectRatio(IBinder token, float aspectRatio) {
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized(this) {
+                final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+                if (r == null || r.getStack().mStackId != PINNED_STACK_ID) {
+                    throw new IllegalStateException("setPictureInPictureAspectRatio: "
+                            + "Requesting activity must be in picture-in-picture mode.");
+                }
+
+                if (!isValidPictureInPictureAspectRatio(aspectRatio)) {
+                    throw new IllegalArgumentException(String.format(
+                            "setPictureInPictureAspectRatio: Aspect ratio is too extreme (must be "
+                                    + "between %f and %f).", mMinPipAspectRatio,
+                            mMaxPipAspectRatio));
+                }
+
+                mWindowManager.setPictureInPictureAspectRatio(aspectRatio);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    private boolean isValidPictureInPictureAspectRatio(Float aspectRatio) {
+        if (aspectRatio == null) {
+            return false;
+        }
+        return mMinPipAspectRatio <= aspectRatio && aspectRatio <= mMaxPipAspectRatio;
+    }
+
     // =========================================================
     // PROCESS INFO
     // =========================================================
@@ -13035,6 +13081,10 @@
                     com.android.internal.R.dimen.thumbnail_width);
             mThumbnailHeight = res.getDimensionPixelSize(
                     com.android.internal.R.dimen.thumbnail_height);
+            mMinPipAspectRatio = res.getFloat(
+                    com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
+            mMaxPipAspectRatio = res.getFloat(
+                    com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
             mAppErrors.loadAppsNotReportingCrashesFromConfigLocked(res.getString(
                     com.android.internal.R.string.config_appsNotReportingCrashes));
             mUserController.mUserSwitchUiEnabled = !res.getBoolean(
diff --git a/services/core/java/com/android/server/wm/BoundsAnimationController.java b/services/core/java/com/android/server/wm/BoundsAnimationController.java
index 5bfece4..cf5cecda 100644
--- a/services/core/java/com/android/server/wm/BoundsAnimationController.java
+++ b/services/core/java/com/android/server/wm/BoundsAnimationController.java
@@ -96,8 +96,8 @@
     private final class BoundsAnimator extends ValueAnimator
             implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
         private final AnimateBoundsUser mTarget;
-        private final Rect mFrom;
-        private final Rect mTo;
+        private final Rect mFrom = new Rect();
+        private final Rect mTo = new Rect();
         private final Rect mTmpRect = new Rect();
         private final Rect mTmpTaskBounds = new Rect();
         private final boolean mMoveToFullScreen;
@@ -117,8 +117,8 @@
                 boolean moveToFullScreen, boolean replacement) {
             super();
             mTarget = target;
-            mFrom = from;
-            mTo = to;
+            mFrom.set(from);
+            mTo.set(to);
             mMoveToFullScreen = moveToFullScreen;
             mReplacement = replacement;
             addUpdateListener(this);
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
index 1ccf722..52c18f1 100644
--- a/services/core/java/com/android/server/wm/PinnedStackController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -26,6 +26,7 @@
 import android.animation.ValueAnimator;
 import android.content.res.Resources;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.IBinder;
@@ -167,6 +168,25 @@
     }
 
     /**
+     * Returns the current bounds (or the default bounds if there are no current bounds) with the
+     * specified aspect ratio.
+     */
+    Rect getAspectRatioBounds(Rect stackBounds, float aspectRatio) {
+        // Save the snap fraction, calculate the aspect ratio based on the current bounds
+        final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
+                getMovementBounds(stackBounds));
+        final float radius = PointF.length(stackBounds.width(), stackBounds.height());
+        final int height = (int) Math.round(Math.sqrt((radius * radius) /
+                (aspectRatio * aspectRatio + 1)));
+        final int width = Math.round(height * aspectRatio);
+        final int left = (int) (stackBounds.centerX() - width / 2f);
+        final int top = (int) (stackBounds.centerY() - height / 2f);
+        stackBounds.set(left, top, left + width, top + height);
+        mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
+        return stackBounds;
+    }
+
+    /**
      * @return the default bounds to show the PIP when there is no active PIP.
      */
     Rect getDefaultBounds() {
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index a0270c6..203ba72 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -132,6 +132,7 @@
     // perfectly fit the region it would have been cropped to. We may also avoid certain logic we
     // would otherwise apply while resizing, while resizing in the bounds animating mode.
     private boolean mBoundsAnimating = false;
+    private Rect mBoundsAnimationTarget = new Rect();
 
     // Temporary storage for the new bounds that should be used after the configuration change.
     // Will be cleared once the client retrieves the new bounds via getBoundsForNewConfiguration().
@@ -329,6 +330,30 @@
         mDisplayContent.getLogicalDisplayRect(out);
     }
 
+    /**
+     * Sets the bounds animation target bounds.  This can't currently be done in onAnimationStart()
+     * since that is started on the UiThread.
+     */
+    void setAnimatingBounds(Rect bounds) {
+        if (bounds != null) {
+            mBoundsAnimationTarget.set(bounds);
+        } else {
+            mBoundsAnimationTarget.setEmpty();
+        }
+    }
+
+    /**
+     * @return the bounds that the task stack is currently being animated towards, or the current
+     *         stack bounds if there is no animation in progress.
+     */
+    void getAnimatingBounds(Rect outBounds) {
+        if (!mBoundsAnimationTarget.isEmpty()) {
+            outBounds.set(mBoundsAnimationTarget);
+            return;
+        }
+        getBounds(outBounds);
+    }
+
     /** Bounds of the stack with other system factors taken into consideration. */
     @Override
     public void getDimBounds(Rect out) {
@@ -1391,6 +1416,7 @@
     public void onAnimationEnd() {
         synchronized (mService.mWindowMap) {
             mBoundsAnimating = false;
+            mBoundsAnimationTarget.setEmpty();
             mService.requestTraversal();
         }
         if (mStackId == PINNED_STACK_ID) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a533d84..8be87a1 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -172,7 +172,6 @@
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
 import static android.app.StatusBarManager.DISABLE_MASK;
 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;
@@ -3404,7 +3403,7 @@
     public Rect getPictureInPictureDefaultBounds(int displayId) {
         synchronized (mWindowMap) {
             if (!mSupportsPictureInPicture) {
-                return new Rect();
+                return null;
             }
 
             final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
@@ -3416,7 +3415,7 @@
     public Rect getPictureInPictureMovementBounds(int displayId) {
         synchronized (mWindowMap) {
             if (!mSupportsPictureInPicture) {
-                return new Rect();
+                return null;
             }
 
             final Rect stackBounds = new Rect();
@@ -3430,6 +3429,47 @@
         }
     }
 
+    public void setPictureInPictureAspectRatio(float aspectRatio) {
+        synchronized (mWindowMap) {
+            if (!mSupportsPictureInPicture) {
+                return;
+            }
+
+            final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID);
+            if (stack == null) {
+                return;
+            }
+
+            animateResizePinnedStack(getPictureInPictureBounds(
+                    stack.getDisplayContent().getDisplayId(), aspectRatio), -1);
+        }
+    }
+
+    public Rect getPictureInPictureBounds(int displayId, float aspectRatio) {
+        synchronized (mWindowMap) {
+            if (!mSupportsPictureInPicture) {
+                return null;
+            }
+
+            final Rect stackBounds;
+            final DisplayContent displayContent;
+            final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID);
+            if (stack != null) {
+                // If the stack exists, then use its final bounds to calculate the new aspect ratio
+                // bounds.
+                displayContent = stack.getDisplayContent();
+                stackBounds = new Rect();
+                stack.getAnimatingBounds(stackBounds);
+            } else {
+                // Otherwise, just calculate the aspect ratio bounds from the default bounds
+                displayContent = mRoot.getDisplayContent(displayId);
+                stackBounds = displayContent.getPinnedStackController().getDefaultBounds();
+            }
+            return displayContent.getPinnedStackController().getAspectRatioBounds(stackBounds,
+                    aspectRatio);
+        }
+    }
+
     /**
      * Place a TaskStack on a DisplayContent. Will create a new TaskStack if none is found with
      * specified stackId.
@@ -8456,6 +8496,7 @@
             }
             final Rect originalBounds = new Rect();
             stack.getBounds(originalBounds);
+            stack.setAnimatingBounds(bounds);
             UiThread.getHandler().post(new Runnable() {
                 @Override
                 public void run() {