Merge "Freeform to recents app transition."
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 821621e..0266a37 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -25,10 +25,12 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IRemoteCallback;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.Pair;
 import android.util.Slog;
+import android.view.AppTransitionAnimationSpec;
 import android.view.View;
 import android.view.Window;
 
@@ -129,6 +131,11 @@
     public static final String KEY_ANIM_START_LISTENER = "android:activity.animStartListener";
 
     /**
+     * Descriptions of app transition animations to be played during the activity launch.
+     */
+    private static final String KEY_ANIM_SPECS = "android:activity.animSpecs";
+
+    /**
      * Where the docked stack should be positioned.
      * @hide
      */
@@ -199,6 +206,7 @@
     private int mExitCoordinatorIndex;
     private PendingIntent mUsageTimeReport;
     private int mDockCreateMode = DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
+    private AppTransitionAnimationSpec mAnimSpecs[];
 
     /**
      * Create an ActivityOptions specifying a custom animation to run when
@@ -512,6 +520,18 @@
         return opts;
     }
 
+    /** @hide */
+    public static ActivityOptions makeThumbnailAspectScaleDownAnimation(View source,
+            AppTransitionAnimationSpec[] specs, Handler handler,
+            OnAnimationStartedListener listener) {
+        ActivityOptions opts = new ActivityOptions();
+        opts.mPackageName = source.getContext().getPackageName();
+        opts.mAnimationType = ANIM_THUMBNAIL_ASPECT_SCALE_DOWN;
+        opts.mAnimSpecs = specs;
+        opts.setOnAnimationStartedListener(handler, listener);
+        return opts;
+    }
+
     /**
      * Create an ActivityOptions to transition between Activities using cross-Activity scene
      * animations. This method carries the position of one shared element to the started Activity.
@@ -698,6 +718,13 @@
                 break;
         }
         mDockCreateMode = opts.getInt(KEY_DOCK_CREATE_MODE, DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT);
+        if (opts.containsKey(KEY_ANIM_SPECS)) {
+            Parcelable[] specs = opts.getParcelableArray(KEY_ANIM_SPECS);
+            mAnimSpecs = new AppTransitionAnimationSpec[specs.length];
+            for (int i = specs.length - 1; i >= 0; i--) {
+                mAnimSpecs[i] = (AppTransitionAnimationSpec) specs[i];
+            }
+        }
     }
 
     /** @hide */
@@ -810,6 +837,9 @@
     }
 
     /** @hide */
+    public AppTransitionAnimationSpec[] getAnimSpecs() { return mAnimSpecs; }
+
+    /** @hide */
     public static void abort(Bundle options) {
         if (options != null) {
             (new ActivityOptions(options)).abort();
@@ -898,6 +928,7 @@
                 mExitCoordinatorIndex = otherOptions.mExitCoordinatorIndex;
                 break;
         }
+        mAnimSpecs = otherOptions.mAnimSpecs;
     }
 
     /**
@@ -964,6 +995,9 @@
                 break;
         }
         b.putInt(KEY_DOCK_CREATE_MODE, mDockCreateMode);
+        if (mAnimSpecs != null) {
+            b.putParcelableArray(KEY_ANIM_SPECS, mAnimSpecs);
+        }
 
         return b;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 2042668..d02e2af 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.recents;
 
+import static android.app.ActivityManager.FREEFORM_WORKSPACE_STACK_ID;
+
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ITaskStackListener;
@@ -31,8 +33,10 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.MutableBoolean;
+import android.view.AppTransitionAnimationSpec;
 import android.view.LayoutInflater;
 import android.view.View;
+
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
@@ -557,12 +561,44 @@
      */
     private ActivityOptions getThumbnailTransitionActivityOptions(
             ActivityManager.RunningTaskInfo topTask, TaskStack stack, TaskStackView stackView) {
+        if (topTask.stackId == FREEFORM_WORKSPACE_STACK_ID) {
+            ArrayList<AppTransitionAnimationSpec> specs = new ArrayList<>();
+            stackView.getScroller().setStackScrollToInitialState();
+            ArrayList<Task> tasks = stack.getTasks();
+            for (int i = tasks.size() - 1; i >= 0; i--) {
+                Task task = tasks.get(i);
+                if (SystemServicesProxy.isFreeformStack(task.key.stackId)) {
+                    mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
+                            stackView.getScroller().getStackScroll(), mTmpTransform, null);
+                    Rect toTaskRect = new Rect();
+                    mTmpTransform.rect.round(toTaskRect);
+                    Bitmap thumbnail = getThumbnailBitmap(topTask, task, mTmpTransform);
+                    specs.add(new AppTransitionAnimationSpec(task.key.id, thumbnail, toTaskRect));
+                }
+            }
+            AppTransitionAnimationSpec[] specsArray = new AppTransitionAnimationSpec[specs.size()];
+            specs.toArray(specsArray);
+            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
+                    specsArray, mHandler, this);
+        } else {
+            // Update the destination rect
+            Task toTask = new Task();
+            TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
+                    topTask.id, toTask);
+            RectF toTaskRect = toTransform.rect;
+            Bitmap thumbnail = getThumbnailBitmap(topTask, toTask, toTransform);
+            if (thumbnail != null) {
+                return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
+                        thumbnail, (int) toTaskRect.left, (int) toTaskRect.top,
+                        (int) toTaskRect.width(), (int) toTaskRect.height(), mHandler, this);
+            }
+            // If both the screenshot and thumbnail fails, then just fall back to the default transition
+            return getUnknownTransitionActivityOptions();
+        }
+    }
 
