Maximize animation when activity is relaunched.

This is the first step towards maximize operation animations. It
builds upon preserving old window and starts clip+translate animation
of the new window from the frame of the old window. It mostly uses the
existing infrastructure, expanding the idea of opening apps list to
include also relaunching apps and treating maximize operation as an app
transition.

Change-Id: I7be402bd329c2fe5bf7d53a2a910532286a8b194
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index b7c55d4..dd9b6f1 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -3092,8 +3092,11 @@
                     // to size change. The activity will first remove the old window and then add a
                     // new one. This call will tell window manager about this, so it can preserve
                     // the old window until the new one is drawn. This prevents having a gap between
-                    // the removal and addition, in which no window is visible.
-                    mWindowManager.setReplacingWindow(r.appToken);
+                    // the removal and addition, in which no window is visible. If we also changed
+                    // the stack to the fullscreen stack, i.e. maximized the window, we will animate
+                    // the transition.
+                    mWindowManager.setReplacingWindow(r.appToken,
+                            stackId == FULLSCREEN_WORKSPACE_STACK_ID /* animate */);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index de7c07e..5dc6887 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -122,6 +122,8 @@
     public static final int TRANSIT_TASK_OPEN_BEHIND = 16;
     /** A window in a task is being animated in-place. */
     public static final int TRANSIT_TASK_IN_PLACE = 17;
+    /** An activity is being relaunched (e.g. due to configuration change). */
+    public static final int TRANSIT_ACTIVITY_RELAUNCH = 18;
 
     /** Fraction of animation at which the recents thumbnail stays completely transparent */
     private static final float RECENTS_THUMBNAIL_FADEIN_FRACTION = 0.5f;
@@ -1004,6 +1006,22 @@
         return prepareThumbnailAnimation(a, appWidth, appHeight, transit);
     }
 
+    private Animation createRelaunchAnimation(int appWidth, int appHeight) {
+        getDefaultNextAppTransitionStartRect(mTmpFromClipRect);
+        final int left = mTmpFromClipRect.left;
+        final int top = mTmpFromClipRect.top;
+        mTmpFromClipRect.offset(-left, -top);
+        mTmpToClipRect.set(0, 0, appWidth, appHeight);
+        AnimationSet set = new AnimationSet(true);
+        ClipRectAnimation clip = new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect);
+        TranslateAnimation translate = new TranslateAnimation(left, 0, top, 0);
+        clip.setInterpolator(mDecelerateInterpolator);
+        set.addAnimation(clip);
+        set.addAnimation(translate);
+        set.setDuration(DEFAULT_APP_TRANSITION_DURATION);
+        return set;
+    }
+
     /**
      * @return true if and only if the first frame of the transition can be skipped, i.e. the first
      *         frame of the transition doesn't change the visuals on screen, so we can start
@@ -1040,6 +1058,8 @@
                     "applyAnimation voice:"
                     + " anim=" + a + " transit=" + appTransitionToString(transit)
                     + " isEntrance=" + enter + " Callers=" + Debug.getCallers(3));
+        } else if (transit == TRANSIT_ACTIVITY_RELAUNCH) {
+            a = createRelaunchAnimation(appWidth, appHeight);
         } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM) {
             a = loadAnimationRes(mNextAppTransitionPackage, enter ?
                     mNextAppTransitionEnter : mNextAppTransitionExit);
@@ -1326,6 +1346,9 @@
             case TRANSIT_TASK_OPEN_BEHIND: {
                 return "TRANSIT_TASK_OPEN_BEHIND";
             }
+            case TRANSIT_ACTIVITY_RELAUNCH: {
+                return "TRANSIT_ACTIVITY_RELAUNCH";
+            }
             default: {
                 return "<UNKNOWN>";
             }
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 6ee2c39..4f62909 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -116,6 +116,10 @@
     // 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 mReplacingWindow;
+    // 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;
 
     AppWindowToken(WindowManagerService _service, IApplicationToken _token,
             boolean _voiceInteraction) {
@@ -230,16 +234,24 @@
     }
 
     WindowState findMainWindow() {
+        WindowState candidate = null;
         int j = windows.size();
         while (j > 0) {
             j--;
             WindowState win = windows.get(j);
             if (win.mAttrs.type == WindowManager.LayoutParams.TYPE_BASE_APPLICATION
                     || win.mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING) {
-                return win;
+                // In cases where there are multiple windows, we prefer the non-exiting window. This
+                // happens for example when when replacing windows during an activity relaunch. When
+                // constructing the animation, we want the new window, not the exiting one.
+                if (win.mExiting) {
+                    candidate = win;
+                } else {
+                    return win;
+                }
             }
         }
-        return null;
+        return candidate;
     }
 
     boolean isVisible() {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ebabc52..f3e7d1d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -125,7 +125,6 @@
 import com.android.server.LocalServices;
 import com.android.server.UiThread;
 import com.android.server.Watchdog;
-import com.android.server.am.BatteryStatsService;
 import com.android.server.input.InputManagerService;
 import com.android.server.policy.PhoneWindowManager;
 import com.android.server.power.ShutdownThread;
@@ -290,6 +289,10 @@
 
     private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.emulator.circular";
 
+    // Used to indicate that if there is already a transition set, it should be preserved when
+    // trying to apply a new one.
+    private static final boolean ALWAYS_KEEP_CURRENT = true;
+
     final private KeyguardDisableHandler mKeyguardDisableHandler;
 
     final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -1740,6 +1743,7 @@
 
             boolean addToken = false;
             WindowToken token = mTokenMap.get(attrs.token);
+            AppWindowToken atoken = null;
             if (token == null) {
                 if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                     Slog.w(TAG, "Attempted to add application window with unknown token "
@@ -1774,7 +1778,7 @@
                 token = new WindowToken(this, attrs.token, -1, false);
                 addToken = true;
             } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
-                AppWindowToken atoken = token.appWindowToken;
+                atoken = token.appWindowToken;
                 if (atoken == null) {
                     Slog.w(TAG, "Attempted to add window with non-application token "
                           + token + ".  Aborting.");
@@ -1920,6 +1924,7 @@
             final WindowStateAnimator winAnimator = win.mWinAnimator;
             winAnimator.mEnterAnimationPending = true;
             winAnimator.mEnteringAnimation = true;
+            prepareWindowReplacementTransition(atoken);
 
             if (displayContent.isDefaultDisplay) {
                 mPolicy.getInsetHintLw(win.mAttrs, mRotation, outContentInsets, outStableInsets,
@@ -1977,6 +1982,33 @@
         return res;
     }
 
+    private void prepareWindowReplacementTransition(AppWindowToken atoken) {
+        if (atoken == null || !atoken.mReplacingWindow || !atoken.mAnimateReplacingWindow) {
+            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) {
+                replacedWindow = candidate;
+            }
+        }
+        if (replacedWindow == null) {
+            // We expect to already receive a request to remove the old window. If it did not
+            // happen, let's just simply add a window.
+            return;
+        }
+        Rect frame = replacedWindow.mFrame;
+        // We treat this as if this activity was opening, so we can trigger the app transition
+        // animation and piggy-back on existing transition animation infrastructure.
+        mOpeningApps.add(atoken);
+        prepareAppTransition(AppTransition.TRANSIT_ACTIVITY_RELAUNCH, ALWAYS_KEEP_CURRENT);
+        mAppTransition.overridePendingAppTransitionClipReveal(frame.left, frame.top,
+                frame.width(), frame.height());
+        executeAppTransition();
+    }
+
     /**
      * Returns whether screen capture is disabled for all windows of a specific user.
      */
