Merge "Change TV PiP enter/exit transitions" into main
diff --git a/libs/WindowManager/Shell/res/values/config_tv.xml b/libs/WindowManager/Shell/res/values/config_tv.xml
new file mode 100644
index 0000000..3da5539
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/config_tv.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2023 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.
+-->
+<resources>
+    <integer name="config_tvPipEnterFadeOutDuration">500</integer>
+    <integer name="config_tvPipEnterFadeInDuration">1500</integer>
+    <integer name="config_tvPipExitFadeOutDuration">500</integer>
+    <integer name="config_tvPipExitFadeInDuration">500</integer>
+</resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index d520ff7..8b6c7b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -258,7 +258,7 @@
             ActivityTaskManager.getService().onPictureInPictureStateChanged(
                     new PictureInPictureUiState(stashedState != STASH_TYPE_NONE /* isStashed */)
             );
-        } catch (RemoteException e) {
+        } catch (RemoteException | IllegalStateException e) {
             ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: Unable to set alert PiP state change.", TAG);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index a9675f9..1947097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -20,6 +20,8 @@
 import android.os.Handler;
 import android.os.SystemClock;
 
+import androidx.annotation.NonNull;
+
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.common.DisplayController;
@@ -41,7 +43,6 @@
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipTaskOrganizer;
-import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
 import com.android.wm.shell.pip.tv.TvPipBoundsController;
@@ -78,11 +79,12 @@
             PipDisplayLayoutState pipDisplayLayoutState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             TvPipBoundsController tvPipBoundsController,
+            PipTransitionState pipTransitionState,
             PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             TvPipMenuController tvPipMenuController,
             PipMediaController pipMediaController,
-            PipTransitionController pipTransitionController,
+            TvPipTransition tvPipTransition,
             TvPipNotificationController tvPipNotificationController,
             TaskStackListenerImpl taskStackListener,
             PipParamsChangedForwarder pipParamsChangedForwarder,
@@ -99,9 +101,10 @@
                         pipDisplayLayoutState,
                         tvPipBoundsAlgorithm,
                         tvPipBoundsController,
+                        pipTransitionState,
                         pipAppOpsListener,
                         pipTaskOrganizer,
-                        pipTransitionController,
+                        tvPipTransition,
                         tvPipMenuController,
                         pipMediaController,
                         tvPipNotificationController,
@@ -151,25 +154,23 @@
         return new LegacySizeSpecSource(context, pipDisplayLayoutState);
     }
 
-    // Handler needed for loadDrawableAsync() in PipControlsViewController
     @WMSingleton
     @Provides
-    static PipTransitionController provideTvPipTransition(
+    static TvPipTransition provideTvPipTransition(
             Context context,
-            ShellInit shellInit,
-            ShellTaskOrganizer shellTaskOrganizer,
-            Transitions transitions,
+            @NonNull ShellInit shellInit,
+            @NonNull ShellTaskOrganizer shellTaskOrganizer,
+            @NonNull Transitions transitions,
             TvPipBoundsState tvPipBoundsState,
-            PipDisplayLayoutState pipDisplayLayoutState,
-            PipTransitionState pipTransitionState,
-            TvPipMenuController pipMenuController,
+            TvPipMenuController tvPipMenuController,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            PipTransitionState pipTransitionState,
             PipAnimationController pipAnimationController,
-            PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+            PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
+            PipDisplayLayoutState pipDisplayLayoutState) {
         return new TvPipTransition(context, shellInit, shellTaskOrganizer, transitions,
-                tvPipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController,
-                tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
-                Optional.empty());
+                tvPipBoundsState, tvPipMenuController, tvPipBoundsAlgorithm, pipTransitionState,
+                pipAnimationController, pipSurfaceTransactionHelper, pipDisplayLayoutState);
     }
 
     @WMSingleton
@@ -207,7 +208,7 @@
             PipTransitionState pipTransitionState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             PipAnimationController pipAnimationController,
-            PipTransitionController pipTransitionController,
+            TvPipTransition tvPipTransition,
             PipParamsChangedForwarder pipParamsChangedForwarder,
             PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
             Optional<SplitScreenController> splitScreenControllerOptional,
@@ -217,7 +218,7 @@
         return new TvPipTaskOrganizer(context,
                 syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState,
                 tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController,
-                pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+                pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
                 splitScreenControllerOptional, displayController, pipUiEventLogger,
                 shellTaskOrganizer, mainExecutor);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index cbed4b5..a58d94e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -81,15 +81,35 @@
      */
     public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
             Rect sourceBounds, Rect destinationBounds) {
+        mTmpDestinationRectF.set(destinationBounds);
+        return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, RectF destinationBounds) {
         return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */);
     }
 
     /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, Rect destinationBounds, float degrees) {
+        mTmpDestinationRectF.set(destinationBounds);
+        return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees);
+    }
+
+    /**
      * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation.
      * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
      */
     public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
-            Rect sourceBounds, Rect destinationBounds, float degrees) {
+            Rect sourceBounds, RectF destinationBounds, float degrees) {
         mTmpSourceRectF.set(sourceBounds);
         // We want the matrix to position the surface relative to the screen coordinates so offset
         // the source to 0,0
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index c05601b..b4067d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -297,9 +297,9 @@
     // changed RunningTaskInfo when it finishes.
     private ActivityManager.RunningTaskInfo mDeferredTaskInfo;
     private WindowContainerToken mToken;
-    private SurfaceControl mLeash;
+    protected SurfaceControl mLeash;
     protected PipTransitionState mPipTransitionState;
-    private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+    protected PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
             mSurfaceControlTransactionFactory;
     protected PictureInPictureParams mPictureInPictureParams;
     private IntConsumer mOnDisplayIdChangeCallback;
@@ -973,7 +973,7 @@
             return;
         }
 
