Adding ability for an app to request auto-enter picture-in-picture.

- If an activity requests that it can auto-enter PIP, then we will trigger
  it to enter PIP when the task is effectively being occluded.  This does
  not affect the activity when the screen is locking, or if it starts new
  activities within its own task, or if it finishes itself, or if there is
  already a PIP activity.
- Changed setPictureInPictureAspectRatio to also specify the aspect ratio
  to use when auto-entering PIP.  If the activity is not PIP'ed and has
  not requested auto-enter, then the call continues to fail.

Test: android.server.cts.ActivityManagerPinnedStackTests
Test: #testAutoEnterPictureInPicture
Test: #testAutoEnterPictureInPictureLaunchActivity
Test: #testAutoEnterPictureInPictureFinish
Test: #testAutoEnterPictureInPictureAspectRatio
Test: #testAutoEnterPictureInPictureOverPip

Change-Id: I6477b6d1f160cf0219d935123bbb505f57ee7a56
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 982c57c..625df23 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -269,10 +269,8 @@
 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
-import static android.app.ActivityManager.StackId.HOME_STACK_ID;
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
-import static android.app.ActivityManager.StackId.RECENTS_STACK_ID;
 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
 import static android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY;
 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
@@ -7494,46 +7492,51 @@
 
     @Override
     public void enterPictureInPictureMode(IBinder token) {
-        enterPictureInPictureMode(token, DEFAULT_DISPLAY, null /* aspectRatio */);
+        enterPictureInPictureMode(token, DEFAULT_DISPLAY, -1f /* aspectRatio */,
+                false /* checkAspectRatio */);
     }
 
     @Override
     public void enterPictureInPictureModeWithAspectRatio(IBinder token, float aspectRatio) {
-        enterPictureInPictureMode(token, DEFAULT_DISPLAY, aspectRatio);
+        enterPictureInPictureMode(token, DEFAULT_DISPLAY, aspectRatio, true /* checkAspectRatio */);
     }
 
