Replace setFinalCrop with temporary layer

- Creates a temporary animation bounds layer when setting up the
  animation in AppWindowToken
- The leash is parented to this new layer if the appStackClipMode
   is set to STACK_CLIP_AFTER_ANIM

Test: Manual test - open activity in split screen
Test: atest FrameworksServicesTests:AppWindowTokenAnimationTests
Change-Id: I6a9cf99832ee868a6e65da0150291d521f5eca35
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index d9ddf9f..fe8ca28 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -78,6 +78,7 @@
 import static com.android.server.wm.AppWindowTokenProto.STARTING_WINDOW;
 import static com.android.server.wm.AppWindowTokenProto.THUMBNAIL;
 import static com.android.server.wm.AppWindowTokenProto.WINDOW_TOKEN;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
 
 import android.annotation.CallSuper;
 import android.app.Activity;
@@ -264,6 +265,12 @@
      */
     private boolean mWillCloseOrEnterPip;
 
+    /** Layer used to constrain the animation to a token's stack bounds. */
+    SurfaceControl mAnimationBoundsLayer;
+
+    /** Whether this token needs to create mAnimationBoundsLayer for cropping animations. */
+    boolean mNeedsAnimationBoundsLayer;
+
     AppWindowToken(WindowManagerService service, IApplicationToken token, boolean voiceInteraction,
             DisplayContent dc, long inputDispatchingTimeoutNanos, boolean fullscreen,
             boolean showForAllUsers, int targetSdk, int orientation, int rotationAnimationHint,
@@ -1720,6 +1727,20 @@
         return !isSplitScreenPrimary || allowSplitScreenPrimaryAnimation;
     }
 
+    /**
+     * Creates a layer to apply crop to an animation.
+     */
+    private SurfaceControl createAnimationBoundsLayer(Transaction t) {
+        if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.i(TAG, "Creating animation bounds layer");
+        final SurfaceControl.Builder builder = makeAnimationLeash()
+                .setParent(getAnimationLeashParent())
+                .setName(getSurfaceControl() + " - animation-bounds")
+                .setSize(getSurfaceWidth(), getSurfaceHeight());
+        final SurfaceControl boundsLayer = builder.build();
+        t.show(boundsLayer);
+        return boundsLayer;
+    }
+
     boolean applyAnimationLocked(WindowManager.LayoutParams lp, int transit, boolean enter,
             boolean isVoiceInteraction) {
 
@@ -1753,12 +1774,15 @@
                 adapter = mService.mAppTransition.getRemoteAnimationController()
                         .createAnimationAdapter(this, mTmpPoint, mTmpRect);
             } else {
+                final int appStackClipMode = mService.mAppTransition.getAppStackClipMode();
+                mNeedsAnimationBoundsLayer = (appStackClipMode == STACK_CLIP_AFTER_ANIM);
+
                 final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
                 if (a != null) {
                     adapter = new LocalAnimationAdapter(
                             new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
                                     mService.mAppTransition.canSkipFirstFrame(),
-                                    mService.mAppTransition.getAppStackClipMode(),
+                                    appStackClipMode,
                                     true /* isAppAnimation */),
                             mService.mSurfaceAnimationRunner);
                     if (a.getZAdjustment() == Animation.ZORDER_TOP) {
@@ -1858,6 +1882,11 @@
     @Override
     public void onAnimationLeashDestroyed(Transaction t) {
         super.onAnimationLeashDestroyed(t);
+        if (mAnimationBoundsLayer != null) {
+            t.destroy(mAnimationBoundsLayer);
+            mAnimationBoundsLayer = null;
+        }
+
         if (mAnimatingAppWindowTokenRegistry != null) {
             mAnimatingAppWindowTokenRegistry.notifyFinished(this);
         }
@@ -1908,6 +1937,26 @@
         if (mAnimatingAppWindowTokenRegistry != null) {
             mAnimatingAppWindowTokenRegistry.notifyStarting(this);
         }
+
+        // If the animation needs to be cropped then an animation bounds layer is created as a child
+        // of the pinned stack or animation layer. The leash is then reparented to this new layer.
+        if (mNeedsAnimationBoundsLayer) {
+            final TaskStack stack = getStack();
+            if (stack == null) {
+                return;
+            }
+            mAnimationBoundsLayer = createAnimationBoundsLayer(t);
+
+            // Set clip rect to stack bounds.
+            mTmpRect.setEmpty();
+            stack.getBounds(mTmpRect);
+
+            // Crop to stack bounds.
+            t.setWindowCrop(mAnimationBoundsLayer, mTmpRect);
+
+            // Reparent leash to animation bounds layer.
+            t.reparent(leash, mAnimationBoundsLayer.getHandle());
+        }
     }
 
     /**
@@ -1927,6 +1976,7 @@
         mTransit = TRANSIT_UNSET;
         mTransitFlags = 0;
         mNeedsZBoost = false;
+        mNeedsAnimationBoundsLayer = false;
 
         setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER,
                 "AppWindowToken");
diff --git a/services/core/java/com/android/server/wm/WindowAnimationSpec.java b/services/core/java/com/android/server/wm/WindowAnimationSpec.java
index 548e23a..825255e 100644
--- a/services/core/java/com/android/server/wm/WindowAnimationSpec.java
+++ b/services/core/java/com/android/server/wm/WindowAnimationSpec.java
@@ -99,13 +99,7 @@
         tmp.transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
         t.setMatrix(leash, tmp.transformation.getMatrix(), tmp.floats);
         t.setAlpha(leash, tmp.transformation.getAlpha());
-        if (mStackClipMode == STACK_CLIP_NONE) {
-            t.setWindowCrop(leash, tmp.transformation.getClipRect());
-        } else if (mStackClipMode == STACK_CLIP_AFTER_ANIM) {
-            mTmpRect.set(mStackBounds);
-            // Offset stack bounds to stack position so the final crop is in screen space.
-            mTmpRect.offsetTo(mPosition.x, mPosition.y);
-            t.setFinalCrop(leash, mTmpRect);
+        if (mStackClipMode == STACK_CLIP_NONE || mStackClipMode == STACK_CLIP_AFTER_ANIM) {
             t.setWindowCrop(leash, tmp.transformation.getClipRect());
         } else {
             mTmpRect.set(mStackBounds);
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
new file mode 100644
index 0000000..c3907ff
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.SurfaceControl.Transaction;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+
+import com.android.server.wm.WindowTestUtils.TestAppWindowToken;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Animation related tests for the {@link AppWindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.AppWindowTokenAnimationTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppWindowTokenAnimationTests extends WindowTestsBase {
+
+    private TestAppWindowToken mToken;
+
+    @Mock
+    private Transaction mTransaction;
+    @Mock
+    private AnimationAdapter mSpec;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+
+        mToken = createTestAppWindowToken(mDisplayContent, WINDOWING_MODE_FULLSCREEN,
+                ACTIVITY_TYPE_STANDARD);
+        mToken.setPendingTransaction(mTransaction);
+    }
+
+    @Test
+    public void clipAfterAnim_boundsLayerIsCreated() throws Exception {
+        mToken.mNeedsAnimationBoundsLayer = true;
+
+        mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+        verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
+                eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+        verify(mTransaction).reparent(eq(mToken.mSurfaceAnimator.mLeash),
+                eq(mToken.mAnimationBoundsLayer.getHandle()));
+    }
+
+    @Test
+    public void clipAfterAnim_boundsLayerIsDestroyed() throws Exception {
+        mToken.mNeedsAnimationBoundsLayer = true;
+        mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+        final SurfaceControl leash = mToken.mSurfaceAnimator.mLeash;
+        final SurfaceControl animationBoundsLayer = mToken.mAnimationBoundsLayer;
+        final ArgumentCaptor<SurfaceAnimator.OnAnimationFinishedCallback> callbackCaptor =
+                ArgumentCaptor.forClass(
+                        SurfaceAnimator.OnAnimationFinishedCallback.class);
+        verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+        callbackCaptor.getValue().onAnimationFinished(mSpec);
+        verify(mTransaction).destroy(eq(leash));
+        verify(mTransaction).destroy(eq(animationBoundsLayer));
+        assertThat(mToken.mNeedsAnimationBoundsLayer).isFalse();
+    }
+
+    @Test
+    public void clipAfterAnimCancelled_boundsLayerIsDestroyed() throws Exception {
+        mToken.mNeedsAnimationBoundsLayer = true;
+        mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+        final SurfaceControl leash = mToken.mSurfaceAnimator.mLeash;
+        final SurfaceControl animationBoundsLayer = mToken.mAnimationBoundsLayer;
+
+        mToken.mSurfaceAnimator.cancelAnimation();
+        verify(mTransaction).destroy(eq(leash));
+        verify(mTransaction).destroy(eq(animationBoundsLayer));
+        assertThat(mToken.mNeedsAnimationBoundsLayer).isFalse();
+    }
+
+    @Test
+    public void clipNoneAnim_boundsLayerIsNotCreated() throws Exception {
+        mToken.mNeedsAnimationBoundsLayer = false;
+
+        mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+        verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
+                eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+        assertThat(mToken.mAnimationBoundsLayer).isNull();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
index ca520ed..9dc0025 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
@@ -70,8 +70,6 @@
                 true /* isAppAnimation */);
         windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
         verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
-        verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
-                argThat(rect -> rect.equals(mStackBounds)));
     }
 
     @Test
