Move window replacement tracking to window state.

In preparation for supporting replacement of child windows
we make replacement per window rather than per app.

Bug: 26070641

Change-Id: Ifa332086599c125611e430219c9497bae7e2ce31
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index fd5c704..b49641f 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -19,6 +19,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_MOVEMENT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -122,21 +123,6 @@
     // True if the windows associated with this token should be cropped to their stack bounds.
     boolean mCropWindowsToStack;
 
-    // This application will have its window replaced due to relaunch. This allows window manager
-    // to differentiate between simple removal of a window and replacement. In the latter case it
-    // will preserve the old window until the new one is drawn.
-    boolean mWillReplaceWindow;
-    // If true, the replaced window was already requested to be removed.
-    boolean mReplacingRemoveRequested;
-    // Whether the replacement of the window should trigger app transition animation.
-    boolean mAnimateReplacingWindow;
-    // If not null, the window that will be used to replace the old one. This is being set when
-    // the window is added and unset when this window reports its first draw.
-    WindowState mReplacingWindow;
-    // Whether the new window has replaced the old one, so the old one can be removed without
-    // blinking.
-    boolean mHasReplacedWindow;
-
     AppWindowToken(WindowManagerService _service, IApplicationToken _token,
             boolean _voiceInteraction) {
         super(_service, _token.asBinder(),
@@ -392,6 +378,62 @@
         }
     }
 
