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