-        cancelCurrentAnimator();
+        cancelAnimationOnTaskVanished();
         onExitPipFinished(info);
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -981,6 +981,10 @@
         }
     }
 
+    protected void cancelAnimationOnTaskVanished() {
+        cancelCurrentAnimator();
+    }
+
     @Override
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
         Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken");
@@ -1100,7 +1104,7 @@
     }
 
     /** Called when exiting PIP transition is finished to do the state cleanup. */
-    void onExitPipFinished(TaskInfo info) {
+    public void onExitPipFinished(TaskInfo info) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "onExitPipFinished: %s, state=%s leash=%s",
                 info.topActivity, mPipTransitionState, mLeash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index a48e969f..72c0cd7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -44,6 +44,7 @@
 import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
+import java.util.Collections;
 import java.util.Set;
 
 /**
@@ -101,12 +102,29 @@
                 && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
                 && !mTvPipBoundsState.isTvPipManuallyCollapsed();
         if (isPipExpanded) {
-            updateGravityOnExpansionToggled(/* expanding= */ true);
+            updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded);
         }
         mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
         return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
     }
 
+    @Override
+    public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: getEntryDestinationBoundsIgnoringKeepClearAreas()", TAG);
+
+        updateExpandedPipSize();
+        final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported()
+                && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
+                && !mTvPipBoundsState.isTvPipManuallyCollapsed();
+        if (isPipExpanded) {
+            updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded);
+        }
+        mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
+        return adjustBoundsForTemporaryDecor(getTvPipPlacement(Collections.emptySet(),
+                Collections.emptySet()).getUnstashedBounds());
+    }
+
     /** Returns the current bounds adjusted to the new aspect ratio, if valid. */
     @Override
     public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
@@ -133,16 +151,25 @@
      */
     @NonNull
     public Placement getTvPipPlacement() {
+        final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
+        final Set<Rect> unrestrictedKeepClearAreas =
+                mTvPipBoundsState.getUnrestrictedKeepClearAreas();
+
+        return getTvPipPlacement(restrictedKeepClearAreas, unrestrictedKeepClearAreas);
+    }
+
+    /**
+     * Calculates the PiP bounds.
+     */
+    @NonNull
+    private Placement getTvPipPlacement(Set<Rect> restrictedKeepClearAreas,
+            Set<Rect> unrestrictedKeepClearAreas) {
         final Size pipSize = getPipSize();
         final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
         final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
         final Rect insetBounds = new Rect();
         getInsetBounds(insetBounds);
 
-        final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
-        final Set<Rect> unrestrictedKeepClearAreas =
-                mTvPipBoundsState.getUnrestrictedKeepClearAreas();
-
         mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity());
         mKeepClearAlgorithm.setScreenSize(screenSize);
         mKeepClearAlgorithm.setMovementBounds(insetBounds);
@@ -189,8 +216,11 @@
 
         int updatedGravity;
         if (expanding) {
-            // Save collapsed gravity.
-            mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity());
+            if (!mTvPipBoundsState.isTvPipExpanded()) {
+                // Save collapsed gravity.
+                mTvPipBoundsState.setTvPipPreviousCollapsedGravity(
+                        mTvPipBoundsState.getTvPipGravity());
+            }
 
             if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
                 updatedGravity = Gravity.CENTER_HORIZONTAL | currentY;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index 2b3a93e..5ee3734e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -131,6 +131,7 @@
         mTvFixedPipOrientation = ORIENTATION_UNDETERMINED;
         mTvPipGravity = mDefaultGravity;
         mPreviousCollapsedGravity = mDefaultGravity;
+        mIsTvPipExpanded = false;
         mTvPipManuallyCollapsed = false;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 72115fd..cd3d38b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -56,6 +56,7 @@
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellController;
@@ -122,6 +123,7 @@
     private final PipDisplayLayoutState mPipDisplayLayoutState;
     private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
     private final TvPipBoundsController mTvPipBoundsController;
+    private final PipTransitionState mPipTransitionState;
     private final PipAppOpsListener mAppOpsListener;
     private final PipTaskOrganizer mPipTaskOrganizer;
     private final PipMediaController mPipMediaController;
@@ -157,6 +159,7 @@
             PipDisplayLayoutState pipDisplayLayoutState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             TvPipBoundsController tvPipBoundsController,
+            PipTransitionState pipTransitionState,
             PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             PipTransitionController pipTransitionController,
@@ -177,6 +180,7 @@
                 pipDisplayLayoutState,
                 tvPipBoundsAlgorithm,
                 tvPipBoundsController,
+                pipTransitionState,
                 pipAppOpsListener,
                 pipTaskOrganizer,
                 pipTransitionController,
@@ -199,6 +203,7 @@
             PipDisplayLayoutState pipDisplayLayoutState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             TvPipBoundsController tvPipBoundsController,
+            PipTransitionState pipTransitionState,
             PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             PipTransitionController pipTransitionController,
@@ -212,6 +217,7 @@
             Handler mainHandler,
             ShellExecutor mainExecutor) {
         mContext = context;
+        mPipTransitionState = pipTransitionState;
         mMainHandler = mainHandler;
         mMainExecutor = mainExecutor;
         mShellController = shellController;
@@ -365,7 +371,6 @@
                 "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
 
         mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */);