+    void setReplacingWindows(boolean animate) {
+        if (DEBUG_ADD_REMOVE) Slog.d(TAG_WM, "Marking app token " + appWindowToken
+                + " with replacing windows.");
+
+        for (int i = allAppWindows.size() - 1; i >= 0; i--) {
+            final WindowState w = allAppWindows.get(i);
+            w.setReplacing(animate);
+        }
+        if (animate) {
+            // Set-up dummy animation so we can start treating windows associated with this
+            // token like they are in transition before the new app window is ready for us to
+            // run the real transition animation.
+            if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM,
+                    "setReplacingWindow() Setting dummy animation on: " + this);
+            mAppAnimator.setDummyAnimation();
+        }
+    }
+
+    void addWindow(WindowState w) {
+        for (int i = allAppWindows.size() - 1; i >= 0; i--) {
+            WindowState candidate = allAppWindows.get(i);
+            if (candidate.mWillReplaceWindow && candidate.mReplacingWindow == null &&
+                    candidate.getWindowTag().equals(w.getWindowTag().toString())) {
+                candidate.mReplacingWindow = w;
+            }
+        }
+        allAppWindows.add(w);
+    }
+
+    boolean waitingForReplacement() {
+        for (int i = allAppWindows.size() -1; i >= 0; i--) {
+            WindowState candidate = allAppWindows.get(i);
+            if (candidate.mWillReplaceWindow) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void clearTimedoutReplaceesLocked() {
+        for (int i = allAppWindows.size() - 1; i >= 0;
+             // removeWindowLocked at bottom of loop may remove multiple entries from
+             // allAppWindows if the window to be removed has child windows. It also may
+             // not remove any windows from allAppWindows at all if win is exiting and
+             // currently animating away. This ensures that winNdx is monotonically decreasing
+             // and never beyond allAppWindows bounds.
+             i = Math.min(i - 1, allAppWindows.size() - 1)) {
+            WindowState candidate = allAppWindows.get(i);
+            if (candidate.mWillReplaceWindow == false) {
+                continue;
+            }
+            candidate.mWillReplaceWindow = false;
+            service.removeWindowLocked(candidate);
+        }
+    }
+
     @Override
     void dump(PrintWriter pw, String prefix) {
         super.dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3db9ae0..2e424d0 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -506,7 +506,7 @@
                 inFreeformSpace = stack != null && stack.mStackId == FREEFORM_WORKSPACE_STACK_ID;
             }
 
-            replacing = replacing || (w.mAppToken != null && w.mAppToken.mWillReplaceWindow);
+            replacing = replacing || w.mWillReplaceWindow;
 
             // If the app is executing an animation because the keyguard is going away,
             // keep the wallpaper during the animation so it doesn't flicker out.
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index a6523a4..6a5183f 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -918,10 +918,6 @@
     }
 
     void requestRemovalOfReplacedWindows(WindowState win) {
-        final AppWindowToken token = win.mAppToken;
-        if (token != null && token.mWillReplaceWindow && token.mReplacingWindow == win) {
-            token.mHasReplacedWindow = true;
-        }
         mRemoveReplacedWindows = true;
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0c606fe..f915fe9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -267,6 +267,9 @@
     /** Amount of time (in milliseconds) to delay before declaring a window freeze timeout. */
     static final int WINDOW_FREEZE_TIMEOUT_DURATION = 2000;
 
+    /** Amount of time (in milliseconds) to delay before declaring a window replacement timeout. */
+    static final int WINDOW_REPLACEMENT_TIMEOUT_DURATION = 2000;
+
     /** Amount of time to allow a last ANR message to exist before freeing the memory. */
     static final int LAST_ANR_LIFETIME_DURATION_MSECS = 2 * 60 * 60 * 1000; // Two hours
     /**
@@ -1349,10 +1352,7 @@
         final AppWindowToken appToken = win.mAppToken;
         if (appToken != null) {
             if (addToToken) {
-                appToken.allAppWindows.add(win);
-            }
-            if (appToken.mWillReplaceWindow) {
-                appToken.mReplacingWindow = win;
+                appToken.addWindow(win);
             }
         }
     }
@@ -2083,14 +2083,15 @@
     }
 
     private void prepareWindowReplacementTransition(AppWindowToken atoken) {
-        if (atoken == null || !atoken.mWillReplaceWindow || !atoken.mAnimateReplacingWindow) {
+        if (atoken == null) {
             return;
         }
         atoken.allDrawn = false;
         WindowState replacedWindow = null;
         for (int i = atoken.windows.size() - 1; i >= 0 && replacedWindow == null; i--) {
             WindowState candidate = atoken.windows.get(i);
-            if (candidate.mExiting) {
+            if (candidate.mExiting && candidate.mWillReplaceWindow
+                    && candidate.mAnimateReplacingWindow) {
                 replacedWindow = candidate;
             }
         }
@@ -2190,7 +2191,7 @@
                 + " app-animation="
                 + (win.mAppToken != null ? win.mAppToken.mAppAnimator.animation : null)
                 + " mWillReplaceWindow="
-                + (win.mAppToken != null ? win.mAppToken.mWillReplaceWindow : false)
+                + win.mWillReplaceWindow
                 + " inPendingTransaction="
                 + (win.mAppToken != null ? win.mAppToken.inPendingTransaction : false)
                 + " mDisplayFrozen=" + mDisplayFrozen);
@@ -2202,13 +2203,13 @@
         // animation wouldn't be seen.
         if (win.mHasSurface && okToDisplay()) {
             final AppWindowToken appToken = win.mAppToken;
-            if (appToken != null && appToken.mWillReplaceWindow) {
+            if (win.mWillReplaceWindow) {
                 // This window is going to be replaced. We need to keep it around until the new one
                 // gets added, then we will get rid of this one.
                 if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "Preserving " + win + " until the new one is "
                         + "added");
                 win.mExiting = true;
-                appToken.mReplacingRemoveRequested = true;
+                win.mReplacingRemoveRequested = true;
                 Binder.restoreCallingIdentity(origId);
                 return;
             }
@@ -4042,7 +4043,7 @@
         // transition animation
         // * or this is an opening app and windows are being replaced.
         if (wtoken.hidden == visible || (wtoken.hidden && wtoken.mIsExiting) ||
-                (visible && wtoken.mWillReplaceWindow)) {
+                (visible && wtoken.waitingForReplacement())) {
             boolean changed = false;
             if (DEBUG_APP_TRANSITIONS) Slog.v(
                 TAG_WM, "Changing app " + wtoken + " hidden=" + wtoken.hidden
@@ -7483,6 +7484,8 @@
         public static final int TWO_FINGER_SCROLL_START = 45;
         public static final int SHOW_NON_RESIZEABLE_DOCK_TOAST = 46;
 
+        public static final int WINDOW_REPLACEMENT_TIMEOUT = 47;
+
         /**
          * Used to denote that an integer field in a message will not be used.
          */
@@ -8065,6 +8068,13 @@
                     toast.show();
                 }
                 break;
+                case WINDOW_REPLACEMENT_TIMEOUT: {
+                    final AppWindowToken token = (AppWindowToken) msg.obj;
+                    synchronized (mWindowMap) {
+                        token.clearTimedoutReplaceesLocked();
+                    }
+                }
+                break;
             }
             if (DEBUG_WINDOW_TRACE) {
                 Slog.v(TAG_WM, "handleMessage: exit");
@@ -8587,7 +8597,7 @@
                 final int numTokens = tokens.size();
                 for (int tokenNdx = 0; tokenNdx < numTokens; ++tokenNdx) {
                     final AppWindowToken wtoken = tokens.get(tokenNdx);
-                    if (wtoken.mIsExiting && !wtoken.mWillReplaceWindow) {
+                    if (wtoken.mIsExiting && !wtoken.waitingForReplacement()) {
                         continue;
                     }
                     i = reAddAppWindowsLocked(displayContent, i, wtoken);
@@ -8696,8 +8706,8 @@
     private void forceHigherLayerIfNeeded(WindowState w, WindowStateAnimator winAnimator,
             AppWindowToken wtoken) {
         boolean force = false;
-        if (wtoken.mWillReplaceWindow && wtoken.mReplacingWindow != w
-                && wtoken.mAnimateReplacingWindow) {
+
+        if (w.mWillReplaceWindow) {
             // We know that we will be animating a relaunching window in the near future,
             // which will receive a z-order increase. We want the replaced window to
             // immediately receive the same treatment, e.g. to be above the dock divider.
@@ -10213,26 +10223,20 @@
      * @param token Application token for which the activity will be relaunched.
      */
     public void setReplacingWindow(IBinder token, boolean animate) {
+        AppWindowToken appWindowToken = null;
         synchronized (mWindowMap) {
-            AppWindowToken appWindowToken = findAppWindowToken(token);
+            appWindowToken = findAppWindowToken(token);
             if (appWindowToken == null || !appWindowToken.isVisible()) {
                 Slog.w(TAG_WM, "Attempted to set replacing window on non-existing app token " + token);
                 return;
             }
-            if (DEBUG_ADD_REMOVE) Slog.d(TAG_WM, "Marking app token " + appWindowToken
-                    + " as replacing window.");
-            appWindowToken.mWillReplaceWindow = true;
-            appWindowToken.mHasReplacedWindow = false;
-            appWindowToken.mAnimateReplacingWindow = animate;
+            appWindowToken.setReplacingWindows(animate);
+        }
 
-            if (animate) {
-                // Set-up dummy animation so we can start treating windows associated with this
-                // token like they are in transition before the new app window is ready for us to
-                // run the real transition animation.
-                if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM,
-                        "setReplacingWindow() Setting dummy animation on: " + appWindowToken);
-                appWindowToken.mAppAnimator.setDummyAnimation();
-            }
+        if (appWindowToken != null) {
+            mH.removeMessages(H.WINDOW_REPLACEMENT_TIMEOUT);
+            mH.sendMessageDelayed(mH.obtainMessage(H.WINDOW_REPLACEMENT_TIMEOUT, appWindowToken),
+                    WINDOW_REPLACEMENT_TIMEOUT_DURATION);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5a589e3..e4a6806 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -75,6 +75,7 @@
 import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
@@ -403,6 +404,18 @@
     // used to start an entering animation earlier.
     public boolean mSurfaceSaved = false;
 
+    // This window will be replaced due to relaunch. This allows window manager
+    // to differentiate between simple removal of a window and replacement. In the latter case it
+    // will preserve the old window until the new one is drawn.
+    boolean mWillReplaceWindow = false;
+    // If true, the replaced window was already requested to be removed.
+    boolean mReplacingRemoveRequested = false;
+    // Whether the replacement of the window should trigger app transition animation.
+    boolean mAnimateReplacingWindow = false;
+    // If not null, the window that will be used to replace the old one. This is being set when
+    // the window is added and unset when this window reports its first draw.
+    WindowState mReplacingWindow = null;
+
     /**
      * Wake lock for drawing.
      * Even though it's slightly more expensive to do so, we will use a separate wake lock
@@ -580,8 +593,7 @@
     @Override
     public void computeFrameLw(Rect pf, Rect df, Rect of, Rect cf, Rect vf, Rect dcf, Rect sf,
             Rect osf) {
-        if (mAppToken != null && mAppToken.mWillReplaceWindow
-                && (mExiting || !mAppToken.mReplacingRemoveRequested)) {
+        if (mWillReplaceWindow && (mExiting || !mReplacingRemoveRequested)) {
             // This window is being replaced and either already got information that it's being
             // removed or we are still waiting for some information. Because of this we don't
             // want to apply any more changes to it, so it remains in this state until new window
@@ -1343,17 +1355,17 @@
     }
 
     void maybeRemoveReplacedWindow() {
-        AppWindowToken token = mAppToken;
-        if (token != null && token.mWillReplaceWindow && token.mReplacingWindow == this
-                && token.mHasReplacedWindow) {
-            if (DEBUG_ADD_REMOVE) Slog.d(TAG, "Removing replacing window: " + this);
-            token.mWillReplaceWindow = false;
-            token.mAnimateReplacingWindow = false;
-            token.mReplacingRemoveRequested = false;
-            token.mReplacingWindow = null;
-            token.mHasReplacedWindow = false;
-            for (int i = token.allAppWindows.size() - 1; i >= 0; i--) {
-                final WindowState win = token.allAppWindows.get(i);
+        if (mAppToken == null) {
+            return;
+        }
+        for (int i = mAppToken.allAppWindows.size() - 1; i >= 0; i--) {
+            final WindowState win = mAppToken.allAppWindows.get(i);
+            if (DEBUG_ADD_REMOVE) Slog.d(TAG, "Removing replaced window: " + win);
+            if (win.mWillReplaceWindow && win.mReplacingWindow == this) {
+                win.mWillReplaceWindow = false;
+                win.mAnimateReplacingWindow = false;
+                win.mReplacingRemoveRequested = false;
+                win.mReplacingWindow = null;
                 if (win.mExiting) {
                     mService.removeWindowInnerLocked(win);
                 }
@@ -2161,7 +2173,7 @@
             + " " + getWindowTag();
     }
 
-    private CharSequence getWindowTag() {
+    CharSequence getWindowTag() {
         CharSequence tag = mAttrs.getTitle();
         if (tag == null || tag.length() <= 0) {
             tag = mAttrs.packageName;
@@ -2259,4 +2271,12 @@
     boolean isChildWindow() {
         return mAttachedWindow != null;
     }
+
+    void setReplacing(boolean animate) {
+        if ((mAttrs.privateFlags & PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH) == 0) {
+            mWillReplaceWindow = true;
+            mReplacingWindow = null;
+            mAnimateReplacingWindow = animate;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 539810d..b3eb173 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -1190,7 +1190,7 @@
         // different stack. If we suddenly crop it to the new stack bounds, it might get cut off.
         // We don't want it to happen, so we let it ignore the stack bounds until it gets removed.
         // The window that will replace it will abide them.
-        if (isAnimating() && (appToken.mWillReplaceWindow || w.inDockedWorkspace()
+        if (isAnimating() && (w.mWillReplaceWindow || w.inDockedWorkspace()
                 || w.inFreeformWorkspace())) {
             return;
         }
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index 50bdf25..82a6b56 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -1192,7 +1192,7 @@
                     // if app window is removed, or window relayout to invisible. We don't want to
                     // clear it out for windows that get replaced, because the animation depends on
                     // the flag to remove the replaced window.
-                    if (win.mAppToken == null || !win.mAppToken.mWillReplaceWindow) {
+                    if (!win.mWillReplaceWindow) {
                         win.mExiting = false;
                     }
                     if (win.mWinAnimator.mAnimLayer > layer) {