@@ -83,9 +81,6 @@
                 true /* isAppAnimation */);
         windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
         verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
-        verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
-                argThat(rect -> rect.left == 20 && rect.top == 40 && rect.right == 30
-                        && rect.bottom == 50));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
index 2e4740b..a48a3b9 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
@@ -24,6 +24,8 @@
 import android.os.IBinder;
 import android.view.IApplicationToken;
 import android.view.IWindow;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 import android.view.WindowManager;
 
 import static android.app.AppOpsManager.OP_NONE;
@@ -113,6 +115,7 @@
     /** Used so we can gain access to some protected members of the {@link AppWindowToken} class. */
     public static class TestAppWindowToken extends AppWindowToken {
         boolean mOnTop = false;
+        private Transaction mPendingTransactionOverride;
 
         private TestAppWindowToken(DisplayContent dc) {
             super(dc.mService, new IApplicationToken.Stub() {
@@ -158,6 +161,17 @@
         boolean isOnTop() {
             return mOnTop;
         }
+
+        void setPendingTransaction(Transaction transaction) {
+            mPendingTransactionOverride = transaction;
+        }
+
+        @Override
+        public Transaction getPendingTransaction() {
+            return mPendingTransactionOverride == null
+                    ? super.getPendingTransaction()
+                    : mPendingTransactionOverride;
+        }
     }
 
     static TestWindowToken createTestWindowToken(int type, DisplayContent dc) {
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
index 473a287..ef019fe 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -231,6 +231,11 @@
     }
 
     AppWindowToken createAppWindowToken(DisplayContent dc, int windowingMode, int activityType) {
+        return createTestAppWindowToken(dc, windowingMode, activityType);
+    }
+
+    WindowTestUtils.TestAppWindowToken createTestAppWindowToken(DisplayContent dc, int
+            windowingMode, int activityType) {
         final TaskStack stack = createStackControllerOnStackOnDisplay(windowingMode, activityType,
                 dc).mContainer;
         final Task task = createTaskInStack(stack, 0 /* userId */);