Fixed transition animation from pipable activities

- Defer telling the client it is hidden if it can enter Pip and isn't
current stopped or stopping. This gives it a chance to enter Pip in
onPause().
- Once pause is complete set the visiblity to false to stop deferring
hiding client.
- Don't allow FLAG_RESUME_WHILE_PAUSING activity to resume until the
currently resumed activity is puased if the currently resumed activity
can enter Pip.
- Detach child surfaces added by the client process in
WindowState.sendAppVisibilityToClients() right before we notify the
client.

Test: manual
Change-Id: I3848f2b93f4f1d3ceec5a1ccd2e127c614f70fe4
Fixes: 37370508
Fixes: 37622341
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index 6e84ed6..eeedab8 100644
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -1568,17 +1568,21 @@
     }
 
     void setVisibility(boolean visible) {
-        mWindowContainerController.setVisibility(visible);
+        mWindowContainerController.setVisibility(visible, false /* deferHidingClient */);
+    }
+
+    void setVisible(boolean newVisible) {
+        setVisible(newVisible, false /* deferHidingClient */);
     }
 
     // TODO: Look into merging with #setVisibility()
-    void setVisible(boolean newVisible) {
+    void setVisible(boolean newVisible, boolean deferHidingClient) {
         visible = newVisible;
         if (!visible && mUpdateTaskThumbnailWhenHidden) {
             updateThumbnailLocked(screenshotActivityLocked(), null /* description */);
             mUpdateTaskThumbnailWhenHidden = false;
         }
-        mWindowContainerController.setVisibility(visible);
+        mWindowContainerController.setVisibility(visible, deferHidingClient);
         final ArrayList<ActivityContainer> containers = mChildContainers;
         for (int containerNdx = containers.size() - 1; containerNdx >= 0; --containerNdx) {
             final ActivityContainer container = containers.get(containerNdx);
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 824ec68..297f155 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -1402,7 +1402,9 @@
                 } else if ((!prev.visible && !hasVisibleBehindActivity())
                         || mService.isSleepingOrShuttingDownLocked()) {
                     // If we were visible then resumeTopActivities will release resources before
-                    // stopping.
+                    // stopping. Also, set visibility to false to flush any client hide that might have
+                    // been deferred.
+                    prev.setVisibility(false);
                     addToStopping(prev, true /* scheduleIdle */, false /* idleDelayed */);
                 }
             } else {
@@ -2019,11 +2021,11 @@
         try {
             final boolean canEnterPictureInPicture = r.checkEnterPictureInPictureState(
                     "makeInvisible", true /* noThrow */, true /* beforeStopping */);
-            // We don't want to call setVisible(false) to avoid notifying the client of this
-            // intermittent invisible state if it can enter Pip and isn't stopped or stopping.
-            if (!canEnterPictureInPicture || r.state == STOPPING || r.state == STOPPED) {
-                r.setVisible(false);
-            }
+            // Defer telling the client it is hidden if it can enter Pip and isn't current stopped
+            // or stopping. This gives it a chance to enter Pip in onPause().
+            final boolean deferHidingClient = canEnterPictureInPicture
+                    && r.state != STOPPING && r.state != STOPPED;
+            r.setVisible(false, deferHidingClient);
 
             switch (r.state) {
                 case STOPPING:
@@ -2048,15 +2050,6 @@
                     if (visibleBehind == r) {
                         releaseBackgroundResources(r);
                     } else {
-                        // If this activity is in a state where it can currently enter
-                        // picture-in-picture, then don't immediately schedule the idle now in case
-                        // the activity tries to enterPictureInPictureMode() later. Otherwise,
-                        // we will try and stop the activity next time idle is processed.
-
-                        if (canEnterPictureInPicture) {
-                            // We set r.visible=false so that Stop will later call setVisible for us
-                            r.visible = false;
-                        }
                         addToStopping(r, true /* scheduleIdle */,
                                 canEnterPictureInPicture /* idleDelayed */);
                     }
@@ -2338,20 +2331,22 @@
 
         mStackSupervisor.setLaunchSource(next.info.applicationInfo.uid);
 
-        final boolean prevCanPip = prev != null && prev.checkEnterPictureInPictureState(
-                "resumeTopActivity", true /* noThrow */, userLeaving /* beforeStopping */);
+        boolean lastResumedCanPip = false;
+        final ActivityStack lastFocusedStack = mStackSupervisor.getLastStack();
+        if (lastFocusedStack != null && lastFocusedStack != this) {
+            // So, why aren't we using prev here??? See the param comment on the method. prev doesn't
+            // represent the last resumed activity. However, the last focus stack does if it isn't null.
+            final ActivityRecord lastResumed = lastFocusedStack.mResumedActivity;
+            lastResumedCanPip = lastResumed != null && lastResumed.checkEnterPictureInPictureState(
+                    "resumeTopActivity", true /* noThrow */, userLeaving /* beforeStopping */);
+        }
         // If the flag RESUME_WHILE_PAUSING is set, then continue to schedule the previous activity
         // to be paused, while at the same time resuming the new resume activity only if the
         // previous activity can't go into Pip since we want to give Pip activities a chance to
         // enter Pip before resuming the next activity.
-        final boolean resumeWhilePausing = (next.info.flags & FLAG_RESUME_WHILE_PAUSING) != 0;
-        // TODO: This would be go to have however, the various call points that pass in
-        // prev need to be corrected first. In some cases the prev is equal to the next e.g. launch
-        // an app from home. And, is come other cases it is null e.g. press home button after
-        // launching an app. The doc on the method says prev. is null expect for the case we are
-        // coming from pause. We need to see if that is a valid thing and also if all the code in
-        // this method using prev. are setup to function like that.
-        //&& !prevCanPip;
+        final boolean resumeWhilePausing = (next.info.flags & FLAG_RESUME_WHILE_PAUSING) != 0
+                && !lastResumedCanPip;
+
         boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving, next, false);
         if (mResumedActivity != null) {
             if (DEBUG_STATES) Slog.d(TAG_STATES,
diff --git a/services/core/java/com/android/server/wm/AppWindowContainerController.java b/services/core/java/com/android/server/wm/AppWindowContainerController.java
index 292734d..c625cbe 100644
--- a/services/core/java/com/android/server/wm/AppWindowContainerController.java
+++ b/services/core/java/com/android/server/wm/AppWindowContainerController.java
@@ -323,7 +323,7 @@
         }
     }
 
-    public void setVisibility(boolean visible) {
+    public void setVisibility(boolean visible, boolean deferHidingClient) {
         synchronized(mWindowMap) {
             if (mContainer == null) {
                 Slog.w(TAG_WM, "Attempted to set visibility of non-existing app token: "
@@ -342,6 +342,7 @@
             mService.mClosingApps.remove(wtoken);
             wtoken.waitingToShow = false;
             wtoken.hiddenRequested = !visible;
+            wtoken.mDeferHidingClient = deferHidingClient;
 
             if (!visible) {
                 // If the app is dead while it was visible, we kept its dead window on screen.
@@ -368,15 +369,12 @@
                         wtoken.waitingToShow = true;
                     }
 
-                    if (wtoken.clientHidden) {
-                        // In the case where we are making an app visible
-                        // but holding off for a transition, we still need
-                        // to tell the client to make its windows visible so
-                        // they get drawn.  Otherwise, we will wait on
-                        // performing the transition until all windows have
-                        // been drawn, they never will be, and we are sad.
-                        wtoken.clientHidden = false;
-                        wtoken.sendAppVisibilityToClients();
+                    if (wtoken.isClientHidden()) {
+                        // In the case where we are making an app visible but holding off for a
+                        // transition, we still need to tell the client to make its windows visible
+                        // so they get drawn. Otherwise, we will wait on performing the transition
+                        // until all windows have been drawn, they never will be, and we are sad.
+                        wtoken.setClientHidden(false);
                     }
                 }
                 wtoken.requestUpdateWallpaperIfNeeded();
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 7634644..31fdacd 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -126,7 +126,11 @@
     boolean hiddenRequested;
 
     // Have we told the window clients to hide themselves?
-    boolean clientHidden;
+    private boolean mClientHidden;
+
+    // If true we will defer setting mClientHidden to true and reporting to the client that it is
+    // hidden.
+    boolean mDeferHidingClient;
 
     // Last visibility state we reported to the app token.
     boolean reportedVisible;
@@ -307,16 +311,25 @@
         }
     }
 
+    boolean isClientHidden() {
+        return mClientHidden;
+    }
+
+    void setClientHidden(boolean hideClient) {
+        if (mClientHidden == hideClient || (hideClient && mDeferHidingClient)) {
+            return;
+        }
+        mClientHidden = hideClient;
+        sendAppVisibilityToClients();
+    }
+
     boolean setVisibility(WindowManager.LayoutParams lp,
             boolean visible, int transit, boolean performLayout, boolean isVoiceInteraction) {
 
         boolean delayed = false;
         inPendingTransaction = false;
 
-        if (clientHidden == visible) {
-            clientHidden = !visible;
-            sendAppVisibilityToClients();
-        }
+        setClientHidden(!visible);
 
         // Allow for state changes and animation to be applied if:
         // * token is transitioning visibility state
@@ -1143,10 +1156,7 @@
                 hidden = false;
                 hiddenRequested = false;
             }
-            if (clientHidden != fromToken.clientHidden) {
-                clientHidden = fromToken.clientHidden;
-                sendAppVisibilityToClients();
-            }
+            setClientHidden(fromToken.mClientHidden);
             fromToken.mAppAnimator.transferCurrentAnimation(
                     mAppAnimator, tStartingWindow.mWinAnimator);
 
@@ -1511,10 +1521,9 @@
         pw.print(prefix); pw.print("task="); pw.println(getTask());
         pw.print(prefix); pw.print(" mFillsParent="); pw.print(mFillsParent);
                 pw.print(" mOrientation="); pw.println(mOrientation);
-        pw.print(prefix); pw.print("hiddenRequested="); pw.print(hiddenRequested);
-                pw.print(" clientHidden="); pw.print(clientHidden);
-                pw.print(" reportedDrawn="); pw.print(reportedDrawn);
-                pw.print(" reportedVisible="); pw.println(reportedVisible);
+        pw.println(prefix + "hiddenRequested=" + hiddenRequested + " mClientHidden=" + mClientHidden
+            + ((mDeferHidingClient) ? " mDeferHidingClient=" + mDeferHidingClient : "")
+            + " reportedDrawn=" + reportedDrawn + " reportedVisible=" + reportedVisible);
         if (paused) {
             pw.print(prefix); pw.print("paused="); pw.println(paused);
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 1823610..e2f313a 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2245,7 +2245,7 @@
                 wsa.destroySurface();
                 mService.mForceRemoves.add(w);
                 mTmpWindow = w;
-            } else if (w.mAppToken != null && w.mAppToken.clientHidden) {
+            } else if (w.mAppToken != null && w.mAppToken.isClientHidden()) {
                 Slog.w(TAG_WM, "LEAKED SURFACE (app token hidden): "
                         + w + " surface=" + wsa.mSurfaceController
                         + " token=" + w.mAppToken
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 4262d12..bc749e1 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -439,7 +439,7 @@
         for (int i = mChildren.size() - 1; i >= 0; i--) {
             final AppWindowToken token = mChildren.get(i);
             // skip hidden (or about to hide) apps
-            if (token.mIsExiting || token.clientHidden || token.hiddenRequested) {
+            if (token.mIsExiting || token.isClientHidden() || token.hiddenRequested) {
                 continue;
             }
             final WindowState win = token.findMainWindow();
@@ -607,7 +607,7 @@
         for (int i = mChildren.size() - 1; i >= 0; i--) {
             final AppWindowToken token = mChildren.get(i);
             // skip hidden (or about to hide) apps
-            if (!token.mIsExiting && !token.clientHidden && !token.hiddenRequested) {
+            if (!token.mIsExiting && !token.isClientHidden() && !token.hiddenRequested) {
                 return token;
             }
         }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 252b4d4..95878801 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1470,7 +1470,7 @@
             if (mInTouchMode) {
                 res |= WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE;
             }
-            if (win.mAppToken == null || !win.mAppToken.clientHidden) {
+            if (win.mAppToken == null || !win.mAppToken.isClientHidden()) {
                 res |= WindowManagerGlobal.ADD_FLAG_APP_VISIBLE;
             }
 
@@ -1950,7 +1950,7 @@
             }
             if (viewVisibility == View.VISIBLE &&
                     (win.mAppToken == null || win.mAttrs.type == TYPE_APPLICATION_STARTING
-                            || !win.mAppToken.clientHidden)) {
+                            || !win.mAppToken.isClientHidden())) {
                 result = win.relayoutVisibleWindow(mergedConfiguration, result, attrChanges,
                         oldVisibility);
                 try {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index b9776a3..9bdcc36 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1399,7 +1399,7 @@
      * @return true if the window should be considered while evaluating allDrawn flags.
      */
     boolean mightAffectAllDrawn(boolean visibleOnly) {
-        final boolean isViewVisible = (mAppToken == null || !mAppToken.clientHidden)
+        final boolean isViewVisible = (mAppToken == null || !mAppToken.isClientHidden())
                 && (mViewVisibility == View.VISIBLE) && !mWindowRemovalAllowed;
         return (isOnScreen() && (!visibleOnly || isViewVisible)
                 || mWinAnimator.mAttrType == TYPE_BASE_APPLICATION
@@ -2312,7 +2312,7 @@
      * interacts with it.
      */
     boolean shouldKeepVisibleDeadAppWindow() {
-        if (!isWinVisibleLw() || mAppToken == null || mAppToken.clientHidden) {
+        if (!isWinVisibleLw() || mAppToken == null || mAppToken.isClientHidden()) {
             // Not a visible app window or the app isn't dead.
             return false;
         }
@@ -2570,12 +2570,24 @@
     void sendAppVisibilityToClients() {
         super.sendAppVisibilityToClients();
 
-        final boolean clientHidden = mAppToken.clientHidden;
+        final boolean clientHidden = mAppToken.isClientHidden();
         if (mAttrs.type == TYPE_APPLICATION_STARTING && clientHidden) {
             // Don't hide the starting window.
             return;
         }
 
+        if (clientHidden) {
+            // Once we are notifying the client that it's visibility has changed, we need to prevent
+            // it from destroying child surfaces until the animation has finished. We do this by
+            // detaching any surface control the client added from the client.
+            for (int i = mChildren.size() - 1; i >= 0; --i) {
+                final WindowState c = mChildren.get(i);
+                c.mWinAnimator.detachChildren();
+            }
+
+            mWinAnimator.detachChildren();
+        }
+
         try {
             if (DEBUG_VISIBILITY) Slog.v(TAG,
                     "Setting visibility of " + this + ": " + (!clientHidden));