-    public void enterPictureInPictureMode(IBinder token, int displayId, Float aspectRatio) {
+    private void enterPictureInPictureMode(IBinder token, int displayId, float aspectRatio,
+            boolean checkAspectRatio) {
         final long origId = Binder.clearCallingIdentity();
         try {
             synchronized(this) {
-                if (!mSupportsPictureInPicture) {
-                    throw new IllegalStateException("enterPictureInPictureMode: "
-                            + "Device doesn't support picture-in-picture mode.");
-                }
+                final ActivityRecord r = ensureValidPictureInPictureActivityLocked(
+                        "enterPictureInPictureMode", token, aspectRatio, checkAspectRatio,
+                        true /* checkActivityVisibility */);
 
-                final ActivityRecord r = ActivityRecord.forTokenLocked(token);
-                if (r == null) {
-                    throw new IllegalStateException("enterPictureInPictureMode: "
-                            + "Can't find activity for token=" + token);
-                }
+                enterPictureInPictureModeLocked(r, displayId, aspectRatio,
+                        true /* moveHomeStackToFront */, "enterPictureInPictureMode");
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
 
-                if (!r.canEnterPictureInPicture()) {
-                    throw new IllegalArgumentException("enterPictureInPictureMode: "
-                            + "Current activity does not support picture-in-picture or is not "
-                            + "visible r=" + r);
-                }
+    void enterPictureInPictureModeLocked(ActivityRecord r, int displayId, float aspectRatio,
+            boolean moveHomeStackToFront, String reason) {
+        final Rect bounds = isValidPictureInPictureAspectRatio(aspectRatio)
+                ? mWindowManager.getPictureInPictureBounds(displayId, aspectRatio)
+                : mWindowManager.getPictureInPictureDefaultBounds(displayId);
+        mStackSupervisor.moveActivityToPinnedStackLocked(r, reason, bounds, moveHomeStackToFront);
+    }
 
-                if (aspectRatio != null && !isValidPictureInPictureAspectRatio(aspectRatio)) {
-                    throw new IllegalArgumentException(String.format("enterPictureInPictureMode: "
-                            + "Aspect ratio is too extreme (must be between %f and %f).",
-                                    mMinPipAspectRatio, mMaxPipAspectRatio));
-                }
+    @Override
+    public void enterPictureInPictureModeOnMoveToBackground(IBinder token,
+            boolean enterPictureInPictureOnMoveToBg) {
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized(this) {
+                final ActivityRecord r = ensureValidPictureInPictureActivityLocked(
+                        "requestAutoEnterPictureInPicture", token, -1f /* aspectRatio */,
+                        false /* checkAspectRatio */, false /* checkActivityVisibility */);
 
-                final Rect bounds = isValidPictureInPictureAspectRatio(aspectRatio)
-                        ? mWindowManager.getPictureInPictureBounds(displayId, aspectRatio)
-                        : mWindowManager.getPictureInPictureDefaultBounds(displayId);
-                mStackSupervisor.moveActivityToPinnedStackLocked(r, "enterPictureInPictureMode",
-                        bounds);
+                r.supportsPipOnMoveToBackground = enterPictureInPictureOnMoveToBg;
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -7545,33 +7548,68 @@
         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.");
-                }
+                final ActivityRecord r = ensureValidPictureInPictureActivityLocked(
+                        "setPictureInPictureAspectRatio", token, aspectRatio,
+                        true /* checkAspectRatio */, false /* checkActivityVisibility */);
 
-                if (!isValidPictureInPictureAspectRatio(aspectRatio)) {
-                    throw new IllegalArgumentException(String.format(
-                            "setPictureInPictureAspectRatio: Aspect ratio is too extreme (must be "
-                                    + "between %f and %f).", mMinPipAspectRatio,
-                            mMaxPipAspectRatio));
+                if (r.getStack().getStackId() == PINNED_STACK_ID) {
+                    // If the activity is already in picture-in-picture, update the pinned stack now
+                    mWindowManager.setPictureInPictureAspectRatio(aspectRatio);
                 }
-
-                mWindowManager.setPictureInPictureAspectRatio(aspectRatio);
+                r.pictureInPictureAspectRatio = aspectRatio;
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
         }
     }
 
-    private boolean isValidPictureInPictureAspectRatio(Float aspectRatio) {
-        if (aspectRatio == null) {
-            return false;
-        }
+    private boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
         return mMinPipAspectRatio <= aspectRatio && aspectRatio <= mMaxPipAspectRatio;
     }
 
+    /**
+     * Checks the state of the system and the activity associated with the given {@param token} to
+     * verify that picture-in-picture is supported for that activity.
+     *
+     * @param checkAspectRatio whether or not to check {@param aspectRatio} is within a valid range
+     * @param checkActivityVisibility whether or not to enforce that the activity is currently
+     *                                visible
+     *
+     * @return the activity record for the given {@param token} if all the checks pass.
+     */
+    private ActivityRecord ensureValidPictureInPictureActivityLocked(String caller, IBinder token,
+            float aspectRatio, boolean checkAspectRatio, boolean checkActivityVisibility) {
+        if (!mSupportsPictureInPicture) {
+            throw new IllegalStateException(caller
+                    + ": Device doesn't support picture-in-picture mode.");
+        }
+
+        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+        if (r == null) {
+            throw new IllegalStateException(caller
+                    + ": Can't find activity for token=" + token);
+        }
+
+        if (!r.canEnterPictureInPicture(checkActivityVisibility)) {
+            throw new IllegalArgumentException(caller
+                    + "Current activity does not support picture-in-picture or is not "
+                    + "visible r=" + r);
+        }
+
+        if (r.getStack().isHomeStack()) {
+            throw new IllegalStateException(caller
+                    + ": Activities on the home stack not supported");
+        }
+
+        if (checkAspectRatio && !isValidPictureInPictureAspectRatio(aspectRatio)) {
+            throw new IllegalArgumentException(String.format(caller
+                    + ": Aspect ratio is too extreme (must be between %f and %f).",
+                            mMinPipAspectRatio, mMaxPipAspectRatio));
+        }
+
+        return r;
+    }
+
     // =========================================================
     // PROCESS INFO
     // =========================================================
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index 6d72228..13c422b 100644
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -21,7 +21,6 @@
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.HOME_STACK_ID;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
-import static android.app.ActivityManager.StackId.RECENTS_STACK_ID;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
@@ -84,7 +83,6 @@
 import android.util.TimeUtils;
 import android.view.AppTransitionAnimationSpec;
 import android.view.IApplicationToken;
-import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 
 import com.android.internal.app.ResolverActivity;
@@ -218,6 +216,14 @@
     boolean frozenBeforeDestroy;// has been frozen but not yet destroyed.
     boolean immersive;      // immersive mode (don't interrupt if possible)
     boolean forceNewConfig; // force re-create with new config next time
+    boolean supportsPipOnMoveToBackground;   // Supports automatically entering picture-in-picture
+            // when this activity is hidden. This flag is requested by the activity.
+    private boolean enterPipOnMoveToBackground; // Flag to enter picture in picture when this
+            // activity is made invisible. This flag is set specifically when another task is being
+            // launched or moved to the front which may cause this activity to try and enter PiP
+            // when it is next made invisible.
+    float pictureInPictureAspectRatio; // The aspect ratio to use when auto-entering
+            // picture-in-picture
     int launchCount;        // count of launches since last state
     long lastLaunchTime;    // time of last launch of this activity
     ComponentName requestedVrComponent; // the requested component for handling VR mode.
@@ -434,6 +440,11 @@
         if (info != null) {
             pw.println(prefix + "resizeMode=" + ActivityInfo.resizeModeToString(info.resizeMode));
         }
+        if (supportsPipOnMoveToBackground) {
+            pw.println(prefix + "supportsPipOnMoveToBackground=1 "
+                    + "enterPipOnMoveToBackground="
+                            + (enterPipOnMoveToBackground ? 1 : 0));
+        }
     }
 
     private boolean crossesHorizontalSizeThreshold(int firstDp, int secondDp) {
@@ -842,6 +853,23 @@
     }
 
     /**
+     * If this activity has requested that it auto-enter picture-in-picture and we can actually do
+     * this, then mark it to enter picture in picture at that point.
+     */
+    void setEnterPipOnMoveToBackground(boolean enterPipOnInvisible) {
+        if (supportsPipOnMoveToBackground) {
+            enterPipOnMoveToBackground = enterPipOnInvisible;
+        }
+    }
+
+    /**
+     * @return whether to enter PiP when this activity is made invisible.
+     */
+    public boolean shouldEnterPictureInPictureOnInvisible() {
+        return enterPipOnMoveToBackground;
+    }
+
+    /**
      * @return Stack value from current task, null if there is no task.
      */
     ActivityStack getStack() {
@@ -930,9 +958,15 @@
     }
 
     /**
-     * @return whether this activity is currently allowed to enter PIP.
+     * @return whether this activity is currently allowed to enter PIP, if
+     * {@param checkActivityVisibility} is set, then the current activity visibility is taken into
+     * account.
      */
-    boolean canEnterPictureInPicture() {
+    boolean canEnterPictureInPicture(boolean checkActivityVisibility) {
+        if (!checkActivityVisibility) {
+            return supportsPictureInPicture();
+        }
+
         if (supportsPictureInPicture() && visible) {
             switch (state) {
                 case RESUMED:
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index d94d3cd..d160a46 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -22,7 +22,6 @@
 import static android.app.ActivityManager.StackId.HOME_STACK_ID;
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
-import static android.app.ActivityManager.StackId.RECENTS_STACK_ID;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.FLAG_RESUME_WHILE_PAUSING;
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
@@ -723,7 +722,6 @@
 
         mStacks.remove(this);
         int addIndex = mStacks.size();
-
         if (addIndex > 0) {
             final ActivityStack topStack = mStacks.get(addIndex - 1);
             if (StackId.isAlwaysOnTop(topStack.mStackId) && topStack != this) {
@@ -1718,7 +1716,9 @@
                                 + stackInvisible + " behindFullscreenActivity="
                                 + behindFullscreenActivity + " mLaunchTaskBehind="
                                 + r.mLaunchTaskBehind);
-                        makeInvisible(r, visibleBehind);
+                        if (!enterPictureInPictureOnActivityInvisible(r)) {
+                            makeInvisible(r, visibleBehind);
+                        }
                     }
                 }
                 if (mStackId == FREEFORM_WORKSPACE_STACK_ID) {
@@ -1876,6 +1876,32 @@
         return false;
     }
 
+    /**
+     * Attempts to enter picture-in-picture if the activity that is being made invisible supports
+     * it.  If not, then
+     *
+     * @return whether or not picture-in-picture mode was entered.
+     */
+    private boolean enterPictureInPictureOnActivityInvisible(ActivityRecord r) {
+        final boolean hasPinnedStack =
+                mStackSupervisor.getStack(PINNED_STACK_ID) != null;
+        if (DEBUG_VISIBILITY) Slog.v(TAG_VISIBILITY, " enterPictureInPictureOnInvisible="
+                + r.shouldEnterPictureInPictureOnInvisible()
+                + " hasPinnedStack=" + hasPinnedStack);
+        if (!hasPinnedStack && r.visible && r.shouldEnterPictureInPictureOnInvisible()) {
+            r.setEnterPipOnMoveToBackground(false);
+
+            // Enter picture in picture, but don't move the home stack to the front
+            // since it will affect the focused stack's visibility and occlude
+            // starting activities
+            mService.enterPictureInPictureModeLocked(r, r.getDisplayId(),
+                    r.pictureInPictureAspectRatio, false /* moveHomeStackToFront */,
+                    "ensureActivitiesVisibleLocked");
+            return true;
+        }
+        return false;
+    }
+
     private void makeInvisible(ActivityRecord r, ActivityRecord visibleBehind) {
         if (!r.visible) {
             if (DEBUG_VISIBILITY) Slog.v(TAG_VISIBILITY, "Already invisible: " + r);
@@ -2613,8 +2639,8 @@
         mWindowManager.moveTaskToTop(task.taskId);
     }
 
-    final void startActivityLocked(ActivityRecord r, boolean newTask, boolean keepCurTransition,
-            ActivityOptions options) {
+    final void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
+            boolean newTask, boolean keepCurTransition, ActivityOptions options) {
         TaskRecord rTask = r.task;
         final int taskId = rTask.taskId;
         // mLaunchTaskBehind tasks get placed at the back of the task stack.
@@ -2693,11 +2719,20 @@
                 mWindowManager.prepareAppTransition(TRANSIT_NONE, keepCurTransition);
                 mNoAnimActivities.add(r);
             } else {
-                mWindowManager.prepareAppTransition(newTask
-                        ? r.mLaunchTaskBehind
-                                ? TRANSIT_TASK_OPEN_BEHIND
-                                : TRANSIT_TASK_OPEN
-                        : TRANSIT_ACTIVITY_OPEN, keepCurTransition);
+                int transit = TRANSIT_ACTIVITY_OPEN;
+                if (newTask) {
+                    if (r.mLaunchTaskBehind) {
+                        transit = TRANSIT_TASK_OPEN_BEHIND;
+                    } else {
+                        // If a new task is being launched, then mark the existing top activity to
+                        // enter picture-in-picture if it supports auto-entering PiP
+                        if (focusedTopActivity != null) {
+                            focusedTopActivity.setEnterPipOnMoveToBackground(true);
+                        }
+                        transit = TRANSIT_TASK_OPEN;
+                    }
+                }
+                mWindowManager.prepareAppTransition(transit, keepCurTransition);
                 mNoAnimActivities.remove(r);
             }
             addConfigOverride(r, task);
@@ -4193,6 +4228,8 @@
             AppTimeTracker timeTracker, String reason) {
         if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "moveTaskToFront: " + tr);
 
+        final ActivityRecord focusedTopActivity = mStackSupervisor.getFocusedStack() != null
+                ? mStackSupervisor.getFocusedStack().topActivity() : null;
         final int numTasks = mTaskHistory.size();
         final int index = mTaskHistory.indexOf(tr);
         if (numTasks == 0 || index < 0)  {
@@ -4238,6 +4275,11 @@
         } else {
             updateTransitLocked(TRANSIT_TASK_TO_FRONT, options);
         }
+        // If a new task is moved to the front, then mark the existing top activity to enter
+        // picture-in-picture if it supports auto-entering PiP
+        if (focusedTopActivity != null) {
+            focusedTopActivity.setEnterPipOnMoveToBackground(true);
+        }
 
         mStackSupervisor.resumeFocusedStackTopActivityLocked();
         EventLog.writeEvent(EventLogTags.AM_TASK_TO_FRONT, tr.userId, tr.taskId);
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index dde948f..fd32a5e 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -2638,11 +2638,13 @@
             return false;
         }
 
-        moveActivityToPinnedStackLocked(r, "moveTopActivityToPinnedStack", bounds);
+        moveActivityToPinnedStackLocked(r, "moveTopActivityToPinnedStack", bounds,
+                true /* moveHomeStackToFront */);
         return true;
     }
 
-    void moveActivityToPinnedStackLocked(ActivityRecord r, String reason, Rect bounds) {
+    void moveActivityToPinnedStackLocked(ActivityRecord r, String reason, Rect bounds,
+            boolean moveHomeStackToFront) {
         mWindowManager.deferSurfaceLayout();
         try {
             final TaskRecord task = r.task;
@@ -2666,7 +2668,7 @@
             if (task.mActivities.size() == 1) {
                 // There is only one activity in the task. So, we can just move the task over to
                 // the stack without re-parenting the activity in a different task.
-                if (task.getTaskToReturnTo() == HOME_ACTIVITY_TYPE) {
+                if (moveHomeStackToFront && task.getTaskToReturnTo() == HOME_ACTIVITY_TYPE) {
                     // Move the home stack forward if the task we just moved to the pinned stack
                     // was launched from home so home should be visible behind it.
                     moveHomeStackToFront(reason);
@@ -2674,6 +2676,8 @@
                 moveTaskToStackLocked(
                         task.taskId, PINNED_STACK_ID, ON_TOP, FORCE_FOCUS, reason, !ANIMATE);
             } else {
+                // There are multiple activities in the task and moving the top activity should
+                // reveal/leave the other activities in their original task
                 stack.moveActivityToStack(r);
             }
         } finally {
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 64bf3ad..d0960a0 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -1068,6 +1068,7 @@
         // If the activity being launched is the same as the one currently at the top, then
         // we need to check if it should only be launched once.
         final ActivityStack topStack = mSupervisor.mFocusedStack;
+        final ActivityRecord topFocused = topStack.topActivity();
         final ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(mNotTop);
         final boolean dontStart = top != null && mStartActivity.resultTo == null
                 && top.realActivity.equals(mStartActivity.realActivity)
@@ -1139,7 +1140,8 @@
 
         sendPowerHintForLaunchStartIfNeeded(false /* forceSend */);
 
-        mTargetStack.startActivityLocked(mStartActivity, newTask, mKeepCurTransition, mOptions);
+        mTargetStack.startActivityLocked(mStartActivity, topFocused, newTask, mKeepCurTransition,
+                mOptions);
         if (mDoResume) {
             final ActivityRecord topTaskActivity = mStartActivity.task.topRunningActivityLocked();
             if (!mTargetStack.isFocusable()
diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java
index 01a50b7..0ad4e0a 100644
--- a/services/core/java/com/android/server/wm/PinnedStackController.java
+++ b/services/core/java/com/android/server/wm/PinnedStackController.java
@@ -135,7 +135,7 @@
         mService = service;
         mDisplayContent = displayContent;
         mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
-        mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
+        mMotionHelper = new PipMotionHelper(UiThread.getHandler());
         mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
         reloadResources();
     }