-        // Update the destination rect
-        Task toTask = new Task();
-        TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
-                topTask.id, toTask);
-        RectF toTaskRect = toTransform.rect;
+    private Bitmap getThumbnailBitmap(ActivityManager.RunningTaskInfo topTask, Task toTask,
+            TaskViewTransform toTransform) {
         Bitmap thumbnail;
         if (mThumbnailTransitionBitmapCacheKey != null
                 && mThumbnailTransitionBitmapCacheKey.key != null
@@ -574,14 +610,7 @@
             preloadIcon(topTask);
             thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform);
         }
-        if (thumbnail != null) {
-            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
-                    thumbnail, (int) toTaskRect.left, (int) toTaskRect.top,
-                    (int) toTaskRect.width(), (int) toTaskRect.height(), mHandler, this);
-        }
-
-        // If both the screenshot and thumbnail fails, then just fall back to the default transition
-        return getUnknownTransitionActivityOptions();
+        return thumbnail;
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index 4671cb0..8061a92 100755
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -53,6 +53,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.TimeUtils;
+import android.view.AppTransitionAnimationSpec;
 import android.view.IApplicationToken;
 import android.view.WindowManager;
 
@@ -863,17 +864,24 @@
                     break;
                 case ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_UP:
                 case ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_DOWN:
-                    service.mWindowManager.overridePendingAppTransitionAspectScaledThumb(
-                            pendingOptions.getThumbnail(),
-                            pendingOptions.getStartX(), pendingOptions.getStartY(),
-                            pendingOptions.getWidth(), pendingOptions.getHeight(),
-                            pendingOptions.getOnAnimationStartListener(),
-                            (animationType == ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_UP));
-                    if (intent.getSourceBounds() == null) {
-                        intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
-                                pendingOptions.getStartY(),
-                                pendingOptions.getStartX() + pendingOptions.getWidth(),
-                                pendingOptions.getStartY() + pendingOptions.getHeight()));
+                    final AppTransitionAnimationSpec[] specs = pendingOptions.getAnimSpecs();
+                    if (animationType == ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_DOWN
+                            && specs != null) {
+                        service.mWindowManager.overridePendingAppTransitionMultiThumb(
+                                specs, pendingOptions.getOnAnimationStartListener(), false);
+                    } else {
+                        service.mWindowManager.overridePendingAppTransitionAspectScaledThumb(
+                                pendingOptions.getThumbnail(),
+                                pendingOptions.getStartX(), pendingOptions.getStartY(),
+                                pendingOptions.getWidth(), pendingOptions.getHeight(),
+                                pendingOptions.getOnAnimationStartListener(),
+                                (animationType == ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_UP));
+                        if (intent.getSourceBounds() == null) {
+                            intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
+                                    pendingOptions.getStartY(),
+                                    pendingOptions.getStartX() + pendingOptions.getWidth(),
+                                    pendingOptions.getStartY() + pendingOptions.getHeight()));
+                        }
                     }
                     break;
                 default:
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 1cd3758..51042cb 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -4376,7 +4376,7 @@
 
             RunningTaskInfo ci = new RunningTaskInfo();
             ci.id = task.taskId;
