/* * Copyright (C) 2020 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.pip; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.util.RotationUtils.deltaRotation; import static android.util.RotationUtils.rotateBounds; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; import static com.android.wm.shell.pip.PipAnimationController.FRACTION_START; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isRemovePipDirection; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemProperties; import android.view.Choreographer; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; import android.window.TaskOrganizer; import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.IntConsumer; /** * Manages PiP tasks such as resize and offset. * * This class listens on {@link TaskOrganizer} callbacks for windowing mode change * both to and from PiP and issues corresponding animation if applicable. * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running * and files a final {@link WindowContainerTransaction} at the end of the transition. * * This class is also responsible for general resize/offset PiP operations within SysUI component, * see also {@link PipMotionHelper}. */ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, DisplayController.OnDisplaysChangedListener { private static final String TAG = PipTaskOrganizer.class.getSimpleName(); /** * The fixed start delay in ms when fading out the content overlay from bounds animation. * This is to overcome the flicker caused by configuration change when rotating from landscape * to portrait PiP in button navigation mode. */ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500; private static final int EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS = SystemProperties.getInt( "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400); private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; private final PipBoundsState mPipBoundsState; private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final @NonNull PipMenuController mPipMenuController; private final PipAnimationController mPipAnimationController; protected final PipTransitionController mPipTransitionController; protected final PipParamsChangedForwarder mPipParamsChangedForwarder; private final PipUiEventLogger mPipUiEventLoggerLogger; private final int mEnterAnimationDuration; private final int mExitAnimationDuration; private final int mCrossFadeAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Optional mSplitScreenOptional; @Nullable private final PipPerfHintController mPipPerfHintController; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; // the runnable to execute after WindowContainerTransactions is applied to finish resizing pip private Runnable mPipFinishResizeWCTRunnable; private void maybePerformFinishResizeCallback() { if (mPipFinishResizeWCTRunnable != null) { mPipFinishResizeWCTRunnable.run(); mPipFinishResizeWCTRunnable = null; } } // These callbacks are called on the update thread private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { private boolean mIsCancelled; @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; private void onHighPerfSessionTimeout( PipPerfHintController.PipHighPerfSession session) {} private void cleanUpHighPerfSessionMaybe() { if (mPipHighPerfSession != null) { // Close the high perf session once pointer interactions are over; mPipHighPerfSession.close(); mPipHighPerfSession = null; } } @Override public void onPipAnimationStart(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { if (mPipPerfHintController != null) { // Start a high perf session with a timeout callback. mPipHighPerfSession = mPipPerfHintController.startSession( this::onHighPerfSessionTimeout, "PipTaskOrganizer::mPipAnimationCallback"); } final int direction = animator.getTransitionDirection(); mIsCancelled = false; sendOnPipTransitionStarted(direction); } @Override public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, PipAnimationController.PipTransitionAnimator animator) { // Close the high perf session if needed. cleanUpHighPerfSessionMaybe(); final int direction = animator.getTransitionDirection(); if (mIsCancelled) { sendOnPipTransitionFinished(direction); maybePerformFinishResizeCallback(); return; } final int animationType = animator.getAnimationType(); final Rect destinationBounds = animator.getDestinationBounds(); if (isInPipDirection(direction) && mPipOverlay != null) { fadeOutAndRemoveOverlay(mPipOverlay, null /* callback */, true /* withStartDelay*/); } if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS && direction == TRANSITION_DIRECTION_TO_PIP) { // Notify the display to continue the deferred orientation change. final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.scheduleFinishEnterPip(mToken, destinationBounds); mTaskOrganizer.applyTransaction(wct); // The final task bounds will be applied by onFixedRotationFinished so // that all coordinates are in new rotation. mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); mDeferredAnimEndTransaction = tx; return; } final boolean isExitPipDirection = isOutPipDirection(direction) || isRemovePipDirection(direction); if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP || isExitPipDirection) { // execute the finish resize callback if needed after the transaction is // committed tx.addTransactionCommittedListener(mMainExecutor, PipTaskOrganizer.this::maybePerformFinishResizeCallback); // Finish resize as long as we're not exiting PIP, or, if we are, only if // this is the end of an exit PIP animation. // This is necessary in case there was a resize animation ongoing when // exit PIP started, in which case the first resize will be skipped to // let the exit operation handle the final resize out of PIP mode. // See b/185306679. finishResizeDelayedIfNeeded(() -> { finishResize(tx, destinationBounds, direction, animationType); sendOnPipTransitionFinished(direction); }); } } @Override public void onPipAnimationCancel(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); mIsCancelled = true; if (isInPipDirection(direction) && mPipOverlay != null) { fadeOutAndRemoveOverlay(mPipOverlay, null /* callback */, true /* withStartDelay */); } sendOnPipTransitionCancelled(direction); } }; /** * Finishes resizing the PiP, delaying the operation if it has to be synced with the PiP menu. * * This is done to avoid a race condition between the last transaction applied in * onPipAnimationUpdate and the finishResize in onPipAnimationEnd. The transaction in * onPipAnimationUpdate is applied directly from WmShell, while onPipAnimationEnd creates a * WindowContainerTransaction in finishResize, which is to be applied by WmCore later. Normally, * the WCT should be the last transaction to finish the animation. However, it may happen that * it gets applied *before* the transaction created by the last onPipAnimationUpdate. This * happens only when the PiP surface transaction has to be synced with the PiP menu due to the * necessity for a delay when syncing the PiP surface animation with the PiP menu surface * animation and redrawing the PiP menu contents. As a result, the PiP surface gets scaled after * the new bounds are applied by WmCore, which makes the PiP surface have unexpected bounds. * * To avoid this, we delay the finishResize operation until * the next frame. This aligns the last onAnimationUpdate transaction with the WCT application. */ private void finishResizeDelayedIfNeeded(Runnable finishResizeRunnable) { if (!shouldSyncPipTransactionWithMenu()) { finishResizeRunnable.run(); return; } // Delay the finishResize to the next frame Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> { mMainExecutor.execute(finishResizeRunnable); }, null); } protected boolean shouldSyncPipTransactionWithMenu() { return mPipMenuController.isMenuVisible(); } @VisibleForTesting final PipTransitionController.PipTransitionCallback mPipTransitionCallback = new PipTransitionController.PipTransitionCallback() { @Override public void onPipTransitionStarted(int direction, Rect pipBounds) {} @Override public void onPipTransitionFinished(int direction) { // Apply the deferred RunningTaskInfo if applicable after all proper callbacks // are sent. if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) { onTaskInfoChanged(mDeferredTaskInfo); mDeferredTaskInfo = null; } } @Override public void onPipTransitionCanceled(int direction) {} }; private final PipAnimationController.PipTransactionHandler mPipTransactionHandler = new PipAnimationController.PipTransactionHandler() { @Override public boolean handlePipTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, Rect destinationBounds, float alpha) { if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(leash, tx, destinationBounds, alpha); return true; } return false; } }; private ActivityManager.RunningTaskInfo mTaskInfo; // To handle the edge case that onTaskInfoChanged callback is received during the entering // PiP transition, where we do not want to intercept the transition but still want to apply the // changed RunningTaskInfo when it finishes. private ActivityManager.RunningTaskInfo mDeferredTaskInfo; private WindowContainerToken mToken; protected SurfaceControl mLeash; protected PipTransitionState mPipTransitionState; protected PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; protected PictureInPictureParams mPictureInPictureParams; private IntConsumer mOnDisplayIdChangeCallback; /** * The end transaction of PiP animation for switching between PiP and fullscreen with * orientation change. The transaction should be applied after the display is rotated. */ private SurfaceControl.Transaction mDeferredAnimEndTransaction; /** Whether the existing PiP is hidden by alpha. */ private boolean mHasFadeOut; /** * If set to {@code true}, the entering animation will be skipped and we will wait for * {@link #onFixedRotationFinished(int)} callback to actually enter PiP. */ private boolean mWaitForFixedRotation; /** * The rotation that the display will apply after expanding PiP to fullscreen. This is only * meaningful if {@link #mWaitForFixedRotation} is true. */ private @Surface.Rotation int mNextRotation; private @Surface.Rotation int mCurrentRotation; /** * An optional overlay used to mask content changing between an app in/out of PiP. */ @Nullable SurfaceControl mPipOverlay; /** * The app bounds used for the buffer size of the * {@link com.android.wm.shell.pip.PipContentOverlay.PipAppIconOverlay}. * * Note that this is empty if the overlay is removed or if it's some other type of overlay * defined in {@link PipContentOverlay}. */ @NonNull final Rect mAppBounds = new Rect(); /** The source rect hint from stopSwipePipToHome(). */ @Nullable private Rect mSwipeSourceRectHint; public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional splitScreenOptional, Optional pipPerfHintControllerOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { mContext = context; mSyncTransactionQueue = syncTransactionQueue; mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; mPipDisplayLayoutState = pipDisplayLayoutState; mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; mPipTransitionController = pipTransitionController; mPipParamsChangedForwarder = pipParamsChangedForwarder; mEnterAnimationDuration = context.getResources() .getInteger(R.integer.config_pipEnterAnimationDuration); mExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipExitAnimationDuration); mCrossFadeAnimationDuration = context.getResources() .getInteger(R.integer.config_pipCrossfadeAnimationDuration); mSurfaceTransactionHelper = surfaceTransactionHelper; mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); mSplitScreenOptional = splitScreenOptional; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; // TODO: Can be removed once wm components are created on the shell-main thread if (!PipUtils.isPip2ExperimentEnabled()) { mMainExecutor.execute(() -> { mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); }); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); } } public PipTransitionController getTransitionController() { return mPipTransitionController; } PipAnimationController.PipTransactionHandler getPipTransactionHandler() { return mPipTransactionHandler; } public Rect getCurrentOrAnimatingBounds() { PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); if (animator != null && animator.isRunning()) { return new Rect(animator.getDestinationBounds()); } return mPipBoundsState.getBounds(); } public boolean isInPip() { return mPipTransitionState.isInPip(); } private boolean isLaunchIntoPipTask() { return mPictureInPictureParams != null && mPictureInPictureParams.isLaunchIntoPip(); } /** * Returns whether the entry animation is waiting to be started. */ public boolean isEntryScheduled() { return mPipTransitionState.getTransitionState() == PipTransitionState.ENTRY_SCHEDULED; } /** * Registers a callback when a display change has been detected when we enter PiP. */ public void registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback) { mOnDisplayIdChangeCallback = onDisplayIdChangeCallback; } /** * Override if the PiP should always use a fade-in animation during PiP entry. * * @return true if the mOneShotAnimationType should always be * {@link PipAnimationController#ANIM_TYPE_ALPHA}. */ protected boolean shouldAlwaysFadeIn() { return false; } /** * Whether the menu should get attached as early as possible when entering PiP. * * @return whether the menu should be attached before * {@link PipBoundsAlgorithm#getEntryDestinationBounds()} is called. */ protected boolean shouldAttachMenuEarly() { return false; } /** * Callback when Launcher starts swipe-pip-to-home operation. * @return {@link Rect} for destination bounds. */ public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "startSwipePipToHome: %s, state=%s", componentName, mPipTransitionState); mPipTransitionState.setInSwipePipToHomeTransition(true); sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); return mPipBoundsAlgorithm.getEntryDestinationBounds(); } /** * Callback when launcher finishes preparation of swipe-pip-to-home operation. * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. */ public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState); // do nothing if there is no startSwipePipToHome being called before if (!mPipTransitionState.getInSwipePipToHomeTransition()) { return; } mPipBoundsState.setBounds(destinationBounds); setContentOverlay(overlay, appBounds); mSwipeSourceRectHint = sourceRectHint; if (ENABLE_SHELL_TRANSITIONS && overlay != null) { // With Shell transition, the overlay was attached to the remote transition leash, which // will be removed when the current transition is finished, so we need to reparent it // to the actual Task surface now. // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP // transition. final SurfaceControl.Transaction t = mSurfaceControlTransactionFactory.getTransaction(); mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t); t.setLayer(overlay, Integer.MAX_VALUE); t.apply(); // This serves as a last resort in case the Shell Transition is not handled properly. // We want to make sure the overlay passed from Launcher gets removed eventually. mayRemoveContentOverlay(overlay); } } /** * Returns non-null Rect if the pip is entering from swipe-to-home with a specified source hint. * This also consumes the rect hint. */ @Nullable Rect takeSwipeSourceRectHint() { final Rect sourceRectHint = mSwipeSourceRectHint; if (sourceRectHint == null || sourceRectHint.isEmpty()) { return null; } mSwipeSourceRectHint = null; return mPipTransitionState.getInSwipePipToHomeTransition() ? sourceRectHint : null; } private void mayRemoveContentOverlay(SurfaceControl overlay) { final WeakReference overlayRef = new WeakReference<>(overlay); final long timeoutDuration = (mEnterAnimationDuration + CONTENT_OVERLAY_FADE_OUT_DELAY_MS + EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS) * 2L; mMainExecutor.executeDelayed(() -> { final SurfaceControl overlayLeash = overlayRef.get(); if (overlayLeash != null && overlayLeash.isValid() && overlayLeash == mPipOverlay) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "Cleanup the overlay(%s) as a last resort.", overlayLeash); removeContentOverlay(overlayLeash, null /* callback */); } }, timeoutDuration); } /** * Callback when launcher aborts swipe-pip-to-home operation. */ public void abortSwipePipToHome(int taskId, ComponentName componentName) { if (!mPipTransitionState.getInSwipePipToHomeTransition()) { return; } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "Abort swipe-pip-to-home for %s", componentName); sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP); // Cleanup internal states mPipTransitionState.setInSwipePipToHomeTransition(false); mPictureInPictureParams = null; mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); } public ActivityManager.RunningTaskInfo getTaskInfo() { return mTaskInfo; } public SurfaceControl getSurfaceControl() { return mLeash; } private void setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, ActivityInfo activityInfo) { mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, params, mPipBoundsAlgorithm); } /** * Expands PiP to the previous bounds, this is done in two phases using * {@link WindowContainerTransaction} * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the * transaction. without changing the windowing mode of the Task itself. This makes sure the * activity render it's final configuration while the Task is still in PiP. * - setWindowingMode to undefined at the end of transition * @param animationDurationMs duration in millisecond for the exiting PiP transition * @param requestEnterSplit whether the enterSplit button is pressed on PiP or not. * Indicate the user wishes to directly put PiP into split screen * mode. */ public void exitPip(int animationDurationMs, boolean requestEnterSplit) { if (!mPipTransitionState.isInPip() || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP || mToken == null) { ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Not allowed to exitPip in current state" + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(), mToken); return; } if (mPipTransitionState.isEnteringPip() && !mPipTransitionState.getInSwipePipToHomeTransition()) { // If we are still entering PiP with Shell playing enter animation, jump-cut to // the end of the enter animation and reschedule exitPip to run after enter-PiP // has finished its transition and allowed the client to draw in PiP mode. mPipTransitionController.end(() -> { // TODO(341627042): force set to entered state to avoid potential stack overflow. mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); exitPip(animationDurationMs, requestEnterSplit); }); return; } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); final WindowContainerTransaction wct = new WindowContainerTransaction(); if (isLaunchIntoPipTask()) { exitLaunchIntoPipTask(wct); return; } // bail early if leash is null if (mLeash == null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: leash is null"); return; } final Rect destinationBounds = new Rect(getExitDestinationBounds()); final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit) ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_LEAVE_PIP; // For exiting to fullscreen, the windowing mode of task will be changed to fullscreen // until the animation is finished. Otherwise if the activity is resumed and focused at the // begin of aniamtion, the app may do something too early to distub the animation. if (Transitions.SHELL_TRANSITIONS_ROTATION) { // When exit to fullscreen with Shell transition enabled, we update the Task windowing // mode directly so that it can also trigger display rotation and visibility update in // the same transition if there will be any. wct.setWindowingMode(mToken, getOutPipWindowingMode()); // We can inherit the parent bounds as it is going to be fullscreen. The // destinationBounds calculated above will be incorrect if this is with rotation. wct.setBounds(mToken, null); } else { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: %s, dest=%s", mTaskInfo.topActivity, destinationBounds); final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, mPipBoundsState.getBounds()); tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); // We set to fullscreen here for now, but later it will be set to UNDEFINED for // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. wct.setActivityWindowingMode(mToken, WINDOWING_MODE_FULLSCREEN); wct.setBounds(mToken, destinationBounds); wct.setBoundsChangeTransaction(mToken, tx); } // Cancel the existing animator if there is any. // TODO(b/232439933): this is disabled temporarily to unblock b/234502692. // cancelCurrentAnimator(); // Set the exiting state first so if there is fixed rotation later, the running animation // won't be interrupted by alpha animation for existing PiP. mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); if (Transitions.ENABLE_SHELL_TRANSITIONS) { if (requestEnterSplit && mSplitScreenOptional.isPresent()) { wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); mSplitScreenOptional.get().onPipExpandToSplit(wct, mTaskInfo); mPipTransitionController.startExitTransition( TRANSIT_EXIT_PIP_TO_SPLIT, wct, destinationBounds); return; } if (mSplitScreenOptional.isPresent()) { // If pip activity will reparent to origin task case and if the origin task still // under split root, apply exit split transaction to make it expand to fullscreen. SplitScreenController split = mSplitScreenOptional.get(); if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) { split.prepareExitSplitScreen(wct, split.getStageOfTask( mTaskInfo.lastParentTaskIdBeforePip), SplitScreenController.EXIT_REASON_APP_FINISHED); } } mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds); return; } if (mSplitScreenOptional.isPresent()) { // If pip activity will reparent to origin task case and if the origin task still under // split root, just exit split screen here to ensure it could expand to fullscreen. SplitScreenController split = mSplitScreenOptional.get(); if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) { split.exitSplitScreen(INVALID_TASK_ID, SplitScreenController.EXIT_REASON_APP_FINISHED); } } mSyncTransactionQueue.queue(wct); mSyncTransactionQueue.runInSync(t -> { // Make sure to grab the latest source hint rect as it could have been // updated right after applying the windowing mode change. final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( mPictureInPictureParams, destinationBounds); final PipAnimationController.PipTransitionAnimator animator = animateResizePip(mPipBoundsState.getBounds(), destinationBounds, sourceHintRect, direction, animationDurationMs, 0 /* startingAngle */); if (animator != null) { // Even though the animation was started above, re-apply the transaction for the // first frame using the SurfaceControl.Transaction supplied by the // SyncTransactionQueue. This is necessary because the initial surface transform // may not be applied until the next frame if a different Transaction than the one // supplied is used, resulting in 1 frame not being cropped to the source rect // hint during expansion that causes a visible jank/flash. See b/184166183. animator.applySurfaceControlTransaction(mLeash, t, FRACTION_START); } }); } /** Returns the bounds to restore to when exiting PIP mode. */ public Rect getExitDestinationBounds() { return mPipBoundsState.getDisplayBounds(); } private void exitLaunchIntoPipTask(WindowContainerTransaction wct) { wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */); mTaskOrganizer.applyTransaction(wct); // Remove the PiP with fade-out animation right after the host Task is brought to front. removePip(); } void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { // Reset the final windowing mode. wct.setWindowingMode(mToken, getOutPipWindowingMode()); // Simply reset the activity mode set prior to the animation running. wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); } /** * Removes PiP immediately. */ public void removePip() { if (!mPipTransitionState.isInPip() || mToken == null || mLeash == null) { ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Not allowed to removePip in current state" + " mState=%d mToken=%s mLeash=%s", TAG, mPipTransitionState.getTransitionState(), mToken, mLeash); return; } // removePipImmediately is expected when the following animation finishes. ValueAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), 1f /* alphaStart */, 0f /* alphaEnd */) .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) .setPipTransactionHandler(mPipTransactionHandler) .setPipAnimationCallback(mPipAnimationCallback); animator.setDuration(mExitAnimationDuration); animator.setInterpolator(Interpolators.ALPHA_OUT); animator.start(); mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "removePip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); } private void removePipImmediately() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "removePipImmediately: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); if (Transitions.ENABLE_SHELL_TRANSITIONS) { final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mToken, null); wct.setWindowingMode(mToken, getOutPipWindowingMode()); wct.reorder(mToken, false); mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct, null /* destinationBounds */); return; } try { // Reset the task bounds first to ensure the activity configuration is reset as well final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mToken, null); mTaskOrganizer.applyTransaction(wct); ActivityTaskManager.getService().removeRootTasksInWindowingModes( new int[]{ WINDOWING_MODE_PINNED }); } catch (RemoteException e) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Failed to remove PiP, %s", TAG, e); } } @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { Objects.requireNonNull(info, "Requires RunningTaskInfo"); mTaskInfo = info; mToken = mTaskInfo.token; mPipTransitionState.setTransitionState(PipTransitionState.TASK_APPEARED); mLeash = leash; mPictureInPictureParams = mTaskInfo.pictureInPictureParams; setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, mTaskInfo.topActivityInfo); if (mPictureInPictureParams != null) { mPipParamsChangedForwarder.notifyActionsChanged(mPictureInPictureParams.getActions(), mPictureInPictureParams.getCloseAction()); mPipParamsChangedForwarder.notifyTitleChanged( mPictureInPictureParams.getTitle()); mPipParamsChangedForwarder.notifySubtitleChanged( mPictureInPictureParams.getSubtitle()); } mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); // If the displayId of the task is different than what PipBoundsHandler has, then update // it. This is possible if we entered PiP on an external display. if (info.displayId != mPipDisplayLayoutState.getDisplayId() && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(info.displayId); } // UiEvent logging. final PipUiEventLogger.PipUiEventEnum uiEventEnum; if (isLaunchIntoPipTask()) { uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER_CONTENT_PIP; } else if (mPipTransitionState.getInSwipePipToHomeTransition()) { uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_AUTO_ENTER; } else { uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER; } mPipUiEventLoggerLogger.log(uiEventEnum); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onTaskAppeared: %s, state=%s, taskId=%s", mTaskInfo.topActivity, mPipTransitionState, mTaskInfo.taskId); if (mPipTransitionState.getInSwipePipToHomeTransition()) { if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); } else { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Defer onTaskAppeared-SwipePipToHome until end of fixed rotation.", TAG); } return; } final int animationType = shouldAlwaysFadeIn() ? ANIM_TYPE_ALPHA : mPipAnimationController.takeOneShotEnterAnimationType(); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mPipTransitionController.setEnterAnimationType(animationType); // For Shell transition, we will animate the window in PipTransition#startAnimation // instead of #onTaskAppeared. return; } if (mWaitForFixedRotation) { onTaskAppearedWithFixedRotation(animationType); return; } if (shouldAttachMenuEarly()) { mPipMenuController.attach(mLeash); } final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); Objects.requireNonNull(destinationBounds, "Missing destination bounds"); final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); if (animationType == ANIM_TYPE_BOUNDS) { if (!shouldAttachMenuEarly()) { mPipMenuController.attach(mLeash); } final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( info.pictureInPictureParams, currentBounds); scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, null /* updateBoundsCallback */); mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } else if (animationType == ANIM_TYPE_ALPHA) { enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); } else { throw new RuntimeException("Unrecognized animation type: " + animationType); } } private void onTaskAppearedWithFixedRotation(int animationType) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onTaskAppearedWithFixedRotation: %s, state=%s animationType=%d", mTaskInfo.topActivity, mPipTransitionState, animationType); if (animationType == ANIM_TYPE_ALPHA) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Defer entering PiP alpha animation, fixed rotation is ongoing", TAG); // If deferred, hside the surface till fixed rotation is completed. final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.setAlpha(mLeash, 0f); tx.show(mLeash); tx.apply(); return; } final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( mPictureInPictureParams, currentBounds); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); animateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */); mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } /** * Called when the display rotation handling is skipped (e.g. when rotation happens while in * the middle of an entry transition). */ public void onDisplayRotationSkipped() { if (isEntryScheduled()) { // The PIP animation is scheduled to start with the previous orientation's bounds, // re-calculate the entry bounds and restart the alpha animation. final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); } } @VisibleForTesting void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { // If we are fading the PIP in, then we should move the pip to the final location as // soon as possible, but set the alpha immediately since the transaction can take a // while to process final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.setAlpha(mLeash, 0f); tx.apply(); // When entering PiP this transaction will be applied within WindowContainerTransaction and // ensure that the PiP has rounded corners. final SurfaceControl.Transaction boundsChangeTx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(boundsChangeTx, mLeash, destinationBounds) .round(boundsChangeTx, mLeash, true /* applyCornerRadius */); mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); applyEnterPipSyncTransaction(destinationBounds, () -> { mPipAnimationController .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setPipTransactionHandler(mPipTransactionHandler) .setDuration(durationMs) .start(); // mState is set right after the animation is kicked off to block any resize // requests such as offsetPip that may have been called prior to the transition. mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); }, boundsChangeTx); } private void onEndOfSwipePipToHomeTransition() { if (Transitions.ENABLE_SHELL_TRANSITIONS) { return; } final Rect destinationBounds = mPipBoundsState.getBounds(); final SurfaceControl swipeToHomeOverlay = mPipOverlay; final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .resetScale(tx, mLeash, destinationBounds) .crop(tx, mLeash, destinationBounds) .round(tx, mLeash, isInPip()); // The animation is finished in the Launcher and here we directly apply the final touch. applyEnterPipSyncTransaction(destinationBounds, () -> { // Ensure menu's settled in its final bounds first. finishResizeForMenu(destinationBounds); sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); // Remove the swipe to home overlay if (swipeToHomeOverlay != null) { fadeOutAndRemoveOverlay(swipeToHomeOverlay, null /* callback */, false /* withStartDelay */); } }, tx); mPipTransitionState.setInSwipePipToHomeTransition(false); mPipOverlay = null; } private void applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable, @Nullable SurfaceControl.Transaction boundsChangeTransaction) { // PiP menu is attached late in the process here to avoid any artifacts on the leash // caused by addShellRoot when in gesture navigation mode. if (!shouldAttachMenuEarly()) { mPipMenuController.attach(mLeash); } final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); wct.setBounds(mToken, destinationBounds); if (boundsChangeTransaction != null) { wct.setBoundsChangeTransaction(mToken, boundsChangeTransaction); } mSyncTransactionQueue.queue(wct); if (runnable != null) { mSyncTransactionQueue.runInSync(t -> runnable.run()); } } private void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } mPipTransitionController.sendOnPipTransitionStarted(direction); } @VisibleForTesting void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); } mPipTransitionController.sendOnPipTransitionFinished(direction); } private void sendOnPipTransitionCancelled( @PipAnimationController.TransitionDirection int direction) { mPipTransitionController.sendOnPipTransitionCancelled(direction); } /** * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int, boolean)}. * Meanwhile this callback is invoked whenever the task is removed. For instance: * - as a result of removeRootTasksInWindowingModes from WM * - activity itself is died * Nevertheless, we simply update the internal state here as all the heavy lifting should * have been done in WM. */ @Override public void onTaskVanished(ActivityManager.RunningTaskInfo info) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onTaskVanished: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { return; } if (Transitions.ENABLE_SHELL_TRANSITIONS && mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP) { // With Shell transition, we do the cleanup in PipTransition after exiting PIP. return; } final WindowContainerToken token = info.token; Objects.requireNonNull(token, "Requires valid WindowContainerToken"); if (token.asBinder() != mToken.asBinder()) { ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Unrecognized token: %s", TAG, token); return; } cancelAnimationOnTaskVanished(); onExitPipFinished(info); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mPipTransitionController.forceFinishTransition(); } } protected void cancelAnimationOnTaskVanished() { cancelCurrentAnimator(); } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Defer onTaskInfoChange in current state: %d", TAG, mPipTransitionState.getTransitionState()); // Defer applying PiP parameters if the task is entering PiP to avoid disturbing // the animation. mDeferredTaskInfo = info; return; } mPipBoundsState.setLastPipComponentName(info.topActivity); mPipBoundsState.setOverrideMinSize( mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo)); final PictureInPictureParams newParams = info.pictureInPictureParams; ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onTaskInfoChanged: %s, state=%s oldParams=%s newParams=%s", mTaskInfo.topActivity, mPipTransitionState, mPictureInPictureParams, newParams); // mPictureInPictureParams is only null if there is no PiP if (newParams == null || mPictureInPictureParams == null) { return; } applyNewPictureInPictureParams(newParams); mPictureInPictureParams = newParams; } @Override public boolean supportCompatUI() { // PIP doesn't support compat. return false; } @Override public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { b.setParent(findTaskSurface(taskId)); } @Override public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, SurfaceControl.Transaction t) { t.reparent(sc, findTaskSurface(taskId)); } private SurfaceControl findTaskSurface(int taskId) { if (mTaskInfo == null || mLeash == null || mTaskInfo.taskId != taskId) { throw new IllegalArgumentException("There is no surface for taskId=" + taskId); } return mLeash; } @Override public void onFixedRotationStarted(int displayId, int newRotation) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onFixedRotationStarted: %s, state=%s", mTaskInfo, mPipTransitionState); mNextRotation = newRotation; mWaitForFixedRotation = true; if (Transitions.ENABLE_SHELL_TRANSITIONS) { // The fixed rotation will also be included in the transition info. However, if it is // not a PIP transition (such as open another app to different orientation), // PIP transition handler may not be aware of the fixed rotation start. // Notify the PIP transition handler so that it can fade out the PIP window early for // fixed transition of other windows. mPipTransitionController.onFixedRotationStarted(); return; } if (mPipTransitionState.isInPip()) { // Fade out the existing PiP to avoid jump cut during seamless rotation. fadeExistingPip(false /* show */); } } @Override public void onFixedRotationFinished(int displayId) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onFixedRotationFinished: %s, state=%s", mTaskInfo, mPipTransitionState); if (!mWaitForFixedRotation) { return; } if (Transitions.ENABLE_SHELL_TRANSITIONS) { mPipTransitionController.onFixedRotationFinished(); clearWaitForFixedRotation(); return; } if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) { if (mPipTransitionState.getInSwipePipToHomeTransition()) { onEndOfSwipePipToHomeTransition(); } else { // Schedule a regular animation to ensure all the callbacks are still being sent. enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(), mEnterAnimationDuration); } } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERED_PIP && mHasFadeOut) { fadeExistingPip(true /* show */); } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERING_PIP && mDeferredAnimEndTransaction != null) { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); final Rect destinationBounds = animator.getDestinationBounds(); mPipBoundsState.setBounds(destinationBounds); applyEnterPipSyncTransaction(destinationBounds, () -> { finishResizeForMenu(destinationBounds); sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); }, mDeferredAnimEndTransaction); } clearWaitForFixedRotation(); } /** Called when exiting PIP transition is finished to do the state cleanup. */ public void onExitPipFinished(TaskInfo info) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onExitPipFinished: %s, state=%s leash=%s", info.topActivity, mPipTransitionState, mLeash); if (mLeash == null) { // TODO(239461594): Remove once the double call to onExitPipFinished() is fixed ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "Warning, onExitPipFinished() called multiple times in the same session"); return; } clearWaitForFixedRotation(); if (mPipOverlay != null) { removeContentOverlay(mPipOverlay, null /* callback */); mPipOverlay = null; } resetShadowRadius(); mPipTransitionState.setInSwipePipToHomeTransition(false); mPictureInPictureParams = null; mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); // Re-set the PIP bounds to none. mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); mPipMenuController.detach(); mLeash = null; if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); } } private void fadeExistingPip(boolean show) { if (mLeash == null || !mLeash.isValid()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Invalid leash on fadeExistingPip: %s", TAG, mLeash); return; } final float alphaStart = show ? 0 : 1; final float alphaEnd = show ? 1 : 0; mPipAnimationController .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), alphaStart, alphaEnd) .setTransitionDirection(TRANSITION_DIRECTION_SAME) .setPipTransactionHandler(mPipTransactionHandler) .setDuration(show ? mEnterAnimationDuration : mExitAnimationDuration) .start(); mHasFadeOut = !show; } private void clearWaitForFixedRotation() { mWaitForFixedRotation = false; mDeferredAnimEndTransaction = null; } /** Explicitly set the visibility of PiP window. */ public void setPipVisibility(boolean visible) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "setPipVisibility: %s, state=%s visible=%s", (mTaskInfo != null ? mTaskInfo.topActivity : null), mPipTransitionState, visible); if (!isInPip()) { return; } if (mLeash == null || !mLeash.isValid()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Invalid leash on setPipVisibility: %s", TAG, mLeash); return; } final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper.alpha(tx, mLeash, visible ? 1f : 0f); tx.apply(); } @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { mCurrentRotation = newConfig.windowConfiguration.getRotation(); } /** * Called when display size or font size of settings changed */ public void onDensityOrFontScaleChanged(Context context) { mSurfaceTransactionHelper.onDensityOrFontScaleChanged(context); } /** * TODO(b/152809058): consolidate the display info handling logic in SysUI * * @param destinationBoundsOut the current destination bounds will be populated to this param */ @SuppressWarnings("unchecked") public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, boolean fromImeAdjustment, boolean fromShelfAdjustment, WindowContainerTransaction wct) { // note that this can be called when swipe-to-home or fixed-rotation is happening. // Skip this entirely if that's the case. final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP); if ((mPipTransitionState.getInSwipePipToHomeTransition() || waitForFixedRotationOnEnteringPip) && fromRotation) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Skip onMovementBoundsChanged on rotation change" + " InSwipePipToHomeTransition=%b" + " mWaitForFixedRotation=%b" + " getTransitionState=%d", TAG, mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation, mPipTransitionState.getTransitionState()); return; } final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation; if (rotatingPip && Transitions.ENABLE_SHELL_TRANSITIONS) { // The animation and surface update will be handled by the shell transition handler. mPipBoundsState.setBounds(destinationBoundsOut); } else if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { // The position will be used by fade-in animation when the fixed rotation is done. mPipBoundsState.setBounds(destinationBoundsOut); } else if (rotatingPip) { // Update bounds state to final destination first. It's important to do this // before finishing & cancelling the transition animation so that the MotionHelper // bounds are synchronized to the destination bounds when the animation ends. mPipBoundsState.setBounds(destinationBoundsOut); // If we are rotating while there is a current animation, immediately cancel the // animation (remove the listeners so we don't trigger the normal finish resize // call that should only happen on the update thread) int direction = TRANSITION_DIRECTION_NONE; if (animator != null) { direction = animator.getTransitionDirection(); PipAnimationController.quietCancel(animator); // Do notify the listeners that this was canceled sendOnPipTransitionCancelled(direction); sendOnPipTransitionFinished(direction); } // Create a reset surface transaction for the new bounds and update the window // container transaction final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction( destinationBoundsOut); prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct); } else { // There could be an animation on-going. If there is one on-going, last-reported // bounds isn't yet updated. We'll use the animator's bounds instead. if (animator != null && animator.isRunning()) { if (!animator.getDestinationBounds().isEmpty()) { destinationBoundsOut.set(animator.getDestinationBounds()); } } else { if (!mPipBoundsState.getBounds().isEmpty()) { destinationBoundsOut.set(mPipBoundsState.getBounds()); } } } return; } final Rect currentDestinationBounds = animator.getDestinationBounds(); destinationBoundsOut.set(currentDestinationBounds); if (!fromImeAdjustment && !fromShelfAdjustment && mPipBoundsState.getDisplayBounds().contains(currentDestinationBounds)) { // no need to update the destination bounds, bail early return; } final Rect newDestinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); if (newDestinationBounds.equals(currentDestinationBounds)) return; updateAnimatorBounds(newDestinationBounds); destinationBoundsOut.set(newDestinationBounds); } /** * Directly update the animator bounds. */ public void updateAnimatorBounds(Rect bounds) { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); if (animator != null && animator.isRunning()) { if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { if (mWaitForFixedRotation) { // The new destination bounds are in next rotation (DisplayLayout has been // rotated in computeRotatedBounds). The animation runs in previous rotation so // the end bounds need to be transformed. final Rect displayBounds = mPipBoundsState.getDisplayBounds(); final Rect rotatedEndBounds = new Rect(bounds); rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); animator.updateEndValue(rotatedEndBounds); } else { animator.updateEndValue(bounds); } } animator.setDestinationBounds(bounds); } } /** * Handles all changes to the PictureInPictureParams. */ protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { if (PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), mPictureInPictureParams.getAspectRatioFloat())) { if (mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio( params.getAspectRatioFloat())) { mPipParamsChangedForwarder.notifyAspectRatioChanged(params.getAspectRatioFloat()); } else { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: New aspect ratio is not valid." + " hasAspectRatio=%b" + " aspectRatio=%f", TAG, params.hasSetAspectRatio(), params.getAspectRatioFloat()); } } if (PipUtils.remoteActionsChanged(params.getActions(), mPictureInPictureParams.getActions()) || !PipUtils.remoteActionsMatch(params.getCloseAction(), mPictureInPictureParams.getCloseAction())) { mPipParamsChangedForwarder.notifyActionsChanged(params.getActions(), params.getCloseAction()); } } /** * Animates resizing of the pinned stack given the duration. */ public void scheduleAnimateResizePip(Rect toBounds, int duration, Consumer updateBoundsCallback) { scheduleAnimateResizePip(toBounds, duration, TRANSITION_DIRECTION_NONE, updateBoundsCallback); } /** * Animates resizing of the pinned stack given the duration. */ public void scheduleAnimateResizePip(Rect toBounds, int duration, @PipAnimationController.TransitionDirection int direction, Consumer updateBoundsCallback) { if (mWaitForFixedRotation) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: skip scheduleAnimateResizePip, entering pip deferred", TAG); return; } scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, 0 /* startingAngle */, null /* sourceHintRect */, direction, duration, updateBoundsCallback); } /** * Animates resizing of the pinned stack given the duration and start bounds. * This is used when the starting bounds is not the current PiP bounds. * * @param pipFinishResizeWCTRunnable callback to run after window updates are complete */ public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, float startingAngle, Consumer updateBoundsCallback, Runnable pipFinishResizeWCTRunnable) { mPipFinishResizeWCTRunnable = pipFinishResizeWCTRunnable; if (mPipFinishResizeWCTRunnable != null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "mPipFinishResizeWCTRunnable is set to be called once window updates"); } scheduleAnimateResizePip(fromBounds, toBounds, duration, startingAngle, updateBoundsCallback); } private void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, float startingAngle, Consumer updateBoundsCallback) { if (mWaitForFixedRotation) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: skip scheduleAnimateResizePip, entering pip deferred", TAG); return; } scheduleAnimateResizePip(fromBounds, toBounds, startingAngle, null /* sourceHintRect */, TRANSITION_DIRECTION_SNAP_AFTER_RESIZE, duration, updateBoundsCallback); } /** * Animates resizing of the pinned stack given the duration and start bounds. * This always animates the angle to zero from the starting angle. */ private @Nullable PipAnimationController.PipTransitionAnimator scheduleAnimateResizePip( Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer updateBoundsCallback) { if (!mPipTransitionState.isInPip()) { // TODO: tend to use shouldBlockResizeRequest here as well but need to consider // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window // container transaction callback and we want to set the mState immediately. return null; } final PipAnimationController.PipTransitionAnimator animator = animateResizePip( currentBounds, destinationBounds, sourceHintRect, direction, durationMs, startingAngle); if (updateBoundsCallback != null) { updateBoundsCallback.accept(destinationBounds); } return animator; } /** * Directly perform manipulation/resize on the leash. This will not perform any * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. */ public void scheduleResizePip(Rect toBounds, Consumer updateBoundsCallback) { // Could happen when exitPip if (mToken == null || mLeash == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Abort animation, invalid leash", TAG); return; } mPipBoundsState.setBounds(toBounds); final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, toBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); } else { tx.apply(); } if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } } /** * Directly perform manipulation/resize on the leash, along with rotation. This will not perform * any {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. */ public void scheduleUserResizePip(Rect startBounds, Rect toBounds, Consumer updateBoundsCallback) { scheduleUserResizePip(startBounds, toBounds, 0 /* degrees */, updateBoundsCallback); } /** * Directly perform a scaled matrix transformation on the leash. This will not perform any * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. */ public void scheduleUserResizePip(Rect startBounds, Rect toBounds, float degrees, Consumer updateBoundsCallback) { // Could happen when exitPip if (mToken == null || mLeash == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Abort animation, invalid leash", TAG); return; } if (startBounds.isEmpty() || toBounds.isEmpty()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Attempted to user resize PIP to or from empty bounds, aborting.", TAG); return; } final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .scale(tx, mLeash, startBounds, toBounds, degrees) .round(tx, mLeash, startBounds, toBounds); if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.movePipMenu(mLeash, tx, toBounds, PipMenuController.ALPHA_NO_CHANGE); } else { tx.apply(); } if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } } /** * Finish an intermediate resize operation. This is expected to be called after * {@link #scheduleResizePip}. */ public void scheduleFinishResizePip(Rect destinationBounds) { scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */); } /** * Same as {@link #scheduleFinishResizePip} but with a callback. */ public void scheduleFinishResizePip(Rect destinationBounds, Consumer updateBoundsCallback) { scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback); } /** * Finish an intermediate resize operation. This is expected to be called after * {@link #scheduleResizePip}. * * @param destinationBounds the final bounds of the PIP after resizing * @param direction the transition direction * @param updateBoundsCallback a callback to invoke after finishing the resize */ public void scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer updateBoundsCallback) { if (mPipTransitionState.shouldBlockResizeRequest()) { return; } if (mLeash == null || !mLeash.isValid()) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: scheduleFinishResizePip with null leash! mState=%d", TAG, mPipTransitionState.getTransitionState()); return; } finishResize(createFinishResizeSurfaceTransaction(destinationBounds), destinationBounds, direction, -1); if (updateBoundsCallback != null) { updateBoundsCallback.accept(destinationBounds); } } private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( Rect destinationBounds) { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); return tx; } /** * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation. */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer updateBoundsCallback) { if (mPipTransitionState.shouldBlockResizeRequest() || mPipTransitionState.getInSwipePipToHomeTransition()) { return; } if (mWaitForFixedRotation) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: skip scheduleOffsetPip, entering pip deferred", TAG); return; } offsetPip(originalBounds, 0 /* xOffset */, offset, duration); Rect toBounds = new Rect(originalBounds); toBounds.offset(0, offset); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } } private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { if (mTaskInfo == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: mTaskInfo is not set", TAG); return; } final Rect destinationBounds = new Rect(originalBounds); destinationBounds.offset(xOffset, yOffset); animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_SAME, durationMs, 0); } private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type) { final Rect preResizeBounds = new Rect(mPipBoundsState.getBounds()); mPipBoundsState.setBounds(destinationBounds); if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { removePipImmediately(); return; } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { // TODO: Synchronize this correctly in #applyEnterPipSyncTransaction finishResizeForMenu(destinationBounds); return; } WindowContainerTransaction wct = new WindowContainerTransaction(); prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); // Only corner drag, pinch or expand/un-expand resizing may lead to animating the finish // resize operation. final boolean mayAnimateFinishResize = direction == TRANSITION_DIRECTION_USER_RESIZE || direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE || direction == TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; // Animate with a cross-fade if enabled and seamless resize is disables by the app. final boolean animateCrossFadeResize = mayAnimateFinishResize && mPictureInPictureParams != null && !mPictureInPictureParams.isSeamlessResizeEnabled(); if (animateCrossFadeResize) { // Take a snapshot of the PIP task and show it. We'll fade it out after the wct // transaction is applied and the activity is laid out again. preResizeBounds.offsetTo(0, 0); final Rect snapshotDest = new Rect(0, 0, destinationBounds.width(), destinationBounds.height()); // Note: Put this at layer=MAX_VALUE-2 since the input consumer for PIP is placed at // MAX_VALUE-1 final SurfaceControl snapshotSurface = ScreenshotUtils.takeScreenshot( mSurfaceControlTransactionFactory.getTransaction(), mLeash, preResizeBounds, Integer.MAX_VALUE - 2); if (snapshotSurface != null) { mSyncTransactionQueue.queue(wct); mSyncTransactionQueue.runInSync(t -> { // reset the pinch gesture maybePerformFinishResizeCallback(); // Scale the snapshot from its pre-resize bounds to the post-resize bounds. mSurfaceTransactionHelper.scale(t, snapshotSurface, preResizeBounds, snapshotDest); // Start animation to fade out the snapshot. fadeOutAndRemoveOverlay(snapshotSurface, null /* callback */, false /* withStartDelay */); }); } else { applyFinishBoundsResize(wct, direction, false); } } else { applyFinishBoundsResize(wct, direction, isPipToTopLeft()); // Use sync transaction to apply finish transaction for enter split case. if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { mSyncTransactionQueue.runInSync(t -> { t.merge(tx); }); } } finishResizeForMenu(destinationBounds); } /** Moves the PiP menu to the destination bounds. */ public void finishResizeForMenu(Rect destinationBounds) { if (!isInPip()) { return; } mPipMenuController.movePipMenu(null, null, destinationBounds, PipMenuController.ALPHA_NO_CHANGE); mPipMenuController.updateMenuBounds(destinationBounds); } private void prepareFinishResizeTransaction(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, SurfaceControl.Transaction tx, WindowContainerTransaction wct) { if (mLeash == null || !mLeash.isValid()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Invalid leash on prepareFinishResizeTransaction: %s", TAG, mLeash); return; } final Rect taskBounds; if (isInPipDirection(direction)) { // If we are animating from fullscreen using a bounds animation, then reset the // activity windowing mode set by WM, and set the task bounds to the final bounds taskBounds = destinationBounds; wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); } else if (isOutPipDirection(direction)) { // If we are animating to fullscreen or split screen, then we need to reset the // override bounds on the task to ensure that the task "matches" the parent's bounds. taskBounds = null; applyWindowingModeChangeOnExit(wct, direction); } else { // Just a resize in PIP taskBounds = destinationBounds; } mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); wct.setBounds(mToken, taskBounds); // Pip to split should use sync transaction to sync split bounds change. if (direction != TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { wct.setBoundsChangeTransaction(mToken, tx); } } /** * Applies the window container transaction to finish a bounds resize. * * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has * finished preparing the transaction. It allows subclasses to modify the transaction before * applying it. */ public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, @PipAnimationController.TransitionDirection int direction, boolean wasPipTopLeft) { if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { mSplitScreenOptional.ifPresent(splitScreenController -> splitScreenController.enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct)); } else { mTaskOrganizer.applyTransaction(wct); } } private boolean isPipToTopLeft() { if (!mSplitScreenOptional.isPresent()) { return false; } return mSplitScreenOptional.get().getActivateSplitPosition(mTaskInfo) == SPLIT_POSITION_TOP_OR_LEFT; } /** * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined * and can be overridden to restore to an alternate windowing mode. */ public int getOutPipWindowingMode() { // By default, simply reset the windowing mode to undefined. return WINDOWING_MODE_UNDEFINED; } private @Nullable PipAnimationController.PipTransitionAnimator animateResizePip( Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, float startingAngle) { // Could happen when exitPip if (mToken == null || mLeash == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Abort animation, invalid leash", TAG); return null; } if (isInPipDirection(direction) && !PipBoundsAlgorithm .isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { // The given source rect hint is too small for enter PiP animation, reset it to null. sourceHintRect = null; } final int rotationDelta = mWaitForFixedRotation ? deltaRotation(mCurrentRotation, mNextRotation) : Surface.ROTATION_0; if (rotationDelta != Surface.ROTATION_0) { sourceHintRect = computeRotatedBounds(rotationDelta, direction, destinationBounds, sourceHintRect); } final Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE ? mPipBoundsState.getBounds() : currentBounds; final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null && mPipAnimationController.getCurrentAnimator().isRunning(); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, sourceHintRect, direction, startingAngle, rotationDelta); animator.setTransitionDirection(direction) .setPipTransactionHandler(mPipTransactionHandler) .setDuration(durationMs); if (!existingAnimatorRunning) { animator.setPipAnimationCallback(mPipAnimationCallback); } if (isInPipDirection(direction)) { // Similar to auto-enter-pip transition, we use content overlay when there is no // source rect hint to enter PiP use bounds animation. if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds // animation. // TODO(b/272819817): cleanup the null-check and extra logging. final boolean hasTopActivityInfo = mTaskInfo.topActivityInfo != null; if (hasTopActivityInfo) { animator.setAppIconContentOverlay( mContext, currentBounds, destinationBounds, mTaskInfo.topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } else { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "%s: TaskInfo.topActivityInfo is null", TAG); animator.setColorContentOverlay(mContext); } } else { final TaskSnapshot snapshot = PipUtils.getTaskSnapshot( mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */); if (snapshot != null) { // use the task snapshot during the animation, this is for // launch-into-pip aka. content-pip use case. animator.setSnapshotContentOverlay(snapshot, sourceHintRect); } } mPipOverlay = animator.getContentOverlayLeash(); // The destination bounds are used for the end rect of animation and the final bounds // after animation finishes. So after the animation is started, the destination bounds // can be updated to new rotation (computeRotatedBounds has changed the DisplayLayout // without affecting the animation. if (rotationDelta != Surface.ROTATION_0) { animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds()); } } animator.start(); return animator; } /** Computes destination bounds in old rotation and returns source hint rect if available. * * Note: updates the internal state of {@link PipDisplayLayoutState} by applying a rotation * transformation onto the display layout. */ private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction, Rect outDestinationBounds, Rect sourceHintRect) { if (direction == TRANSITION_DIRECTION_TO_PIP) { mPipDisplayLayoutState.rotateTo(mNextRotation); final Rect displayBounds = mPipBoundsState.getDisplayBounds(); outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); // Transform the destination bounds to current display coordinates. rotateBounds(outDestinationBounds, displayBounds, mNextRotation, mCurrentRotation); // When entering PiP (from button navigation mode), adjust the source rect hint by // display cutout if applicable. if (sourceHintRect != null && mTaskInfo.displayCutoutInsets != null) { if (rotationDelta == Surface.ROTATION_270) { sourceHintRect.offset(mTaskInfo.displayCutoutInsets.left, mTaskInfo.displayCutoutInsets.top); } } } else if (direction == TRANSITION_DIRECTION_LEAVE_PIP) { final Rect rotatedDestinationBounds = new Rect(outDestinationBounds); rotateBounds(rotatedDestinationBounds, mPipBoundsState.getDisplayBounds(), rotationDelta); return PipBoundsAlgorithm.getValidSourceHintRect(mPictureInPictureParams, rotatedDestinationBounds); } return sourceHintRect; } /** * Sync with {@link SplitScreenController} on destination bounds if PiP is going to * split screen. * * @param destinationBoundsOut contain the updated destination bounds if applicable * @return {@code true} if destinationBounds is altered for split screen */ private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit) { if (mSplitScreenOptional.isEmpty()) { return false; } final SplitScreenController split = mSplitScreenOptional.get(); final int position = mTaskInfo.lastParentTaskIdBeforePip > 0 ? split.getSplitPosition(mTaskInfo.lastParentTaskIdBeforePip) : SPLIT_POSITION_UNDEFINED; if (position == SPLIT_POSITION_UNDEFINED && !enterSplit) { return false; } final Rect topLeft = new Rect(); final Rect bottomRight = new Rect(); split.getStageBounds(topLeft, bottomRight); if (enterSplit) { destinationBoundsOut.set(isPipToTopLeft() ? topLeft : bottomRight); return true; } // Moving to an existing split task. destinationBoundsOut.set(position == SPLIT_POSITION_TOP_OR_LEFT ? topLeft : bottomRight); return false; } /** * Fades out and removes an overlay surface. */ void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, boolean withStartDelay) { if (surface == null || !surface.isValid()) { return; } final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f); animator.setDuration(mCrossFadeAnimationDuration); animator.addUpdateListener(animation -> { if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Could happen if onTaskVanished happens during the animation since we may have // set a start delay on this animation. ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Task vanished, skip fadeOutAndRemoveOverlay", TAG); PipAnimationController.quietCancel(animation); } else if (surface.isValid()) { final float alpha = (float) animation.getAnimatedValue(); final SurfaceControl.Transaction transaction = mSurfaceControlTransactionFactory.getTransaction(); transaction.setAlpha(surface, alpha); transaction.apply(); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { removeContentOverlay(surface, callback); } }); animator.setStartDelay(withStartDelay ? CONTENT_OVERLAY_FADE_OUT_DELAY_MS : EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS); animator.start(); } private void removeContentOverlay(SurfaceControl surface, Runnable callback) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "removeContentOverlay: %s, state=%s, surface=%s", mTaskInfo, mPipTransitionState, surface); if (mPipOverlay != null) { if (mPipOverlay != surface) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: trying to remove overlay (%s) which is not local reference (%s)", TAG, surface, mPipOverlay); } clearContentOverlay(); } if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Avoid double removal, which is fatal. ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface); return; } if (surface == null || !surface.isValid()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: trying to remove invalid content overlay (%s)", TAG, surface); return; } final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.remove(surface); tx.apply(); if (callback != null) callback.run(); } void clearContentOverlay() { mPipOverlay = null; mAppBounds.setEmpty(); } void setContentOverlay(@Nullable SurfaceControl leash, @NonNull Rect appBounds) { mPipOverlay = leash; if (mPipOverlay != null) { mAppBounds.set(appBounds); } else { mAppBounds.setEmpty(); } } private void resetShadowRadius() { if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // mLeash is undefined when in PipTransitionState.UNDEFINED return; } final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); tx.setShadowRadius(mLeash, 0f); tx.apply(); } private void cancelCurrentAnimator() { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); // remove any overlays if present if (mPipOverlay != null) { removeContentOverlay(mPipOverlay, null /* callback */); } if (animator != null) { PipAnimationController.quietCancel(animator); mPipAnimationController.resetAnimatorState(); } } @VisibleForTesting public void setSurfaceControlTransactionFactory( PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { mSurfaceControlTransactionFactory = factory; } public boolean isLaunchToSplit(TaskInfo taskInfo) { return mSplitScreenOptional.isPresent() && mSplitScreenOptional.get().isLaunchToSplit(taskInfo); } /** * Dumps internal states. */ @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo); pw.println(innerPrefix + "mToken=" + mToken + " binder=" + (mToken != null ? mToken.asBinder() : null)); pw.println(innerPrefix + "mLeash=" + mLeash); pw.println(innerPrefix + "mPipOverlay=" + mPipOverlay); pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState()); pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); mPipTransitionController.dump(pw, innerPrefix); if (mPipPerfHintController != null) { mPipPerfHintController.dump(pw, innerPrefix); } } @Override public String toString() { return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP); } }