-        onPipDisappeared();
     }
 
     private void togglePipExpansion() {
@@ -420,6 +425,11 @@
 
     @Override
     public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+        if (!mPipTransitionState.hasEnteredPip()) {
+            // Do not schedule a move animation while we're still transitioning into/out of PiP
+            return;
+        }
+
         mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
                 animationDuration, null);
         mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
@@ -447,7 +457,7 @@
             return;
         }
         mPipTaskOrganizer.removePip();
-        onPipDisappeared();
+        mTvPipMenuController.closeMenu();
     }
 
     @Override
@@ -477,7 +487,7 @@
         mPipNotificationController.dismiss();
         mActionBroadcastReceiver.unregister();
 
-        mTvPipMenuController.closeMenu();
+        mTvPipMenuController.detach();
         mTvPipActionsProvider.reset();
         mTvPipBoundsState.resetTvPipState();
         mTvPipBoundsController.reset();
@@ -501,8 +511,6 @@
     public void onPipTransitionCanceled(int direction) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.onPipTransitionFinished(
-                PipAnimationController.isInPipDirection(direction));
         mTvPipActionsProvider.updatePipExpansionState(mTvPipBoundsState.isTvPipExpanded());
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index ee55211..c6803f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -262,8 +262,8 @@
 
     @Override
     public void detach() {
-        closeMenu();
         detachPipMenu();
+        switchToMenuMode(MODE_NO_MENU);
         mLeash = null;
     }
 
@@ -320,10 +320,21 @@
     @Override
     public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction pipTx,
             Rect pipBounds, float alpha) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: movePipMenu: %s, alpha %s", TAG, pipBounds.toShortString(), alpha);
+        movePipMenu(pipTx, pipBounds, alpha);
+    }
 
-        if (pipBounds.isEmpty()) {
+    /**
+     * Move the PiP menu with the given bounds and update its opacity.
+     * The PiP SurfaceControl is given if there is a need to synchronize the movements
+     * on the same frame as PiP.
+     */
+    public void movePipMenu(@Nullable SurfaceControl.Transaction pipTx, @Nullable Rect pipBounds,
+            float alpha) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: movePipMenu: %s, alpha %s", TAG,
+                pipBounds != null ? pipBounds.toShortString() : null, alpha);
+
+        if ((pipBounds == null || pipBounds.isEmpty()) && alpha == ALPHA_NO_CHANGE) {
             if (pipTx == null) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                         "%s: no transaction given", TAG);
@@ -334,28 +345,36 @@
             return;
         }
 
-        final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
-        final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
-        final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds);
         if (pipTx == null) {
             pipTx = new SurfaceControl.Transaction();
         }
-        pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top);
-        pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top);
+
+        final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
+        final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
+
+        if (pipBounds != null) {
+            final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds);
+            pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top);
+            pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top);
+            updateMenuBounds(pipBounds);
+        }
 
         if (alpha != ALPHA_NO_CHANGE) {
             pipTx.setAlpha(frontSurface, alpha);
             pipTx.setAlpha(backSurface, alpha);
         }
 