-            ci.stackId = (task.stack == null) ? INVALID_STACK_ID : task.stack.getStackId();
+            ci.stackId = mStackId;
             ci.baseActivity = r.intent.getComponent();
             ci.topActivity = top.intent.getComponent();
             ci.lastActiveTime = task.lastActiveTime;
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index dc34904..eb0ca23 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -64,7 +64,6 @@
 import android.view.animation.PathInterpolator;
 import android.view.animation.ScaleAnimation;
 import android.view.animation.TranslateAnimation;
-import android.view.animation.TranslateYAnimation;
 
 import com.android.internal.util.DumpUtils.Dump;
 import com.android.server.AttributeCache;
@@ -175,7 +174,7 @@
     private Rect mTmpFromClipRect = new Rect();
     private Rect mTmpToClipRect = new Rect();
 
-    private final Rect mTmpStartRect = new Rect();
+    private final Rect mTmpRect = new Rect();
 
     private final static int APP_STATE_IDLE = 0;
     private final static int APP_STATE_READY = 1;
@@ -459,16 +458,16 @@
     private Animation createScaleUpAnimationLocked(int transit, boolean enter,
             Rect containingFrame) {
         Animation a;
-        getDefaultNextAppTransitionStartRect(mTmpStartRect);
+        getDefaultNextAppTransitionStartRect(mTmpRect);
         final int appWidth = containingFrame.width();
         final int appHeight = containingFrame.height();
         if (enter) {
             // Entering app zooms out from the center of the initial rect.
-            float scaleW = mTmpStartRect.width() / (float) appWidth;
-            float scaleH = mTmpStartRect.height() / (float) appHeight;
+            float scaleW = mTmpRect.width() / (float) appWidth;
+            float scaleH = mTmpRect.height() / (float) appHeight;
             Animation scale = new ScaleAnimation(scaleW, 1, scaleH, 1,
-                    computePivot(mTmpStartRect.left, scaleW),
-                    computePivot(mTmpStartRect.right, scaleH));
+                    computePivot(mTmpRect.left, scaleW),
+                    computePivot(mTmpRect.right, scaleH));
             scale.setInterpolator(mDecelerateInterpolator);
 
             Animation alpha = new AlphaAnimation(0, 1);
@@ -545,20 +544,20 @@
 
             final int appWidth = appFrame.width();
             final int appHeight = appFrame.height();
-            // mTmpStartRect will contain an area around the launcher icon that was pressed. We will
+            // mTmpRect will contain an area around the launcher icon that was pressed. We will
             // clip reveal from that area in the final area of the app.
-            getDefaultNextAppTransitionStartRect(mTmpStartRect);
+            getDefaultNextAppTransitionStartRect(mTmpRect);
 
             float t = 0f;
             if (appHeight > 0) {
-                t = (float) mTmpStartRect.left / appHeight;
+                t = (float) mTmpRect.left / appHeight;
             }
             int translationY = mClipRevealTranslationY + (int)(appHeight / 7f * t);
 
-            int centerX = mTmpStartRect.centerX();
-            int centerY = mTmpStartRect.centerY();
-            int halfWidth = mTmpStartRect.width() / 2;
-            int halfHeight = mTmpStartRect.height() / 2;
+            int centerX = mTmpRect.centerX();
+            int centerY = mTmpRect.centerY();
+            int halfWidth = mTmpRect.width() / 2;
+            int halfHeight = mTmpRect.height() / 2;
 
             // Clip third of the from size of launch icon, expand to full width/height
             Animation clipAnimLR = new ClipRectLRAnimation(
@@ -693,19 +692,19 @@
 
         float scaleW = appWidth / thumbWidth;
         float unscaledHeight = thumbHeight * scaleW;
-        getNextAppTransitionStartRect(taskId, mTmpStartRect);
-        float unscaledStartY = mTmpStartRect.top - (unscaledHeight - thumbHeight) / 2f;
+        getNextAppTransitionStartRect(taskId, mTmpRect);
+        float unscaledStartY = mTmpRect.top - (unscaledHeight - thumbHeight) / 2f;
         if (mNextAppTransitionScaleUp) {
             // Animation up from the thumbnail to the full screen
             Animation scale = new ScaleAnimation(1f, scaleW, 1f, scaleW,
-                    mTmpStartRect.left + (thumbWidth / 2f), mTmpStartRect.top + (thumbHeight / 2f));
+                    mTmpRect.left + (thumbWidth / 2f), mTmpRect.top + (thumbHeight / 2f));
             scale.setInterpolator(mTouchResponseInterpolator);
             scale.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
             Animation alpha = new AlphaAnimation(1, 0);
             alpha.setInterpolator(mThumbnailFadeOutInterpolator);
             alpha.setDuration(THUMBNAIL_APP_TRANSITION_ALPHA_DURATION);
             final float toX = appRect.left + appRect.width() / 2 -
-                    (mTmpStartRect.left + thumbWidth / 2);
+                    (mTmpRect.left + thumbWidth / 2);
             final float toY = appRect.top + mNextAppTransitionInsets.top + -unscaledStartY;
             Animation translate = new TranslateAnimation(0, toX, 0, toY);
             translate.setInterpolator(mTouchResponseInterpolator);
@@ -720,7 +719,7 @@
         } else {
             // Animation down from the full screen to the thumbnail
             Animation scale = new ScaleAnimation(scaleW, 1f, scaleW, 1f,
-                    mTmpStartRect.left + (thumbWidth / 2f), mTmpStartRect.top + (thumbHeight / 2f));
+                    mTmpRect.left + (thumbWidth / 2f), mTmpRect.top + (thumbHeight / 2f));
             scale.setInterpolator(mTouchResponseInterpolator);
             scale.setDuration(THUMBNAIL_APP_TRANSITION_DURATION);
             Animation alpha = new AlphaAnimation(0f, 1f);
@@ -753,10 +752,10 @@
         Animation a;
         final int appWidth = containingFrame.width();
         final int appHeight = containingFrame.height();
-        getDefaultNextAppTransitionStartRect(mTmpStartRect);
-        final int thumbWidthI = mTmpStartRect.width();
+        getDefaultNextAppTransitionStartRect(mTmpRect);
+        final int thumbWidthI = mTmpRect.width();
         final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1;
-        final int thumbHeightI = mTmpStartRect.height();
+        final int thumbHeightI = mTmpRect.height();
         final float thumbHeight = thumbHeightI > 0 ? thumbHeightI : 1;
 
         // Used for the ENTER_SCALE_UP and EXIT_SCALE_DOWN transitions
@@ -766,7 +765,7 @@
         switch (thumbTransitState) {
             case THUMBNAIL_TRANSITION_ENTER_SCALE_UP: {
                 if (freeform) {
-                    a = createAspectScaledThumbnailEnterNonFullscreenAnimationLocked(
+                    a = createAspectScaledThumbnailEnterFreeformAnimationLocked(
                             containingFrame, surfaceInsets, taskId);
                 } else {
                     mTmpFromClipRect.set(containingFrame);
@@ -797,8 +796,8 @@
                     mNextAppTransitionInsets.set(contentInsets);
 
                     Animation scaleAnim = new ScaleAnimation(scale, 1, scale, 1,
-                            computePivot(mTmpStartRect.left, scale),
-                            computePivot(mTmpStartRect.top, scale));
+                            computePivot(mTmpRect.left, scale),
+                            computePivot(mTmpRect.top, scale));
                     Animation clipAnim = new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect);
                     Animation translateAnim = new TranslateAnimation(0, 0, -scaledTopDecor, 0);
 
@@ -834,44 +833,49 @@
             }
             case THUMBNAIL_TRANSITION_EXIT_SCALE_DOWN: {
                 // App window scaling down from full screen
-                mTmpFromClipRect.set(containingFrame);
-                mTmpToClipRect.set(containingFrame);
-                // exclude top screen decor (status bar) region from the destination clip.
-                mTmpToClipRect.top = contentInsets.top;
-                if (orientation == Configuration.ORIENTATION_PORTRAIT) {
-                    // In portrait, we scale the width and clip to the top/left square
-                    scale = thumbWidth / appWidth;
-                    scaledTopDecor = (int) (scale * contentInsets.top);
-                    int unscaledThumbHeight = (int) (thumbHeight / scale);
-                    mTmpToClipRect.bottom = mTmpToClipRect.top + unscaledThumbHeight;
+                if (freeform) {
+                    a = createAspectScaledThumbnailExitFreeformAnimationLocked(
+                            containingFrame, surfaceInsets, taskId);
                 } else {
-                    // In landscape, we scale the height and clip to the top/left square. We only
-                    // scale the part that is not covered by status bar and the nav bar.
-                    scale = thumbHeight / (appHeight - contentInsets.top - contentInsets.bottom);
-                    scaledTopDecor = (int) (scale * contentInsets.top);
-                    int unscaledThumbWidth = (int) (thumbWidth / scale);
-                    mTmpToClipRect.right = mTmpToClipRect.left + unscaledThumbWidth;
-                    // This removes the navigation bar from the last frame, so it better matches the
-                    // thumbnail. We need to do this explicitly in landscape, because in portrait we
-                    // already crop vertically.
-                    mTmpToClipRect.bottom = mTmpToClipRect.bottom - contentInsets.bottom;
+                    mTmpFromClipRect.set(containingFrame);
+                    mTmpToClipRect.set(containingFrame);
+                    // exclude top screen decor (status bar) region from the destination clip.
+                    mTmpToClipRect.top = contentInsets.top;
+                    if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+                        // In portrait, we scale the width and clip to the top/left square
+                        scale = thumbWidth / appWidth;
+                        scaledTopDecor = (int) (scale * contentInsets.top);
+                        int unscaledThumbHeight = (int) (thumbHeight / scale);
+                        mTmpToClipRect.bottom = mTmpToClipRect.top + unscaledThumbHeight;
+                    } else {
+                        // In landscape, we scale the height and clip to the top/left square. We only
+                        // scale the part that is not covered by status bar and the nav bar.
+                        scale = thumbHeight / (appHeight - contentInsets.top - contentInsets.bottom);
+                        scaledTopDecor = (int) (scale * contentInsets.top);
+                        int unscaledThumbWidth = (int) (thumbWidth / scale);
+                        mTmpToClipRect.right = mTmpToClipRect.left + unscaledThumbWidth;
+                        // This removes the navigation bar from the last frame, so it better matches the
+                        // thumbnail. We need to do this explicitly in landscape, because in portrait we
+                        // already crop vertically.
+                        mTmpToClipRect.bottom = mTmpToClipRect.bottom - contentInsets.bottom;
+                    }
+
+                    mNextAppTransitionInsets.set(contentInsets);
+
+                    Animation scaleAnim = new ScaleAnimation(1, scale, 1, scale,
+                            computePivot(mTmpRect.left, scale),
+                            computePivot(mTmpRect.top, scale));
+                    Animation clipAnim = new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect);
+                    Animation translateAnim = new TranslateAnimation(0, 0, 0, -scaledTopDecor);
+
+                    AnimationSet set = new AnimationSet(true);
+                    set.addAnimation(clipAnim);
+                    set.addAnimation(scaleAnim);
+                    set.addAnimation(translateAnim);
+
+                    a = set;
+                    a.setZAdjustment(Animation.ZORDER_TOP);
                 }
-
-                mNextAppTransitionInsets.set(contentInsets);
-
-                Animation scaleAnim = new ScaleAnimation(1, scale, 1, scale,
-                        computePivot(mTmpStartRect.left, scale),
-                        computePivot(mTmpStartRect.top, scale));
-                Animation clipAnim = new ClipRectAnimation(mTmpFromClipRect, mTmpToClipRect);
-                Animation translateAnim = new TranslateAnimation(0, 0, 0, -scaledTopDecor);
-
-                AnimationSet set = new AnimationSet(true);
-                set.addAnimation(clipAnim);
-                set.addAnimation(scaleAnim);
-                set.addAnimation(translateAnim);
-
-                a = set;
-                a.setZAdjustment(Animation.ZORDER_TOP);
                 break;
             }
             default:
@@ -884,27 +888,48 @@
                 mTouchResponseInterpolator);
     }
 
