| /* |
| * 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.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_BOTTOM_OR_RIGHT; |
| import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; |
| 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.SystemClock; |
| 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.annotations.ShellMainThread; |
| import com.android.wm.shell.pip.phone.PipMotionHelper; |
| import com.android.wm.shell.protolog.ShellProtoLogGroup; |
| import com.android.wm.shell.splitscreen.SplitScreenController; |
| import com.android.wm.shell.transition.Transitions; |
| |
| import java.io.PrintWriter; |
| 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, ShellTaskOrganizer.FocusListener { |
| private static final String TAG = PipTaskOrganizer.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| /** |
| * The alpha type is set for swiping to home. But the swiped task may not enter PiP. And if |
| * another task enters PiP by non-swipe ways, e.g. call API in foreground or switch to 3-button |
| * navigation, then the alpha type is unexpected. |
| */ |
| private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 1000; |
| |
| /** |
| * 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 final Context mContext; |
| private final SyncTransactionQueue mSyncTransactionQueue; |
| private final PipBoundsState mPipBoundsState; |
| private final PipBoundsAlgorithm mPipBoundsAlgorithm; |
| private final @NonNull PipMenuController mPipMenuController; |
| private final PipAnimationController mPipAnimationController; |
| private 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<SplitScreenController> mSplitScreenOptional; |
| protected final ShellTaskOrganizer mTaskOrganizer; |
| protected final ShellExecutor mMainExecutor; |
| |
| // These callbacks are called on the update thread |
| private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = |
| new PipAnimationController.PipAnimationCallback() { |
| @Override |
| public void onPipAnimationStart(TaskInfo taskInfo, |
| PipAnimationController.PipTransitionAnimator animator) { |
| final int direction = animator.getTransitionDirection(); |
| sendOnPipTransitionStarted(direction); |
| } |
| |
| @Override |
| public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, |
| PipAnimationController.PipTransitionAnimator animator) { |
| final int direction = animator.getTransitionDirection(); |
| final int animationType = animator.getAnimationType(); |
| final Rect destinationBounds = animator.getDestinationBounds(); |
| if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { |
| fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), |
| animator::clearContentOverlay, 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) { |
| // 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. |
| finishResize(tx, destinationBounds, direction, animationType); |
| sendOnPipTransitionFinished(direction); |
| } |
| } |
| |
| @Override |
| public void onPipAnimationCancel(TaskInfo taskInfo, |
| PipAnimationController.PipTransitionAnimator animator) { |
| final int direction = animator.getTransitionDirection(); |
| if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { |
| fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), |
| animator::clearContentOverlay, true /* withStartDelay */); |
| } |
| sendOnPipTransitionCancelled(direction); |
| } |
| }; |
| |
| private final PipAnimationController.PipTransactionHandler mPipTransactionHandler = |
| new PipAnimationController.PipTransactionHandler() { |
| @Override |
| public boolean handlePipTransaction(SurfaceControl leash, |
| SurfaceControl.Transaction tx, Rect destinationBounds) { |
| if (mPipMenuController.isMenuVisible()) { |
| mPipMenuController.movePipMenu(leash, tx, destinationBounds); |
| 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; |
| private SurfaceControl mLeash; |
| private PipTransitionState mPipTransitionState; |
| private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; |
| private long mLastOneShotAlphaAnimationTime; |
| private 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, only set if |
| * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true. |
| */ |
| @Nullable |
| SurfaceControl mSwipePipToHomeOverlay; |
| |
| public PipTaskOrganizer(Context context, |
| @NonNull SyncTransactionQueue syncTransactionQueue, |
| @NonNull PipTransitionState pipTransitionState, |
| @NonNull PipBoundsState pipBoundsState, |
| @NonNull PipBoundsAlgorithm boundsHandler, |
| @NonNull PipMenuController pipMenuController, |
| @NonNull PipAnimationController pipAnimationController, |
| @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, |
| @NonNull PipTransitionController pipTransitionController, |
| @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, |
| Optional<SplitScreenController> splitScreenOptional, |
| @NonNull DisplayController displayController, |
| @NonNull PipUiEventLogger pipUiEventLogger, |
| @NonNull ShellTaskOrganizer shellTaskOrganizer, |
| @ShellMainThread ShellExecutor mainExecutor) { |
| mContext = context; |
| mSyncTransactionQueue = syncTransactionQueue; |
| mPipTransitionState = pipTransitionState; |
| mPipBoundsState = pipBoundsState; |
| 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 = SurfaceControl.Transaction::new; |
| mSplitScreenOptional = splitScreenOptional; |
| mTaskOrganizer = shellTaskOrganizer; |
| mMainExecutor = mainExecutor; |
| |
| // TODO: Can be removed once wm components are created on the shell-main thread |
| mMainExecutor.execute(() -> { |
| mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); |
| }); |
| mTaskOrganizer.addFocusListener(this); |
| mPipTransitionController.setPipOrganizer(this); |
| displayController.addDisplayWindowListener(this); |
| } |
| |
| public PipTransitionController getTransitionController() { |
| return mPipTransitionController; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Sets the preferred animation type for one time. |
| * This is typically used to set the animation type to |
| * {@link PipAnimationController#ANIM_TYPE_ALPHA}. |
| */ |
| public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { |
| mOneShotAnimationType = animationType; |
| if (animationType == ANIM_TYPE_ALPHA) { |
| mLastOneShotAlphaAnimationTime = SystemClock.uptimeMillis(); |
| } |
| } |
| |
| /** |
| * Callback when Launcher starts swipe-pip-to-home operation. |
| * @return {@link Rect} for destination bounds. |
| */ |
| public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, |
| PictureInPictureParams pictureInPictureParams) { |
| mPipTransitionState.setInSwipePipToHomeTransition(true); |
| sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); |
| setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); |
| return mPipBoundsAlgorithm.getEntryDestinationBounds(); |
| } |
| |
| /** |
| * Callback when launcher finishes swipe-pip-to-home operation. |
| * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. |
| */ |
| public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, |
| SurfaceControl overlay) { |
| // do nothing if there is no startSwipePipToHome being called before |
| if (!mPipTransitionState.getInSwipePipToHomeTransition()) { |
| return; |
| } |
| mPipBoundsState.setBounds(destinationBounds); |
| mSwipePipToHomeOverlay = overlay; |
| 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 = new SurfaceControl.Transaction(); |
| mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t); |
| t.setLayer(overlay, Integer.MAX_VALUE); |
| t.apply(); |
| } |
| } |
| |
| 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; |
| } |
| |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| if (isLaunchIntoPipTask()) { |
| exitLaunchIntoPipTask(wct); |
| return; |
| } |
| |
| if (ENABLE_SHELL_TRANSITIONS) { |
| if (requestEnterSplit && mSplitScreenOptional.isPresent()) { |
| mSplitScreenOptional.get().prepareEnterSplitScreen(wct, mTaskInfo, |
| isPipTopLeft() |
| ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); |
| mPipTransitionController.startExitTransition( |
| TRANSIT_EXIT_PIP_TO_SPLIT, wct, null /* destinationBounds */); |
| return; |
| } |
| } |
| |
| final Rect destinationBounds = getExitDestinationBounds(); |
| final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit) |
| ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN |
| : TRANSITION_DIRECTION_LEAVE_PIP; |
| |
| if (Transitions.ENABLE_SHELL_TRANSITIONS && direction == TRANSITION_DIRECTION_LEAVE_PIP) { |
| // 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, WINDOWING_MODE_UNDEFINED); |
| // 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 { |
| 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) { |
| mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds); |
| return; |
| } |
| 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(); |
| } |
| |
| private 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) { |
| ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, |
| "%s: Not allowed to removePip in current state" |
| + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(), |
| mToken); |
| 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); |
| } |
| |
| private void removePipImmediately() { |
| if (Transitions.ENABLE_SHELL_TRANSITIONS) { |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| wct.setBounds(mToken, null); |
| wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); |
| 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); |
| mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); |
| |
| // 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 != mPipBoundsState.getDisplayId() |
| && mOnDisplayIdChangeCallback != null) { |
| mOnDisplayIdChangeCallback.accept(info.displayId); |
| } |
| |
| 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; |
| } |
| |
| if (mOneShotAnimationType == ANIM_TYPE_ALPHA |
| && SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime |
| > ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) { |
| ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, |
| "%s: Alpha animation is expired. Use bounds animation.", TAG); |
| mOneShotAnimationType = ANIM_TYPE_BOUNDS; |
| } |
| |
| if (Transitions.ENABLE_SHELL_TRANSITIONS) { |
| // For Shell transition, we will animate the window in PipTransition#startAnimation |
| // instead of #onTaskAppeared. |
| return; |
| } |
| |
| if (mWaitForFixedRotation) { |
| onTaskAppearedWithFixedRotation(); |
| return; |
| } |
| |
| final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); |
| Objects.requireNonNull(destinationBounds, "Missing destination bounds"); |
| final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); |
| |
| if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { |
| 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 (mOneShotAnimationType == ANIM_TYPE_ALPHA) { |
| enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); |
| mOneShotAnimationType = ANIM_TYPE_BOUNDS; |
| } else { |
| throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); |
| } |
| } |
| |
| private void onTaskAppearedWithFixedRotation() { |
| if (mOneShotAnimationType == 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(); |
| mOneShotAnimationType = ANIM_TYPE_BOUNDS; |
| 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 = mSwipePipToHomeOverlay; |
| 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); |
| mSwipePipToHomeOverlay = 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. |
| 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); |
| // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent. |
| if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) { |
| onTaskInfoChanged(mDeferredTaskInfo); |
| mDeferredTaskInfo = null; |
| } |
| } |
| |
| 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) { |
| 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; |
| } |
| |
| cancelCurrentAnimator(); |
| onExitPipFinished(info); |
| |
| if (Transitions.ENABLE_SHELL_TRANSITIONS) { |
| mPipTransitionController.forceFinishTransition(); |
| } |
| } |
| |
| @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; |
| |
| // mPictureInPictureParams is only null if there is no PiP |
| if (newParams == null || mPictureInPictureParams == null) { |
| return; |
| } |
| applyNewPictureInPictureParams(newParams); |
| mPictureInPictureParams = newParams; |
| } |
| |
| @Override |
| public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { |
| mPipMenuController.onFocusTaskChanged(taskInfo); |
| } |
| |
| @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) { |
| 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) { |
| if (!mWaitForFixedRotation) { |
| return; |
| } |
| if (Transitions.ENABLE_SHELL_TRANSITIONS) { |
| 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. */ |
| void onExitPipFinished(TaskInfo info) { |
| clearWaitForFixedRotation(); |
| if (mSwipePipToHomeOverlay != null) { |
| removeContentOverlay(mSwipePipToHomeOverlay, null /* callback */); |
| mSwipePipToHomeOverlay = 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); |
| mMainExecutor.executeDelayed(() -> mPipMenuController.detach(), 0); |
| 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) { |
| 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) { |
| if (DEBUG) { |
| 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; |
| 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(newDestinationBounds); |
| rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); |
| animator.updateEndValue(rotatedEndBounds); |
| } else { |
| animator.updateEndValue(newDestinationBounds); |
| } |
| } |
| animator.setDestinationBounds(newDestinationBounds); |
| destinationBoundsOut.set(newDestinationBounds); |
| } |
| |
| /** |
| * Handles all changes to the PictureInPictureParams. |
| */ |
| protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { |
| if (mDeferredTaskInfo != null || PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), |
| mPictureInPictureParams.getAspectRatioFloat())) { |
| mPipParamsChangedForwarder.notifyAspectRatioChanged(params.getAspectRatioFloat()); |
| } |
| if (mDeferredTaskInfo != null |
| || 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<Rect> 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<Rect> 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. |
| */ |
| public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, |
| float startingAngle, Consumer<Rect> 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<Rect> 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<Rect> 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 (mPipMenuController.isMenuVisible()) { |
| 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<Rect> 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<Rect> 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 (mPipMenuController.isMenuVisible()) { |
| mPipMenuController.movePipMenu(mLeash, tx, toBounds); |
| } 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<Rect> 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<Rect> updateBoundsCallback) { |
| if (mPipTransitionState.shouldBlockResizeRequest()) { |
| 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<Rect> 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()); |
| final boolean isPipTopLeft = isPipTopLeft(); |
| 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 -> { |
| // 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, isPipTopLeft); |
| } |
| } else { |
| applyFinishBoundsResize(wct, direction, isPipTopLeft); |
| } |
| |
| finishResizeForMenu(destinationBounds); |
| } |
| |
| /** Moves the PiP menu to the destination bounds. */ |
| public void finishResizeForMenu(Rect destinationBounds) { |
| if (!isInPip()) { |
| return; |
| } |
| mPipMenuController.movePipMenu(null, null, destinationBounds); |
| mPipMenuController.updateMenuBounds(destinationBounds); |
| } |
| |
| private void prepareFinishResizeTransaction(Rect destinationBounds, |
| @PipAnimationController.TransitionDirection int direction, |
| SurfaceControl.Transaction tx, |
| WindowContainerTransaction wct) { |
| 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); |
| 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 isPipTopLeft() { |
| if (!mSplitScreenOptional.isPresent()) { |
| return false; |
| } |
| final Rect topLeft = new Rect(); |
| final Rect bottomRight = new Rect(); |
| mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); |
| |
| return topLeft.contains(mPipBoundsState.getBounds()); |
| } |
| |
| /** |
| * 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) |
| && !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); |
| } |
| 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) { |
| 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); |
| } |
| } |
| // 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. */ |
| private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction, |
| Rect outDestinationBounds, Rect sourceHintRect) { |
| if (direction == TRANSITION_DIRECTION_TO_PIP) { |
| mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), 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; |
| } |
| |
| /** |
| * This is a situation in which the source rect hint on at least one axis is smaller |
| * than the destination bounds, which represents a problem because we would have to scale |
| * up that axis to fit the bounds. So instead, just fallback to the non-source hint |
| * animation in this case. |
| * |
| * @return {@code false} if the given source is too small to use for the entering animation. |
| */ |
| private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { |
| return sourceRectHint != null |
| && sourceRectHint.width() > destinationBounds.width() |
| && sourceRectHint.height() > destinationBounds.height(); |
| } |
| |
| /** |
| * 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 (!enterSplit || !mSplitScreenOptional.isPresent()) { |
| return false; |
| } |
| final Rect topLeft = new Rect(); |
| final Rect bottomRight = new Rect(); |
| mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); |
| final boolean isPipTopLeft = isPipTopLeft(); |
| destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); |
| return true; |
| } |
| |
| /** |
| * 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 : 0); |
| animator.start(); |
| } |
| |
| private void removeContentOverlay(SurfaceControl surface, Runnable callback) { |
| if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { |
| // Avoid double removal, which is fatal. |
| 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(); |
| } |
| |
| 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(); |
| if (animator != null) { |
| if (animator.getContentOverlayLeash() != null) { |
| removeContentOverlay(animator.getContentOverlayLeash(), |
| animator::clearContentOverlay); |
| } |
| PipAnimationController.quietCancel(animator); |
| } |
| } |
| |
| @VisibleForTesting |
| public void setSurfaceControlTransactionFactory( |
| PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { |
| mSurfaceControlTransactionFactory = factory; |
| } |
| |
| /** |
| * 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 + "mState=" + mPipTransitionState.getTransitionState()); |
| pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); |
| pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); |
| } |
| |
| @Override |
| public String toString() { |
| return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP); |
| } |
| } |