@@ -2072,6 +2104,7 @@
                 if (DEBUG_ADD_REMOVE) Slog.v(TAG, "Preserving " + win + " until the new one is "
                         + "added");
                 win.mExiting = true;
+                appToken.mReplacingRemoveRequested = true;
                 return;
             }
             // If we are not currently running the exit animation, we
@@ -2730,6 +2763,7 @@
         try {
             synchronized (mWindowMap) {
                 WindowState win = windowForClientLocked(session, client, false);
+                if (DEBUG_ADD_REMOVE) Slog.d(TAG, "finishDrawingWindow: " + win);
                 if (win != null && win.mWinAnimator.finishDrawingLocked()) {
                     if ((win.mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0) {
                         getDefaultDisplayContentLocked().pendingLayoutChanges |=
@@ -3426,6 +3460,12 @@
         }
     }
 
+    /**
+     * @param transit What kind of transition is happening. Use one of the constants
+     *                AppTransition.TRANSIT_*.
+     * @param alwaysKeepCurrent If true and a transition is already set, new transition will NOT
+     *                          be set.
+     */
     @Override
     public void prepareAppTransition(int transit, boolean alwaysKeepCurrent) {
         if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,
@@ -3822,10 +3862,13 @@
         }
 
         wtoken.willBeHidden = false;
-        // Allow for state changes and animation to be applied if token is transitioning
-        // visibility state or the token was marked as hidden and is exiting before we had a chance
-        // to play the transition animation.
-        if (wtoken.hidden == visible || (wtoken.hidden && wtoken.mIsExiting)) {
+        // Allow for state changes and animation to be applied if:
+        // * token is transitioning visibility state
+        // * or the token was marked as hidden and is exiting before we had a chance to play the
+        // transition animation
+        // * or this is an opening app and windows are being replaced.
+        if (wtoken.hidden == visible || (wtoken.hidden && wtoken.mIsExiting) ||
+                (visible && wtoken.mReplacingWindow)) {
             boolean changed = false;
             if (DEBUG_APP_TRANSITIONS) Slog.v(
                 TAG, "Changing app " + wtoken + " hidden=" + wtoken.hidden
@@ -9723,7 +9766,7 @@
         return mWindowMap;
     }
 
-    public void setReplacingWindow(IBinder token) {
+    public void setReplacingWindow(IBinder token, boolean animate) {
         synchronized (mWindowMap) {
             AppWindowToken appWindowToken = findAppWindowToken(token);
             if (appWindowToken == null) {
@@ -9731,6 +9774,7 @@
                 return;
             }
             appWindowToken.mReplacingWindow = true;
+            appWindowToken.mAnimateReplacingWindow = animate;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 49d260e..d756d39 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -545,6 +545,14 @@
     @Override
     public void computeFrameLw(Rect pf, Rect df, Rect of, Rect cf, Rect vf, Rect dcf, Rect sf,
             Rect osf) {
+        if (mAppToken != null && mAppToken.mReplacingWindow
+                && (mExiting || !mAppToken.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
+            // appears.
+            return;
+        }
         mHaveFrame = true;
 
         final Task task = mAppToken != null ? getTask() : null;
@@ -1274,6 +1282,8 @@
         AppWindowToken token = mAppToken;
         if (token != null && token.mReplacingWindow) {
             token.mReplacingWindow = false;
+            token.mAnimateReplacingWindow = false;
+            token.mReplacingRemoveRequested = false;
             for (int i = token.allAppWindows.size() - 1; i >= 0; i--) {
                 WindowState win = token.allAppWindows.get(i);
                 if (win.mExiting) {
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index efad8bf..066dc95 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -1501,6 +1501,7 @@
             w.mLastHScale = w.mHScale;
             w.mLastVScale = w.mVScale;
             if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
+                    "control=" + mSurfaceControl +
                     "alpha=" + mShownAlpha + " layer=" + mAnimLayer
                     + " matrix=[" + mDsDx + "*" + w.mHScale
                     + "," + mDtDx + "*" + w.mVScale
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index ce07a9d..8582ca2 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -47,7 +47,6 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.provider.Settings;
-import android.util.Log;
 import android.util.Slog;
 import android.view.Display;
 import android.view.DisplayInfo;
@@ -305,9 +304,9 @@
                     if (DEBUG_LAYOUT_REPEATS) debugLayoutRepeats(
                             "On entry to LockedInner", displayContent.pendingLayoutChanges);
 
-                    if ((displayContent.pendingLayoutChanges &
-                            FINISH_LAYOUT_REDO_WALLPAPER) != 0 &&
-                            mWallpaperControllerLocked.adjustWallpaperWindows()) {
+                    if ((displayContent.pendingLayoutChanges
+                            & FINISH_LAYOUT_REDO_WALLPAPER) != 0
+                            && mWallpaperControllerLocked.adjustWallpaperWindows()) {
                         mService.assignLayersLocked(windows);
                         displayContent.layoutNeeded = true;
                     }
@@ -430,8 +429,7 @@
                     // Moved from updateWindowsAndWallpaperLocked().
                     if (w.mHasSurface) {
                         // Take care of the window being ready to display.
-                        final boolean committed =
-                                winAnimator.commitFinishDrawingLocked();
+                        final boolean committed = winAnimator.commitFinishDrawingLocked();
                         if (isDefaultDisplay && committed) {
                             if (w.mAttrs.type == TYPE_DREAM) {
                                 // HACK: When a dream is shown, it may at that
@@ -447,9 +445,8 @@
                                 }
                             }
                             if ((w.mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0) {
-                                if (DEBUG_WALLPAPER_LIGHT)
-                                    Slog.v(TAG,
-                                            "First draw done in potential wallpaper target " + w);
+                                if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
+                                        "First draw done in potential wallpaper target " + w);
                                 mWallpaperMayChange = true;
                                 displayContent.pendingLayoutChanges |=
                                         FINISH_LAYOUT_REDO_WALLPAPER;
@@ -544,7 +541,6 @@
             // Give the display manager a chance to adjust properties
             // like display rotation if it needs to.
             mService.mDisplayManagerInternal.performTraversalInTransactionFromWindowManager();
-
         } catch (RuntimeException e) {
             Slog.wtf(TAG, "Unhandled exception in Window Manager", e);
         } finally {
@@ -1461,8 +1457,6 @@
                 // open/close animation (only on the way down)
                 anim = mService.mAppTransition.createThumbnailAspectScaleAnimationLocked(appRect,
                         thumbnailHeader, taskId);
-                Log.d(TAG, "assigning thumbnail force above layer: "
-                        + openingLayer + " " + closingLayer);
                 openingAppAnimator.thumbnailForceAboveLayer = Math.max(openingLayer, closingLayer);
                 openingAppAnimator.deferThumbnailDestruction =
                         !mService.mAppTransition.isNextThumbnailTransitionScaleUp();