-    private Animation createAspectScaledThumbnailEnterNonFullscreenAnimationLocked(
-            Rect frame, @Nullable Rect surfaceInsets, int taskId) {
-        getNextAppTransitionStartRect(taskId, mTmpStartRect);
-        float width = frame.width();
-        float height = frame.height();
-        float scaleWidth = mTmpStartRect.width() / width;
-        float scaleHeight = mTmpStartRect.height() / height;
+    private Animation createAspectScaledThumbnailEnterFreeformAnimationLocked(Rect frame,
+            @Nullable Rect surfaceInsets, int taskId) {
+        getNextAppTransitionStartRect(taskId, mTmpRect);
+        return createAspectScaledThumbnailFreeformAnimationLocked(mTmpRect, frame, surfaceInsets,
+                true);
+    }
+
+    private Animation createAspectScaledThumbnailExitFreeformAnimationLocked(Rect frame,
+            @Nullable Rect surfaceInsets, int taskId) {
+        getNextAppTransitionStartRect(taskId, mTmpRect);
+        return createAspectScaledThumbnailFreeformAnimationLocked(frame, mTmpRect, surfaceInsets,
+                false);
+    }
+
+    private AnimationSet createAspectScaledThumbnailFreeformAnimationLocked(Rect sourceFrame,
+            Rect destFrame, @Nullable Rect surfaceInsets, boolean enter) {
+        final float sourceWidth = sourceFrame.width();
+        final float sourceHeight = sourceFrame.height();
+        final float destWidth = destFrame.width();
+        final float destHeight = destFrame.height();
+        final float scaleH = enter ? sourceWidth / destWidth : destWidth / sourceWidth;
+        final float scaleV = enter ? sourceHeight / destHeight : destHeight / sourceHeight;
         AnimationSet set = new AnimationSet(true);
-        int surfaceInsetsHorizontal = surfaceInsets == null
+        final int surfaceInsetsH = surfaceInsets == null
                 ? 0 : surfaceInsets.left + surfaceInsets.right;
-        int surfaceInsetsVertical = surfaceInsets == null
+        final int surfaceInsetsV = surfaceInsets == null
                 ? 0 : surfaceInsets.top + surfaceInsets.bottom;
         // We want the scaling to happen from the center of the surface. In order to achieve that,
         // we need to account for surface insets that will be used to enlarge the surface.
-        ScaleAnimation scale = new ScaleAnimation(scaleWidth, 1, scaleHeight, 1,
-                (width + surfaceInsetsHorizontal) / 2, (height + surfaceInsetsVertical) / 2);
-        int fromX = mTmpStartRect.left + mTmpStartRect.width() / 2
-                - (frame.left + frame.width() / 2);
-        int fromY = mTmpStartRect.top + mTmpStartRect.height() / 2
-                - (frame.top + frame.height() / 2);
-        TranslateAnimation translation = new TranslateAnimation(fromX, 0, fromY, 0);
+        final float scaleHCenter = ((enter ? destWidth : sourceWidth) + surfaceInsetsH) / 2;
+        final float scaleVCenter = ((enter ? destHeight : sourceHeight) + surfaceInsetsV) / 2;
+        final ScaleAnimation scale = enter ?
+                new ScaleAnimation(scaleH, 1, scaleV, 1, scaleHCenter, scaleVCenter)
+                : new ScaleAnimation(1, scaleH, 1, scaleV, scaleHCenter, scaleVCenter);
+        final int sourceHCenter = sourceFrame.left + sourceFrame.width() / 2;
+        final int sourceVCenter = sourceFrame.top + sourceFrame.height() / 2;
+        final int destHCenter = destFrame.left + destFrame.width() / 2;
+        final int destVCenter = destFrame.top + destFrame.height() / 2;
+        final int fromX = enter ? sourceHCenter - destHCenter : destHCenter - sourceHCenter;
+        final int fromY = enter ? sourceVCenter - destVCenter : destVCenter - sourceVCenter;
+        final TranslateAnimation translation = enter ? new TranslateAnimation(fromX, 0, fromY, 0)
+                : new TranslateAnimation(0, fromX, 0, fromY);
         set.addAnimation(scale);
         set.addAnimation(translation);
         return set;
@@ -917,7 +942,7 @@
     Animation createThumbnailScaleAnimationLocked(int appWidth, int appHeight, int transit,
             Bitmap thumbnailHeader) {
         Animation a;
-        getDefaultNextAppTransitionStartRect(mTmpStartRect);
+        getDefaultNextAppTransitionStartRect(mTmpRect);
         final int thumbWidthI = thumbnailHeader.getWidth();
         final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1;
         final int thumbHeightI = thumbnailHeader.getHeight();
@@ -928,8 +953,8 @@
             float scaleW = appWidth / thumbWidth;
             float scaleH = appHeight / thumbHeight;
             Animation scale = new ScaleAnimation(1, scaleW, 1, scaleH,
-                    computePivot(mTmpStartRect.left, 1 / scaleW),
-                    computePivot(mTmpStartRect.top, 1 / scaleH));
+                    computePivot(mTmpRect.left, 1 / scaleW),
+                    computePivot(mTmpRect.top, 1 / scaleH));
             scale.setInterpolator(mDecelerateInterpolator);
 
             Animation alpha = new AlphaAnimation(1, 0);
@@ -945,8 +970,8 @@
             float scaleW = appWidth / thumbWidth;
             float scaleH = appHeight / thumbHeight;
             a = new ScaleAnimation(scaleW, 1, scaleH, 1,
-                    computePivot(mTmpStartRect.left, 1 / scaleW),
-                    computePivot(mTmpStartRect.top, 1 / scaleH));
+                    computePivot(mTmpRect.left, 1 / scaleW),
+                    computePivot(mTmpRect.top, 1 / scaleH));
         }
 
         return prepareThumbnailAnimation(a, appWidth, appHeight, transit);