-        // Synchronize drawing the content in the front and back surfaces together with the pip
-        // transaction and the position change for the front and back surfaces
-        final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip");
-        syncGroup.add(mPipMenuView.getRootSurfaceControl(), null);
-        syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null);
-        updateMenuBounds(pipBounds);
-        syncGroup.addTransaction(pipTx);
-        syncGroup.markSyncReady();
+        if (pipBounds != null) {
+            // Synchronize drawing the content in the front and back surfaces together with the pip
+            // transaction and the position change for the front and back surfaces
+            final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip");
+            syncGroup.add(mPipMenuView.getRootSurfaceControl(), null);
+            syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null);
+            syncGroup.addTransaction(pipTx);
+            syncGroup.markSyncReady();
+        } else {
+            pipTx.apply();
+        }
     }
 
     private boolean isMenuAttached() {
@@ -388,14 +407,19 @@
         final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds);
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
-        mSystemWindows.updateViewLayout(mPipBackgroundView,
-                getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
-                        menuBounds.height()));
-        mSystemWindows.updateViewLayout(mPipMenuView,
-                getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
-                        menuBounds.height()));
-        if (mPipMenuView != null) {
-            mPipMenuView.setPipBounds(pipBounds);
+
+        boolean needsRelayout = mPipBackgroundView.getLayoutParams().width != menuBounds.width()
+                || mPipBackgroundView.getLayoutParams().height != menuBounds.height();
+        if (needsRelayout) {
+            mSystemWindows.updateViewLayout(mPipBackgroundView,
+                    getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
+                            menuBounds.height()));
+            mSystemWindows.updateViewLayout(mPipMenuView,
+                    getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
+                            menuBounds.height()));
+            if (mPipMenuView != null) {
+                mPipMenuView.setPipBounds(pipBounds);
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
index f86f987..202d36f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -168,6 +168,9 @@
      * that the edu text will be marqueed
      */
     private boolean isEduTextMarqueed() {
+        if (mEduTextView.getLayout() == null) {
+            return false;
+        }
         final int availableWidth = (int) mEduTextView.getWidth()
                 - mEduTextView.getCompoundPaddingLeft()
                 - mEduTextView.getCompoundPaddingRight();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index f315afb..21223c9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -35,7 +35,6 @@
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipTaskOrganizer;
-import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
@@ -46,6 +45,7 @@
  * TV specific changes to the PipTaskOrganizer.
  */
 public class TvPipTaskOrganizer extends PipTaskOrganizer {
+    private final TvPipTransition mTvPipTransition;
 
     public TvPipTaskOrganizer(Context context,
             @NonNull SyncTransactionQueue syncTransactionQueue,
@@ -56,7 +56,7 @@
             @NonNull PipMenuController pipMenuController,
             @NonNull PipAnimationController pipAnimationController,
             @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper,
-            @NonNull PipTransitionController pipTransitionController,
+            @NonNull TvPipTransition tvPipTransition,
             @NonNull PipParamsChangedForwarder pipParamsChangedForwarder,
             Optional<SplitScreenController> splitScreenOptional,
             @NonNull DisplayController displayController,
@@ -65,9 +65,10 @@
             ShellExecutor mainExecutor) {
         super(context, syncTransactionQueue, pipTransitionState, pipBoundsState,
                 pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController,
-                surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+                surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
                 splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer,
                 mainExecutor);
+        mTvPipTransition = tvPipTransition;
     }
 
     @Override
@@ -105,4 +106,14 @@
         // when the menu alpha is 0 (e.g. when a fade-in animation starts).
         return true;
     }
+
+    @Override
+    protected void cancelAnimationOnTaskVanished() {
+        mTvPipTransition.cancelAnimations();
+        if (mLeash != null) {
+            mSurfaceControlTransactionFactory.getTransaction()
+                    .setAlpha(mLeash, 0f)
+                    .apply();
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index f24b2b3..571c839 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -16,43 +16,822 @@
 
 package com.android.wm.shell.pip.tv;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PIP;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.transitTypeToString;
+
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE;
+import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
+
+import android.animation.AnimationHandler;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.TaskInfo;
 import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.IBinder;
+import android.os.Trace;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
 
+import androidx.annotation.FloatRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
-import com.android.wm.shell.pip.PipTransition;
+import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
 
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
 
 /**
  * PiP Transition for TV.
  */
-public class TvPipTransition extends PipTransition {
+public class TvPipTransition extends PipTransitionController {
+    private static final String TAG = "TvPipTransition";
+    private static final float ZOOM_ANIMATION_SCALE_FACTOR = 0.97f;
+
+    private final PipTransitionState mPipTransitionState;
+    private final PipAnimationController mPipAnimationController;
+    private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+    private final TvPipMenuController mTvPipMenuController;
+    private final PipDisplayLayoutState mPipDisplayLayoutState;
+    private final PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory
+            mTransactionFactory;
+
+    private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+            ThreadLocal.withInitial(() -> {
+                AnimationHandler handler = new AnimationHandler();
+                handler.setProvider(new SfVsyncFrameCallbackProvider());
+                return handler;
+            });
+
+    private final long mEnterFadeOutDuration;
+    private final long mEnterFadeInDuration;
+    private final long mExitFadeOutDuration;
+    private final long mExitFadeInDuration;
+
+    @Nullable
+    private Animator mCurrentAnimator;
+
+    /**
+     * The Task window that is currently in PIP windowing mode.
+     */
+    @Nullable
+    private WindowContainerToken mCurrentPipTaskToken;
+
+    @Nullable
+    private IBinder mPendingExitTransition;
 
     public TvPipTransition(Context context,
             @NonNull ShellInit shellInit,
             @NonNull ShellTaskOrganizer shellTaskOrganizer,
             @NonNull Transitions transitions,
             TvPipBoundsState tvPipBoundsState,
-            PipDisplayLayoutState pipDisplayLayoutState,
-            PipTransitionState pipTransitionState,
             TvPipMenuController tvPipMenuController,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            PipTransitionState pipTransitionState,
             PipAnimationController pipAnimationController,
             PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
-            Optional<SplitScreenController> splitScreenOptional) {
-        super(context, shellInit, shellTaskOrganizer, transitions, tvPipBoundsState,
-                pipDisplayLayoutState, pipTransitionState, tvPipMenuController,
-                tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
-                splitScreenOptional);
+            PipDisplayLayoutState pipDisplayLayoutState) {
+        super(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, tvPipMenuController,
+                tvPipBoundsAlgorithm);
+        mPipTransitionState = pipTransitionState;
+        mPipAnimationController = pipAnimationController;
+        mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+        mTvPipMenuController = tvPipMenuController;
+        mPipDisplayLayoutState = pipDisplayLayoutState;
+        mTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+
+        mEnterFadeOutDuration = context.getResources().getInteger(
+                R.integer.config_tvPipEnterFadeOutDuration);
+        mEnterFadeInDuration = context.getResources().getInteger(
+                R.integer.config_tvPipEnterFadeInDuration);
+        mExitFadeOutDuration = context.getResources().getInteger(
+                R.integer.config_tvPipExitFadeOutDuration);
+        mExitFadeInDuration = context.getResources().getInteger(
+                R.integer.config_tvPipExitFadeInDuration);
     }
 
+    @Override
+    public void startExitTransition(int type, WindowContainerTransaction out,
+            @Nullable Rect destinationBounds) {
+        cancelAnimations();
+        mPendingExitTransition = mTransitions.startTransition(type, out, this);
+    }
+
+    @Override
+    public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+
+        if (isCloseTransition(info)) {
+            // PiP is closing (without reentering fullscreen activity)
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Starting close animation", TAG);
+            cancelAnimations();
+            startCloseAnimation(info, startTransaction, finishTransaction, finishCallback);
+            mCurrentPipTaskToken = null;
+            return true;
+
+        } else if (transition.equals(mPendingExitTransition)) {
+            // PiP is exiting (reentering fullscreen activity)
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Starting exit animation", TAG);
+
+            final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
+            mPendingExitTransition = null;
+            // PipTaskChange can be null if the PIP task has been detached, for example, when the
+            // task contains multiple activities, the PIP will be moved to a new PIP task when
+            // entering, and be moved back when exiting. In that case, the PIP task will be removed
+            // immediately.
+            final TaskInfo pipTaskInfo = currentPipTaskChange != null
+                    ? currentPipTaskChange.getTaskInfo()
+                    : mPipOrganizer.getTaskInfo();
+            if (pipTaskInfo == null) {
+                throw new RuntimeException("Cannot find the pip task for exit-pip transition.");
+            }
+
+            final int type = info.getType();
+            switch (type) {
+                case TRANSIT_EXIT_PIP -> {
+                    TransitionInfo.Change pipChange = currentPipTaskChange;
+                    SurfaceControl activitySc = null;
+                    if (mCurrentPipTaskToken == null) {
+                        ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                                "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
+                    } else if (pipChange == null) {
+                        // The pipTaskChange is null, this can happen if we are reparenting the
+                        // PIP activity back to its original Task. In that case, we should animate
+                        // the activity leash instead, which should be the change whose last parent
+                        // is the recorded PiP Task.
+                        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                            final TransitionInfo.Change change = info.getChanges().get(i);
+                            if (mCurrentPipTaskToken.equals(change.getLastParent())) {
+                                // Find the activity that is exiting PiP.
+                                pipChange = change;
+                                activitySc = change.getLeash();
+                                break;
+                            }
+                        }
+                    }
+                    if (pipChange == null) {
+                        ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                                "%s: No window of exiting PIP is found. Can't play expand "
+                                        + "animation",
+                                TAG);
+                        removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction,
+                                finishCallback);
+                        return true;
+                    }
+                    final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info);
+                    final SurfaceControl pipLeash;
+                    if (activitySc != null) {
+                        // Use a local leash to animate activity in case the activity has
+                        // letterbox which may be broken by PiP animation, e.g. always end at 0,0
+                        // in parent and unable to include letterbox area in crop bounds.
+                        final SurfaceControl activitySurface = pipChange.getLeash();
+                        pipLeash = new SurfaceControl.Builder()
+                                .setName(activitySc + "_pip-leash")
+                                .setContainerLayer()
+                                .setHidden(false)
+                                .setParent(root.getLeash())
+                                .build();
+                        startTransaction.reparent(activitySurface, pipLeash);
+                        // Put the activity at local position with offset in case it is letterboxed.
+                        final Point activityOffset = pipChange.getEndRelOffset();
+                        startTransaction.setPosition(activitySc, activityOffset.x,
+                                activityOffset.y);
+                    } else {
+                        pipLeash = pipChange.getLeash();
+                        startTransaction.reparent(pipLeash, root.getLeash());
+                    }
+                    startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
+                    final Rect currentBounds = mPipBoundsState.getBounds();
+                    final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
+                    cancelAnimations();
+                    startExitAnimation(pipTaskInfo, pipLeash, currentBounds, destinationBounds,
+                            startTransaction,
+                            finishTransaction, finishCallback);
+                }
+                // pass through here is intended
+                case TRANSIT_TO_BACK, TRANSIT_REMOVE_PIP -> removePipImmediately(info, pipTaskInfo,
+                        startTransaction, finishTransaction,
+                        finishCallback
+                );
+                default -> {
+                    return false;
+                }
+            }
+            mCurrentPipTaskToken = null;
+            return true;
+
+        } else if (isEnteringPip(info)) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Starting enter animation", TAG);
+
+            // Search for an Enter PiP transition
+            TransitionInfo.Change enterPip = null;
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                final TransitionInfo.Change change = info.getChanges().get(i);
+                if (change.getTaskInfo() != null
+                        && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
+                    enterPip = change;
+                }
+            }
+            if (enterPip == null) {
+                throw new IllegalStateException("Trying to start PiP animation without a pip"
+                        + "participant");
+            }
+
+            // Make sure other open changes are visible as entering PIP. Some may be hidden in
+            // Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                final TransitionInfo.Change change = info.getChanges().get(i);
+                if (change == enterPip) continue;
+                if (TransitionUtil.isOpeningType(change.getMode())) {
+                    final SurfaceControl leash = change.getLeash();
+                    startTransaction.show(leash).setAlpha(leash, 1.f);
+                }
+            }
+
+            cancelAnimations();
+            startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task.
+     */
+    private void removePipImmediately(@NonNull TransitionInfo info,
+            @NonNull TaskInfo taskInfo, @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePipImmediately", TAG);
+        cancelAnimations();
+        startTransaction.apply();
+        finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
+                mPipDisplayLayoutState.getDisplayBounds());
+        mTvPipMenuController.detach();
+        mPipOrganizer.onExitPipFinished(taskInfo);
+        finishCallback.onTransitionFinished(/* wct= */ null);
+
+        mPipTransitionState.setTransitionState(UNDEFINED);
+        sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
+    }
+
+    private void startCloseAnimation(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        final TransitionInfo.Change pipTaskChange = findCurrentPipTaskChange(info);
+        final SurfaceControl pipLeash = pipTaskChange.getLeash();
+
+        final List<SurfaceControl> closeLeashes = new ArrayList<>();
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (TransitionUtil.isClosingType(change.getMode()) && change != pipTaskChange) {
+                closeLeashes.add(change.getLeash());
+            }
+        }
+
+        final Rect pipBounds = mPipBoundsState.getBounds();
+        mSurfaceTransactionHelper
+                .resetScale(startTransaction, pipLeash, pipBounds)
+                .crop(startTransaction, pipLeash, pipBounds)
+                .shadow(startTransaction, pipLeash, false);
+
+        final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction();
+        for (SurfaceControl leash : closeLeashes) {
+            startTransaction.setShadowRadius(leash, 0f);
+        }
+
+        ValueAnimator closeFadeOutAnimator = createAnimator();
+        closeFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+        closeFadeOutAnimator.setDuration(mExitFadeOutDuration);
+        closeFadeOutAnimator.addUpdateListener(
+                animationUpdateListener(pipLeash).fadingOut().withMenu());
+        for (SurfaceControl leash : closeLeashes) {
+            closeFadeOutAnimator.addUpdateListener(animationUpdateListener(leash).fadingOut());
+        }
+
+        closeFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(@NonNull Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: close animation: start", TAG);
+                for (SurfaceControl leash : closeLeashes) {
+                    startTransaction.setShadowRadius(leash, 0f);
+                }
+                startTransaction.apply();
+
+                mPipTransitionState.setTransitionState(EXITING_PIP);
+                sendOnPipTransitionStarted(TRANSITION_DIRECTION_REMOVE_STACK);
+            }
+
+            @Override
+            public void onAnimationCancel(@NonNull Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: close animation: cancel", TAG);
+                sendOnPipTransitionCancelled(TRANSITION_DIRECTION_REMOVE_STACK);
+            }
+
+            @Override
+            public void onAnimationEnd(@NonNull Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: close animation: end", TAG);
+                mTvPipMenuController.detach();
+                finishCallback.onTransitionFinished(null /* wct */);
+                transaction.close();
+                mPipTransitionState.setTransitionState(UNDEFINED);
+                sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
+
+                mCurrentAnimator = null;
+            }
+        });
+
+        closeFadeOutAnimator.start();
+        mCurrentAnimator = closeFadeOutAnimator;
+    }
+
+    @Override
+    public void startEnterAnimation(@NonNull TransitionInfo.Change pipChange,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        // Keep track of the PIP task
+        mCurrentPipTaskToken = pipChange.getContainer();
+        final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo();
+        final SurfaceControl leash = pipChange.getLeash();
+
+        mTvPipMenuController.attach(leash);
+        setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
+                taskInfo.topActivityInfo);
+
+        final Rect pipBounds =
+                mPipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas();
+        mPipBoundsState.setBounds(pipBounds);
+        mTvPipMenuController.movePipMenu(null, pipBounds, 0f);
+
+        final WindowContainerTransaction resizePipWct = new WindowContainerTransaction();
+        resizePipWct.setWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
+        resizePipWct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
+        resizePipWct.setBounds(taskInfo.token, pipBounds);
+
+        mSurfaceTransactionHelper
+                .resetScale(finishTransaction, leash, pipBounds)
+                .crop(finishTransaction, leash, pipBounds)
+                .shadow(finishTransaction, leash, false);
+
+        final Rect currentBounds = pipChange.getStartAbsBounds();
+        final Rect fadeOutCurrentBounds = scaledRect(currentBounds, ZOOM_ANIMATION_SCALE_FACTOR);
+
+        final ValueAnimator enterFadeOutAnimator = createAnimator();
+        enterFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+        enterFadeOutAnimator.setDuration(mEnterFadeOutDuration);
+        enterFadeOutAnimator.addUpdateListener(
+                animationUpdateListener(leash)
+                        .fadingOut()
+                        .animateBounds(currentBounds, fadeOutCurrentBounds, currentBounds));
+
+        enterFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+            @SuppressLint("MissingPermission")
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: enter fade out animation: end", TAG);
+                SurfaceControl.Transaction tx = mTransactionFactory.getTransaction();
+                mSurfaceTransactionHelper
+                        .resetScale(tx, leash, pipBounds)
+                        .crop(tx, leash, pipBounds)
+                        .shadow(tx, leash, false);
+                mShellTaskOrganizer.applyTransaction(resizePipWct);
+                tx.apply();
+            }
+        });
+
+        final ValueAnimator enterFadeInAnimator = createAnimator();
+        enterFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
+        enterFadeInAnimator.setDuration(mEnterFadeInDuration);
+        enterFadeInAnimator.addUpdateListener(
+                animationUpdateListener(leash)
+                        .fadingIn()
+                        .withMenu()
+                        .atBounds(pipBounds));
+
+        final AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet
+                .play(enterFadeInAnimator)
+                .after(500)
+                .after(enterFadeOutAnimator);
+
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: enter animation: start", TAG);
+                startTransaction.apply();
+                mPipTransitionState.setTransitionState(ENTERING_PIP);
+                sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: enter animation: cancel", TAG);
+                enterFadeInAnimator.setCurrentFraction(1f);
+                sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: enter animation: end", TAG);
+                WindowContainerTransaction wct = new WindowContainerTransaction();
+                wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+                wct.setBounds(taskInfo.token, pipBounds);
+                finishCallback.onTransitionFinished(wct);
+
+                mPipTransitionState.setTransitionState(ENTERED_PIP);
+                sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+                mCurrentAnimator = null;
+            }
+        });
+
+        animatorSet.start();
+        mCurrentAnimator = animatorSet;
+    }
+
+    private void startExitAnimation(@NonNull TaskInfo taskInfo, SurfaceControl leash,
+            Rect currentBounds, Rect destinationBounds,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        final Rect fadeInStartBounds = scaledRect(destinationBounds, ZOOM_ANIMATION_SCALE_FACTOR);
+
+        final ValueAnimator exitFadeOutAnimator = createAnimator();
+        exitFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+        exitFadeOutAnimator.setDuration(mExitFadeOutDuration);
+        exitFadeOutAnimator.addUpdateListener(
+                animationUpdateListener(leash)
+                        .fadingOut()
+                        .withMenu()
+                        .atBounds(currentBounds));
+        exitFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: exit fade out animation: end", TAG);
+                startTransaction.apply();
+                mPipMenuController.detach();
+            }
+        });
+
+        final ValueAnimator exitFadeInAnimator = createAnimator();
+        exitFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
+        exitFadeInAnimator.setDuration(mExitFadeInDuration);
+        exitFadeInAnimator.addUpdateListener(
+                animationUpdateListener(leash)
+                        .fadingIn()
+                        .animateBounds(fadeInStartBounds, destinationBounds, destinationBounds));
+
+        final AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.playSequentially(
+                exitFadeOutAnimator,
+                exitFadeInAnimator
+        );
+
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: exit animation: start", TAG);
+                mPipTransitionState.setTransitionState(EXITING_PIP);
+                sendOnPipTransitionStarted(TRANSITION_DIRECTION_LEAVE_PIP);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: exit animation: cancel", TAG);
+                sendOnPipTransitionCancelled(TRANSITION_DIRECTION_LEAVE_PIP);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: exit animation: end", TAG);
+                mPipOrganizer.onExitPipFinished(taskInfo);
+
+                final WindowContainerTransaction wct = new WindowContainerTransaction();
+                wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+                wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+                wct.setBounds(taskInfo.token, destinationBounds);
+                finishCallback.onTransitionFinished(wct);
+
+                mPipTransitionState.setTransitionState(UNDEFINED);
+                sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP);
+
+                mCurrentAnimator = null;
+            }
+        });
+
+        animatorSet.start();
+        mCurrentAnimator = animatorSet;
+    }
+
+    @NonNull
+    private ValueAnimator createAnimator() {
+        final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+        animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
+        return animator;
+    }
+
+    @NonNull
+    private TvPipTransitionAnimatorUpdateListener animationUpdateListener(
+            @NonNull SurfaceControl leash) {
+        return new TvPipTransitionAnimatorUpdateListener(leash, mTvPipMenuController,
+                mTransactionFactory.getTransaction(), mSurfaceTransactionHelper);
+    }
+
+    @NonNull
+    private static Rect scaledRect(@NonNull Rect rect, float scale) {
+        final Rect out = new Rect(rect);
+        out.inset((int) (rect.width() * (1 - scale) / 2), (int) (rect.height() * (1 - scale) / 2));
+        return out;
+    }
+
+    private boolean isCloseTransition(TransitionInfo info) {
+        final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
+        return currentPipTaskChange != null && info.getType() == TRANSIT_CLOSE;
+    }
+
+    @Nullable
+    private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) {
+        if (mCurrentPipTaskToken == null) {
+            return null;
+        }
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (mCurrentPipTaskToken.equals(change.getContainer())) {
+                return change;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Whether we should handle the given {@link TransitionInfo} animation as entering PIP.
+     */
+    private boolean isEnteringPip(@NonNull TransitionInfo info) {
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (isEnteringPip(change, info.getType())) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Whether a particular change is a window that is entering pip.
+     */
+    @Override
+    public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
+            @WindowManager.TransitionType int transitType) {
+        if (change.getTaskInfo() != null
+                && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
+                && !Objects.equals(change.getContainer(), mCurrentPipTaskToken)) {
+            if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN
+                    || transitType == TRANSIT_CHANGE) {
+                return true;
+            }
+            // Please file a bug to handle the unexpected transition type.
+            android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type="
+                    + transitTypeToString(transitType), new Throwable());
+        }
+        return false;
+    }
+
+    @Override
+    public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG);
+        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+            mCurrentAnimator.end();
+        }
+    }
+
+    @Nullable
+    @Override
+    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+            @NonNull TransitionRequestInfo request) {
+        if (requestHasPipEnter(request)) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: handle PiP enter request", TAG);
+            WindowContainerTransaction wct = new WindowContainerTransaction();
+            augmentRequest(transition, request, wct);
+            return wct;
+        } else if (request.getType() == TRANSIT_TO_BACK && request.getTriggerTask() != null
+                && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_PINNED) {
+            // if we receive a TRANSIT_TO_BACK type of request while in PiP
+            mPendingExitTransition = transition;
+
+            // update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls
+            mPipTransitionState.setTransitionState(EXITING_PIP);
+
+            // return an empty WindowContainerTransaction so that we don't check other handlers
+            return new WindowContainerTransaction();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
+            @NonNull WindowContainerTransaction outWCT) {
+        if (!requestHasPipEnter(request)) {
+            throw new IllegalStateException("Called PiP augmentRequest when request has no PiP");
+        }
+        outWCT.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED);
+    }
+
+    /**
+     * Cancel any ongoing PiP transitions/animations.
+     */
+    public void cancelAnimations() {
+        if (mPipAnimationController.isAnimating()) {
+            mPipAnimationController.getCurrentAnimator().cancel();
+            mPipAnimationController.resetAnimatorState();
+        }
+        if (mCurrentAnimator != null) {
+            mCurrentAnimator.cancel();
+        }
+    }
+
+    @Override
+    public void end() {
+        if (mCurrentAnimator != null) {
+            mCurrentAnimator.end();
+        }
+    }
+
+    private static class TvPipTransitionAnimatorUpdateListener implements
+            ValueAnimator.AnimatorUpdateListener {
+        private final SurfaceControl mLeash;
+        private final TvPipMenuController mTvPipMenuController;
+        private final SurfaceControl.Transaction mTransaction;
+        private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+        private final RectF mTmpRectF = new RectF();
+        private final Rect mTmpRect = new Rect();
+
+        private float mStartAlpha = ALPHA_NO_CHANGE;
+        private float mEndAlpha = ALPHA_NO_CHANGE;
+
+        @Nullable
+        private Rect mStartBounds;
+        @Nullable
+        private Rect mEndBounds;
+        private Rect mWindowContainerBounds;
+        private boolean mShowMenu;
+
+        TvPipTransitionAnimatorUpdateListener(@NonNull SurfaceControl leash,
+                @NonNull TvPipMenuController tvPipMenuController,
+                @NonNull SurfaceControl.Transaction transaction,
+                @NonNull PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+            mLeash = leash;
+            mTvPipMenuController = tvPipMenuController;
+            mTransaction = transaction;
+            mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+        }
+
+        public TvPipTransitionAnimatorUpdateListener animateAlpha(
+                @FloatRange(from = 0.0, to = 1.0) float startAlpha,
+                @FloatRange(from = 0.0, to = 1.0) float endAlpha) {
+            mStartAlpha = startAlpha;
+            mEndAlpha = endAlpha;
+            return this;
+        }
+
+        public TvPipTransitionAnimatorUpdateListener animateBounds(@NonNull Rect startBounds,
+                @NonNull Rect endBounds, @NonNull Rect windowContainerBounds) {
+            mStartBounds = startBounds;
+            mEndBounds = endBounds;
+            mWindowContainerBounds = windowContainerBounds;
+            return this;
+        }
+
+        public TvPipTransitionAnimatorUpdateListener atBounds(@NonNull Rect bounds) {
+            return animateBounds(bounds, bounds, bounds);
+        }
+
+        public TvPipTransitionAnimatorUpdateListener fadingOut() {
+            return animateAlpha(1f, 0f);
+        }
+
+        public TvPipTransitionAnimatorUpdateListener fadingIn() {
+            return animateAlpha(0f, 1f);
+        }
+
+        public TvPipTransitionAnimatorUpdateListener withMenu() {
+            mShowMenu = true;
+            return this;
+        }
+
+        @Override
+        public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+            final float fraction = animation.getAnimatedFraction();
+            final float alpha = lerp(mStartAlpha, mEndAlpha, fraction);
+            if (mStartBounds != null && mEndBounds != null) {
+                lerp(mStartBounds, mEndBounds, fraction, mTmpRectF);
+                applyAnimatedValue(alpha, mTmpRectF);
+            } else {
+                applyAnimatedValue(alpha, null);
+            }
+        }
+
+        private void applyAnimatedValue(float alpha, @Nullable RectF bounds) {
+            Trace.beginSection("applyAnimatedValue");
+            final SurfaceControl.Transaction tx = mTransaction;
+
+            Trace.beginSection("leash scale and alpha");
+            if (alpha != ALPHA_NO_CHANGE) {
+                mSurfaceTransactionHelper.alpha(tx, mLeash, alpha);
+            }
+            if (bounds != null) {
+                mSurfaceTransactionHelper.scale(tx, mLeash, mWindowContainerBounds, bounds);
+            }
+            mSurfaceTransactionHelper.shadow(tx, mLeash, false);
+            tx.show(mLeash);
+            Trace.endSection();
+
+            if (mShowMenu) {
+                Trace.beginSection("movePipMenu");
+                if (bounds != null) {
+                    mTmpRect.set((int) bounds.left, (int) bounds.top, (int) bounds.right,
+                            (int) bounds.bottom);
+                    mTvPipMenuController.movePipMenu(tx, mTmpRect, alpha);
+                } else {
+                    mTvPipMenuController.movePipMenu(tx, null, alpha);
+                }
+                Trace.endSection();
+            } else {
+                mTvPipMenuController.movePipMenu(tx, null, 0f);
+            }
+
+            tx.apply();
+            Trace.endSection();
+        }
+
+        private float lerp(float start, float end, float fraction) {
+            return start * (1 - fraction) + end * fraction;
+        }
+
+        private void lerp(@NonNull Rect start, @NonNull Rect end, float fraction,
+                @NonNull RectF out) {
+            out.set(
+                    start.left * (1 - fraction) + end.left * fraction,
+                    start.top * (1 - fraction) + end.top * fraction,
+                    start.right * (1 - fraction) + end.right * fraction,
+                    start.bottom * (1 - fraction) + end.bottom * fraction);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 4e2b7f6..800f9e4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -66,6 +66,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -283,7 +284,7 @@
         doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
                 .round(any(), any(), anyBoolean());
         doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
-                .scale(any(), any(), any(), any(), anyFloat());
+                .scale(any(), any(), any(), ArgumentMatchers.<Rect>any(), anyFloat());
         doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
                 .alpha(any(), any(), anyFloat());
         doNothing().when(mMockPipSurfaceTransactionHelper).onDensityOrFontScaleChanged(any());