/* * 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.systemui.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_BOUNDS; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_SPLIT_SCREEN; import static com.android.systemui.pip.PipAnimationController.isInPipDirection; import static com.android.systemui.pip.PipAnimationController.isOutPipDirection; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.util.EventLog; import android.util.Log; import android.util.Size; import android.view.SurfaceControl; import android.window.TaskOrganizer; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import android.window.WindowOrganizer; import com.android.internal.os.SomeArgs; import com.android.systemui.R; import com.android.systemui.pip.phone.PipUpdateThread; import com.android.systemui.stackdivider.Divider; import com.android.systemui.wm.DisplayController; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Singleton; /** * 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 com.android.systemui.pip.phone.PipMotionHelper}. */ @Singleton public class PipTaskOrganizer extends TaskOrganizer implements DisplayController.OnDisplaysChangedListener { private static final String TAG = PipTaskOrganizer.class.getSimpleName(); private static final boolean DEBUG = false; private static final int MSG_RESIZE_IMMEDIATE = 1; private static final int MSG_RESIZE_ANIMATE = 2; private static final int MSG_OFFSET_ANIMATE = 3; private static final int MSG_FINISH_RESIZE = 4; private static final int MSG_RESIZE_USER = 5; // Not a complete set of states but serves what we want right now. private enum State { UNDEFINED(0), TASK_APPEARED(1), ENTERING_PIP(2), EXITING_PIP(3); private final int mStateValue; State(int value) { mStateValue = value; } private boolean isInPip() { return mStateValue >= TASK_APPEARED.mStateValue && mStateValue != EXITING_PIP.mStateValue; } /** * Resize request can be initiated in other component, ignore if we are no longer in PIP, * still waiting for animation or we're exiting from it. * * @return {@code true} if the resize request should be blocked/ignored. */ private boolean shouldBlockResizeRequest() { return mStateValue < ENTERING_PIP.mStateValue || mStateValue == EXITING_PIP.mStateValue; } } private final Handler mMainHandler; private final Handler mUpdateHandler; private final PipBoundsHandler mPipBoundsHandler; private final PipAnimationController mPipAnimationController; private final PipUiEventLogger mPipUiEventLoggerLogger; private final List mPipTransitionCallbacks = new ArrayList<>(); private final Rect mLastReportedBounds = new Rect(); private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Map mCompactState = new HashMap<>(); private final Divider mSplitDivider; // These callbacks are called on the update thread private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { @Override public void onPipAnimationStart(PipAnimationController.PipTransitionAnimator animator) { sendOnPipTransitionStarted(animator.getTransitionDirection()); } @Override public void onPipAnimationEnd(SurfaceControl.Transaction tx, PipAnimationController.PipTransitionAnimator animator) { finishResize(tx, animator.getDestinationBounds(), animator.getTransitionDirection(), animator.getAnimationType()); sendOnPipTransitionFinished(animator.getTransitionDirection()); } @Override public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) { sendOnPipTransitionCancelled(animator.getTransitionDirection()); } }; @SuppressWarnings("unchecked") private final Handler.Callback mUpdateCallbacks = (msg) -> { SomeArgs args = (SomeArgs) msg.obj; Consumer updateBoundsCallback = (Consumer) args.arg1; switch (msg.what) { case MSG_RESIZE_IMMEDIATE: { Rect toBounds = (Rect) args.arg2; resizePip(toBounds); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } break; } case MSG_RESIZE_ANIMATE: { Rect currentBounds = (Rect) args.arg2; Rect toBounds = (Rect) args.arg3; Rect sourceHintRect = (Rect) args.arg4; int duration = args.argi2; animateResizePip(currentBounds, toBounds, sourceHintRect, args.argi1 /* direction */, duration); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } break; } case MSG_OFFSET_ANIMATE: { Rect originalBounds = (Rect) args.arg2; final int offset = args.argi1; final int duration = args.argi2; offsetPip(originalBounds, 0 /* xOffset */, offset, duration); Rect toBounds = new Rect(originalBounds); toBounds.offset(0, offset); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } break; } case MSG_FINISH_RESIZE: { SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2; Rect toBounds = (Rect) args.arg3; finishResize(tx, toBounds, args.argi1 /* direction */, -1); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } break; } case MSG_RESIZE_USER: { Rect startBounds = (Rect) args.arg2; Rect toBounds = (Rect) args.arg3; userResizePip(startBounds, toBounds); break; } } args.recycle(); return true; }; private ActivityManager.RunningTaskInfo mTaskInfo; private WindowContainerToken mToken; private SurfaceControl mLeash; private State mState = State.UNDEFINED; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; private PictureInPictureParams mPictureInPictureParams; private int mOverridableMinSize; /** * 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 mShouldDeferEnteringPip; private @ActivityInfo.ScreenOrientation int mRequestedOrientation; @Inject public PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @Nullable Divider divider, @NonNull DisplayController displayController, @NonNull PipAnimationController pipAnimationController, @NonNull PipUiEventLogger pipUiEventLogger) { mMainHandler = new Handler(Looper.getMainLooper()); mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); mPipBoundsHandler = boundsHandler; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); mOverridableMinSize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.overridable_minimal_size_pip_resizable_task); mSurfaceTransactionHelper = surfaceTransactionHelper; mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; mSplitDivider = divider; displayController.addDisplayWindowListener(this); } public Handler getUpdateHandler() { return mUpdateHandler; } public Rect getLastReportedBounds() { return new Rect(mLastReportedBounds); } public Rect getCurrentOrAnimatingBounds() { PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); if (animator != null && animator.isRunning()) { return new Rect(animator.getDestinationBounds()); } return getLastReportedBounds(); } public boolean isInPip() { return mState.isInPip(); } public boolean isDeferringEnterPipAnimation() { return mState.isInPip() && mShouldDeferEnteringPip; } /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ public void registerPipTransitionCallback(PipTransitionCallback callback) { mPipTransitionCallbacks.add(callback); } /** * 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; } /** * 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 */ public void exitPip(int animationDurationMs) { if (!mState.isInPip() || mToken == null) { Log.wtf(TAG, "Not allowed to exitPip in current state" + " mState=" + mState + " mToken=" + mToken); return; } final PipWindowConfigurationCompact config = mCompactState.remove(mToken.asBinder()); if (config == null) { Log.wtf(TAG, "Token not in record, this should not happen mToken=" + mToken); return; } mPipUiEventLoggerLogger.log( PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); config.syncWithScreenOrientation(mRequestedOrientation, mPipBoundsHandler.getDisplayRotation()); final boolean orientationDiffers = config.getRotation() != mPipBoundsHandler.getDisplayRotation(); final WindowContainerTransaction wct = new WindowContainerTransaction(); final Rect destinationBounds = config.getBounds(); final int direction = syncWithSplitScreenBounds(destinationBounds) ? TRANSITION_DIRECTION_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_TO_FULLSCREEN; if (orientationDiffers) { mState = State.EXITING_PIP; // Send started callback though animation is ignored. sendOnPipTransitionStarted(direction); // Don't bother doing an animation if the display rotation differs or if it's in // a non-supported windowing mode applyWindowingModeChangeOnExit(wct, direction); WindowOrganizer.applyTransaction(wct); // Send finished callback though animation is ignored. sendOnPipTransitionFinished(direction); } else { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, mLastReportedBounds); tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); wct.setActivityWindowingMode(mToken, direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY : WINDOWING_MODE_FULLSCREEN); wct.setBounds(mToken, destinationBounds); wct.setBoundsChangeTransaction(mToken, tx); applySyncTransaction(wct, new WindowContainerTransactionCallback() { @Override public void onTransactionReady(int id, SurfaceControl.Transaction t) { t.apply(); scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, null /* sourceHintRect */, direction, animationDurationMs, null /* updateBoundsCallback */); mState = State.EXITING_PIP; } }); } } 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); if (mSplitDivider != null && direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN) { wct.reparent(mToken, mSplitDivider.getSecondaryRoot(), true /* onTop */); } } /** * Removes PiP immediately. */ public void removePip() { if (!mState.isInPip() || mToken == null) { Log.wtf(TAG, "Not allowed to removePip in current state" + " mState=" + mState + " mToken=" + mToken); return; } // removePipImmediately is expected when the following animation finishes. mUpdateHandler.post(() -> mPipAnimationController .getAnimator(mLeash, mLastReportedBounds, 1f, 0f) .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) .start()); mCompactState.remove(mToken.asBinder()); mState = State.EXITING_PIP; } private void removePipImmediately() { try { // Reset the task bounds first to ensure the activity configuration is reset as well final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mToken, null); WindowOrganizer.applyTransaction(wct); ActivityTaskManager.getService().removeStacksInWindowingModes( new int[]{ WINDOWING_MODE_PINNED }); } catch (RemoteException e) { Log.e(TAG, "Failed to remove PiP", e); } } @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { Objects.requireNonNull(info, "Requires RunningTaskInfo"); mTaskInfo = info; mToken = mTaskInfo.token; mState = State.TASK_APPEARED; mLeash = leash; mCompactState.put(mToken.asBinder(), new PipWindowConfigurationCompact(mTaskInfo.configuration.windowConfiguration)); mPictureInPictureParams = mTaskInfo.pictureInPictureParams; mRequestedOrientation = info.requestedOrientation; mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); if (mShouldDeferEnteringPip) { if (DEBUG) Log.d(TAG, "Defer entering PiP animation, fixed rotation is ongoing"); // if deferred, hide 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 destinationBounds = mPipBoundsHandler.getDestinationBounds( mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); Objects.requireNonNull(destinationBounds, "Missing destination bounds"); final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { final Rect sourceHintRect = getValidSourceHintRect(info, currentBounds); scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, null /* updateBoundsCallback */); mState = State.ENTERING_PIP; } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { enterPipWithAlphaAnimation(destinationBounds, mEnterExitAnimationDuration); mOneShotAnimationType = ANIM_TYPE_BOUNDS; } else { throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); } } /** * Returns the source hint rect if it is valid (if provided and is contained by the current * task bounds). */ private Rect getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds) { final Rect sourceHintRect = info.pictureInPictureParams != null && info.pictureInPictureParams.hasSourceBoundsHint() ? info.pictureInPictureParams.getSourceRectHint() : null; if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { return sourceHintRect; } return null; } private 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(); final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); wct.setBounds(mToken, destinationBounds); wct.scheduleFinishEnterPip(mToken, destinationBounds); applySyncTransaction(wct, new WindowContainerTransactionCallback() { @Override public void onTransactionReady(int id, SurfaceControl.Transaction t) { t.apply(); mUpdateHandler.post(() -> mPipAnimationController .getAnimator(mLeash, destinationBounds, 0f, 1f) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .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. mState = State.ENTERING_PIP; } }); } private void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { runOnMainHandler(() -> { for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); callback.onPipTransitionStarted(mTaskInfo.baseActivity, direction); } }); } private void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { runOnMainHandler(() -> { for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); callback.onPipTransitionFinished(mTaskInfo.baseActivity, direction); } }); } private void sendOnPipTransitionCancelled( @PipAnimationController.TransitionDirection int direction) { runOnMainHandler(() -> { for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction); } }); } private void runOnMainHandler(Runnable r) { if (Looper.getMainLooper() == Looper.myLooper()) { r.run(); } else { mMainHandler.post(r); } } /** * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}. * Meanwhile this callback is invoked whenever the task is removed. For instance: * - as a result of removeStacksInWindowingModes 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 (!mState.isInPip()) { return; } final WindowContainerToken token = info.token; Objects.requireNonNull(token, "Requires valid WindowContainerToken"); if (token.asBinder() != mToken.asBinder()) { Log.wtf(TAG, "Unrecognized token: " + token); return; } mShouldDeferEnteringPip = false; mPictureInPictureParams = null; mState = State.UNDEFINED; mPipUiEventLoggerLogger.setTaskInfo(null); } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); mRequestedOrientation = info.requestedOrientation; // check PictureInPictureParams for aspect ratio change. final PictureInPictureParams newParams = info.pictureInPictureParams; if (newParams == null || !applyPictureInPictureParams(newParams)) { Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); return; } final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( info.topActivity, getAspectRatioOrDefault(newParams), mLastReportedBounds, getMinimalSize(info.topActivityInfo), true /* userCurrentMinEdgeSize */); Objects.requireNonNull(destinationBounds, "Missing destination bounds"); scheduleAnimateResizePip(destinationBounds, mEnterExitAnimationDuration, null /* updateBoundsCallback */); } @Override public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { // Do nothing } @Override public void onFixedRotationStarted(int displayId, int newRotation) { mShouldDeferEnteringPip = true; } @Override public void onFixedRotationFinished(int displayId) { if (mShouldDeferEnteringPip && mState.isInPip()) { final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); // schedule a regular animation to ensure all the callbacks are still being sent enterPipWithAlphaAnimation(destinationBounds, 0 /* durationMs */); } mShouldDeferEnteringPip = false; } /** * @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) { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { if (mState.isInPip() && fromRotation) { // 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(); animator.removeAllUpdateListeners(); animator.removeAllListeners(); animator.cancel(); // Do notify the listeners that this was canceled sendOnPipTransitionCancelled(direction); sendOnPipTransitionFinished(direction); } mLastReportedBounds.set(destinationBoundsOut); // 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 (!mLastReportedBounds.isEmpty()) { destinationBoundsOut.set(mLastReportedBounds); } } } return; } final Rect currentDestinationBounds = animator.getDestinationBounds(); destinationBoundsOut.set(currentDestinationBounds); if (!fromImeAdjustment && !fromShelfAdjustment && mPipBoundsHandler.getDisplayBounds().contains(currentDestinationBounds)) { // no need to update the destination bounds, bail early return; } final Rect newDestinationBounds = mPipBoundsHandler.getDestinationBounds( mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); if (newDestinationBounds.equals(currentDestinationBounds)) return; if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { animator.updateEndValue(newDestinationBounds); } animator.setDestinationBounds(newDestinationBounds); destinationBoundsOut.set(newDestinationBounds); } /** * @return {@code true} if the aspect ratio is changed since no other parameters within * {@link PictureInPictureParams} would affect the bounds. */ private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { final boolean changed = (mPictureInPictureParams == null) || !Objects.equals( mPictureInPictureParams.getAspectRatioRational(), params.getAspectRatioRational()); if (changed) { mPictureInPictureParams = params; mPipBoundsHandler.onAspectRatioChanged(params.getAspectRatio()); } return changed; } /** * Animates resizing of the pinned stack given the duration. */ public void scheduleAnimateResizePip(Rect toBounds, int duration, Consumer updateBoundsCallback) { if (mShouldDeferEnteringPip) { Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); return; } scheduleAnimateResizePip(mLastReportedBounds, toBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); } private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer updateBoundsCallback) { if (!mState.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; } SomeArgs args = SomeArgs.obtain(); args.arg1 = updateBoundsCallback; args.arg2 = currentBounds; args.arg3 = destinationBounds; args.arg4 = sourceHintRect; args.argi1 = direction; args.argi2 = durationMs; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); } /** * 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) { SomeArgs args = SomeArgs.obtain(); args.arg1 = updateBoundsCallback; args.arg2 = toBounds; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args)); } /** * 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, Consumer updateBoundsCallback) { SomeArgs args = SomeArgs.obtain(); args.arg1 = updateBoundsCallback; args.arg2 = startBounds; args.arg3 = toBounds; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_USER, args)); } /** * 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); } private void scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer updateBoundsCallback) { if (mState.shouldBlockResizeRequest()) { return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = updateBoundsCallback; args.arg2 = createFinishResizeSurfaceTransaction( destinationBounds); args.arg3 = destinationBounds; args.argi1 = direction; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_FINISH_RESIZE, args)); } private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( Rect destinationBounds) { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) .round(tx, mLeash, mState.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 (mState.shouldBlockResizeRequest()) { return; } if (mShouldDeferEnteringPip) { Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = updateBoundsCallback; args.arg2 = originalBounds; // offset would be zero if triggered from screen rotation. args.argi1 = offset; args.argi2 = duration; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args)); } private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleOffsetPip() instead of this " + "directly"); } if (mTaskInfo == null) { Log.w(TAG, "mTaskInfo is not set"); return; } final Rect destinationBounds = new Rect(originalBounds); destinationBounds.offset(xOffset, yOffset); animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_SAME, durationMs); } private void resizePip(Rect destinationBounds) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleResizePip() instead of this " + "directly"); } // Could happen when exitPip if (mToken == null || mLeash == null) { Log.w(TAG, "Abort animation, invalid leash"); return; } mLastReportedBounds.set(destinationBounds); final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .round(tx, mLeash, mState.isInPip()); tx.apply(); } private void userResizePip(Rect startBounds, Rect destinationBounds) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleUserResizePip() instead of " + "this directly"); } // Could happen when exitPip if (mToken == null || mLeash == null) { Log.w(TAG, "Abort animation, invalid leash"); return; } if (startBounds.isEmpty() || destinationBounds.isEmpty()) { Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting."); return; } final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds); tx.apply(); } private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleResizePip() instead of this " + "directly"); } mLastReportedBounds.set(destinationBounds); if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { removePipImmediately(); return; } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); applyFinishBoundsResize(wct, direction); } 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); wct.scheduleFinishEnterPip(mToken, destinationBounds); } else if (isOutPipDirection(direction)) { // If we are animating to fullscreen, then we need to reset the override bounds // on the task to ensure that the task "matches" the parent's bounds. taskBounds = (direction == TRANSITION_DIRECTION_TO_FULLSCREEN) ? null : destinationBounds; applyWindowingModeChangeOnExit(wct, direction); } else { // Just a resize in PIP taskBounds = destinationBounds; } 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) { WindowOrganizer.applyTransaction(wct); } /** * 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 void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of " + "this directly"); } // Could happen when exitPip if (mToken == null || mLeash == null) { Log.w(TAG, "Abort animation, invalid leash"); return; } mPipAnimationController .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect) .setTransitionDirection(direction) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) .start(); } private Size getMinimalSize(ActivityInfo activityInfo) { if (activityInfo == null || activityInfo.windowLayout == null) { return null; } final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; // -1 will be populated if an activity specifies defaultWidth/defaultHeight in // without minWidth/minHeight if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { // If either dimension is smaller than the allowed minimum, adjust them // according to mOverridableMinSize and log to SafeNet if (windowLayout.minWidth < mOverridableMinSize || windowLayout.minHeight < mOverridableMinSize) { EventLog.writeEvent(0x534e4554, "174302616", -1, ""); } return new Size(Math.max(windowLayout.minWidth, mOverridableMinSize), Math.max(windowLayout.minHeight, mOverridableMinSize)); } return null; } private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) { return params == null || !params.hasSetAspectRatio() ? mPipBoundsHandler.getDefaultAspectRatio() : params.getAspectRatio(); } /** * Sync with {@link #mSplitDivider} 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) { if (mSplitDivider == null || !mSplitDivider.isDividerVisible()) { // bail early if system is not in split screen mode return false; } // PiP window will go to split-secondary mode instead of fullscreen, populates the // split screen bounds here. destinationBoundsOut.set( mSplitDivider.getView().getNonMinimizedSplitScreenSecondaryBounds()); return true; } /** * Dumps internal states. */ 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=" + mState); pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); pw.println(innerPrefix + "mLastReportedBounds=" + mLastReportedBounds); pw.println(innerPrefix + "mInitialState:"); for (Map.Entry e : mCompactState.entrySet()) { pw.println(innerPrefix + " binder=" + e.getKey() + " config=" + e.getValue()); } } /** * Callback interface for PiP transitions (both from and to PiP mode) */ public interface PipTransitionCallback { /** * Callback when the pip transition is started. */ void onPipTransitionStarted(ComponentName activity, int direction); /** * Callback when the pip transition is finished. */ void onPipTransitionFinished(ComponentName activity, int direction); /** * Callback when the pip transition is cancelled. */ void onPipTransitionCanceled(ComponentName activity, int direction); } }