@@ -962,7 +987,7 @@
         final int appHeight = containingFrame.height();
         Bitmap thumbnailHeader = getAppTransitionThumbnailHeader(taskId);
         Animation a;
-        getDefaultNextAppTransitionStartRect(mTmpStartRect);
+        getDefaultNextAppTransitionStartRect(mTmpRect);
         final int thumbWidthI = thumbnailHeader != null ? thumbnailHeader.getWidth() : appWidth;
         final float thumbWidth = thumbWidthI > 0 ? thumbWidthI : 1;
         final int thumbHeightI = thumbnailHeader != null ? thumbnailHeader.getHeight() : appHeight;
@@ -974,8 +999,8 @@
                 float scaleW = thumbWidth / appWidth;
                 float scaleH = thumbHeight / appHeight;
                 a = new ScaleAnimation(scaleW, 1, scaleH, 1,
-                        computePivot(mTmpStartRect.left, scaleW),
-                        computePivot(mTmpStartRect.top, scaleH));
+                        computePivot(mTmpRect.left, scaleW),
+                        computePivot(mTmpRect.top, scaleH));
                 break;
             }
             case THUMBNAIL_TRANSITION_EXIT_SCALE_UP: {
@@ -1002,8 +1027,8 @@
                 float scaleW = thumbWidth / appWidth;
                 float scaleH = thumbHeight / appHeight;
                 Animation scale = new ScaleAnimation(1, scaleW, 1, scaleH,
-                        computePivot(mTmpStartRect.left, scaleW),
-                        computePivot(mTmpStartRect.top, scaleH));
+                        computePivot(mTmpRect.left, scaleW),
+                        computePivot(mTmpRect.top, scaleH));
 
                 Animation alpha = new AlphaAnimation(1, 0);
 
@@ -1492,15 +1517,15 @@
                         pw.print(Integer.toHexString(mNextAppTransitionInPlace));
                 break;
             case NEXT_TRANSIT_TYPE_SCALE_UP: {
-                getDefaultNextAppTransitionStartRect(mTmpStartRect);
+                getDefaultNextAppTransitionStartRect(mTmpRect);
                 pw.print(prefix); pw.print("mNextAppTransitionStartX=");
-                        pw.print(mTmpStartRect.left);
+                        pw.print(mTmpRect.left);
                         pw.print(" mNextAppTransitionStartY=");
-                        pw.println(mTmpStartRect.top);
+                        pw.println(mTmpRect.top);
                 pw.print(prefix); pw.print("mNextAppTransitionStartWidth=");
-                        pw.print(mTmpStartRect.width());
+                        pw.print(mTmpRect.width());
                         pw.print(" mNextAppTransitionStartHeight=");
-                        pw.println(mTmpStartRect.height());
+                        pw.println(mTmpRect.height());
                 break;
             }
             case NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP: