[PIP2] Add bounds animator for enter/exit pip

Adds bounds animator for entering and exiting pip. This currently
implements the basic portrait case, and a follow up CL will add
rotation.

Test: manual check
Flag: com.android.wm.shell.enable_pip2_implementation
Bug: b/350801661
Change-Id: Ib570f7fa4f29cf6aa303e5c23fed7de7ffc63b6d
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java
new file mode 100644
index 0000000..8a9302b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.pip2.animation;
+
+import android.animation.Animator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Animator that handles bounds animations for entering / exiting PIP.
+ */
+public class PipEnterExitAnimator extends ValueAnimator
+        implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
+    @IntDef(prefix = {"BOUNDS_"}, value = {
+            BOUNDS_ENTER,
+            BOUNDS_EXIT
+    })
+
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BOUNDS {}
+
+    public static final int BOUNDS_ENTER = 0;
+    public static final int BOUNDS_EXIT = 1;
+
+    @NonNull private final SurfaceControl mLeash;
+    private final SurfaceControl.Transaction mStartTransaction;
+    private final int mEnterAnimationDuration;
+    private final @BOUNDS int mDirection;
+
+    // optional callbacks for tracking animation start and end
+    @Nullable private Runnable mAnimationStartCallback;
+    @Nullable private Runnable mAnimationEndCallback;
+
+    private final Rect mBaseBounds = new Rect();
+    private final Rect mStartBounds = new Rect();
+    private final Rect mEndBounds = new Rect();
+
+    // Bounds updated by the evaluator as animator is running.
+    private final Rect mAnimatedRect = new Rect();
+
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final RectEvaluator mRectEvaluator;
+    private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
+
+    public PipEnterExitAnimator(Context context,
+            @NonNull SurfaceControl leash,
+            SurfaceControl.Transaction startTransaction,
+            @NonNull Rect baseBounds,
+            @NonNull Rect startBounds,
+            @NonNull Rect endBounds,
+            @BOUNDS int direction) {
+        mLeash = leash;
+        mStartTransaction = startTransaction;
+        mBaseBounds.set(baseBounds);
+        mStartBounds.set(startBounds);
+        mAnimatedRect.set(startBounds);
+        mEndBounds.set(endBounds);
+        mRectEvaluator = new RectEvaluator(mAnimatedRect);
+        mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(context);
+        mDirection = direction;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+        mEnterAnimationDuration = context.getResources()
+                .getInteger(R.integer.config_pipEnterAnimationDuration);
+
+        setDuration(mEnterAnimationDuration);
+        setEvaluator(mRectEvaluator);
+        addListener(this);
+        addUpdateListener(this);
+    }
+
+    public void setAnimationStartCallback(@NonNull Runnable runnable) {
+        mAnimationStartCallback = runnable;
+    }
+
+    public void setAnimationEndCallback(@NonNull Runnable runnable) {
+        mAnimationEndCallback = runnable;
+    }
+
+    @Override
+    public void onAnimationStart(@NonNull Animator animation) {
+        if (mAnimationStartCallback != null) {
+            mAnimationStartCallback.run();
+        }
+        if (mStartTransaction != null) {
+            mStartTransaction.apply();
+        }
+    }
+
+    @Override
+    public void onAnimationEnd(@NonNull Animator animation) {
+        if (mAnimationEndCallback != null) {
+            mAnimationEndCallback.run();
+        }
+    }
+
+    @Override
+    public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+        final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+        final float fraction = getAnimatedFraction();
+        // TODO (b/350801661): implement fixed rotation
+
+        mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, null,
+                mBaseBounds, mAnimatedRect, null, isInPipDirection(), fraction)
+                .round(tx, mLeash, isInPipDirection())
+                .shadow(tx, mLeash, isInPipDirection());
+        tx.apply();
+    }
+
+    private boolean isInPipDirection() {
+        return mDirection == BOUNDS_ENTER;
+    }
+
+    // no-ops
+
+    @Override
+    public void onAnimationCancel(@NonNull Animator animation) {}
+
+    @Override
+    public void onAnimationRepeat(@NonNull Animator animation) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 683d30d..33703ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -53,6 +53,7 @@
 import com.android.wm.shell.pip.PipContentOverlay;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
+import com.android.wm.shell.pip2.animation.PipEnterExitAnimator;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
@@ -378,12 +379,34 @@
         if (pipChange == null) {
             return false;
         }
-        // cache the PiP task token and leash
-        WindowContainerToken pipTaskToken = pipChange.getContainer();
 
-        startTransaction.apply();
-        // TODO: b/275910498 Use a new implementation of the PiP animator here.
-        finishCallback.onTransitionFinished(null);
+        WindowContainerToken pipTaskToken = pipChange.getContainer();
+        if (pipTaskToken == null) {
+            return false;
+        }
+
+        WindowContainerTransaction finishWct = new WindowContainerTransaction();
+        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+
+        Rect startBounds = pipChange.getStartAbsBounds();
+        Rect endBounds = pipChange.getEndAbsBounds();
+        SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+        Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+
+        PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash,
+                startTransaction, startBounds, startBounds, endBounds,
+                PipEnterExitAnimator.BOUNDS_ENTER);
+
+        tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
+                this::onClientDrawAtTransitionEnd);
+        finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
+
+        animator.setAnimationEndCallback(() -> {
+            mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
+            finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
+        });
+
+        animator.start();
         return true;
     }
 
@@ -421,10 +444,25 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        startTransaction.apply();
-        // TODO: b/275910498 Use a new implementation of the PiP animator here.
-        finishCallback.onTransitionFinished(null);
-        mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+        TransitionInfo.Change pipChange = getPipChange(info);
+        if (pipChange == null) {
+            return false;
+        }
+
+        Rect startBounds = pipChange.getStartAbsBounds();
+        Rect endBounds = pipChange.getEndAbsBounds();
+        SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+        Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+
+        PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash,
+                startTransaction, startBounds, startBounds, endBounds,
+                PipEnterExitAnimator.BOUNDS_EXIT);
+        animator.setAnimationEndCallback(() -> {
+            finishCallback.onTransitionFinished(null);
+            mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+        });
+
+        animator.start();
         return true;
     }