blob: f839363b2da8116de5eae118605d905a832bb739 [file] [log] [blame]
/*
* 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.phone;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static android.view.WindowManager.INPUT_CONSUMER_PIP;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
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_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.isOutPipDirection;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
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.UserHandle;
import android.os.UserManager;
import android.util.Pair;
import android.util.Size;
import android.view.DisplayInfo;
import android.view.SurfaceControl;
import android.view.WindowManagerGlobal;
import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.pip.IPip;
import com.android.wm.shell.pip.IPipAnimationListener;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
/**
* Manages the picture-in-picture (PIP) UI and states for Phones.
*/
public class PipController implements PipTransitionController.PipTransitionCallback,
RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener {
private static final String TAG = "PipController";
private Context mContext;
protected ShellExecutor mMainExecutor;
private DisplayController mDisplayController;
private PipInputConsumer mPipInputConsumer;
private WindowManagerShellWrapper mWindowManagerShellWrapper;
private PipAppOpsListener mAppOpsListener;
private PipMediaController mMediaController;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
private PipBoundsState mPipBoundsState;
private PipMotionHelper mPipMotionHelper;
private PipTouchHandler mTouchHandler;
private PipTransitionController mPipTransitionController;
private TaskStackListenerImpl mTaskStackListener;
private PipParamsChangedForwarder mPipParamsChangedForwarder;
private Optional<OneHandedController> mOneHandedController;
private final ShellCommandHandler mShellCommandHandler;
private final ShellController mShellController;
protected final PipImpl mImpl;
private final Rect mTmpInsetBounds = new Rect();
private final int mEnterAnimationDuration;
private boolean mIsInFixedRotation;
private PipAnimationListener mPinnedStackAnimationRecentsCallback;
protected PhonePipMenuController mMenuController;
protected PipTaskOrganizer mPipTaskOrganizer;
private PipTransitionState mPipTransitionState;
protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener =
new PipControllerPinnedTaskListener();
private boolean mIsKeyguardShowingOrAnimating;
private Consumer<Boolean> mOnIsInPipStateChangedListener;
private interface PipAnimationListener {
/**
* Notifies the listener that the Pip animation is started.
*/
void onPipAnimationStarted();
/**
* Notifies the listener about PiP resource dimensions changed.
* Listener can expect an immediate callback the first time they attach.
*
* @param cornerRadius the pixel value of the corner radius, zero means it's disabled.
* @param shadowRadius the pixel value of the shadow radius, zero means it's disabled.
*/
void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius);
/**
* Notifies the listener that user leaves PiP by tapping on the expand button.
*/
void onExpandPip();
}
/**
* Handler for display rotation changes.
*/
private final DisplayChangeController.OnDisplayChangingListener mRotationController = (
displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> {
if (mPipTransitionController.handleRotateDisplay(fromRotation, toRotation, t)) {
return;
}
if (mPipBoundsState.getDisplayLayout().rotation() == toRotation) {
// The same rotation may have been set by auto PiP-able or fixed rotation. So notify
// the change with fromRotation=false to apply the rotated destination bounds from
// PipTaskOrganizer#onMovementBoundsChanged.
updateMovementBounds(null, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t);
return;
}
if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isEntryScheduled()) {
// Update display layout and bounds handler if we aren't in PIP or haven't actually
// entered PIP yet.
onDisplayRotationChangedNotInPip(mContext, toRotation);
// do not forget to update the movement bounds as well.
updateMovementBounds(mPipBoundsState.getNormalBounds(), true /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t);
mPipTaskOrganizer.onDisplayRotationSkipped();
return;
}
// If there is an animation running (ie. from a shelf offset), then ensure that we calculate
// the bounds for the next orientation using the destination bounds of the animation
// TODO: Technically this should account for movement animation bounds as well
Rect currentBounds = mPipTaskOrganizer.getCurrentOrAnimatingBounds();
final Rect outBounds = new Rect();
final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds,
mTmpInsetBounds, displayId, fromRotation, toRotation, t);
if (changed) {
// If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the
// movement bounds
mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(),
mTmpInsetBounds);
// The bounds are being applied to a specific snap fraction, so reset any known offsets
// for the previous orientation before updating the movement bounds.
// We perform the resets if and only if this callback is due to screen rotation but
// not during the fixed rotation. In fixed rotation case, app is about to enter PiP
// and we need the offsets preserved to calculate the destination bounds.
if (!mIsInFixedRotation) {
// Update the shelf visibility without updating the movement bounds. We're already
// updating them below with the |fromRotation| flag set, which is more accurate
// than using the |fromShelfAdjustment|.
mPipBoundsState.setShelfVisibility(false /* showing */, 0 /* height */,
false /* updateMovementBounds */);
mPipBoundsState.setImeVisibility(false /* showing */, 0 /* height */);
mTouchHandler.onShelfVisibilityChanged(false, 0);
mTouchHandler.onImeVisibilityChanged(false, 0);
}
updateMovementBounds(outBounds, true /* fromRotation */, false /* fromImeAdjustment */,
false /* fromShelfAdjustment */, t);
}
};
@VisibleForTesting
final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
new DisplayController.OnDisplaysChangedListener() {
@Override
public void onFixedRotationStarted(int displayId, int newRotation) {
mIsInFixedRotation = true;
}
@Override
public void onFixedRotationFinished(int displayId) {
mIsInFixedRotation = false;
}
@Override
public void onDisplayAdded(int displayId) {
if (displayId != mPipBoundsState.getDisplayId()) {
return;
}
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
false /* saveRestoreSnapFraction */);
}
@Override
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
if (displayId != mPipBoundsState.getDisplayId()) {
return;
}
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
true /* saveRestoreSnapFraction */);
}
@Override
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) {
if (mPipBoundsState.getDisplayId() == displayId) {
mPipBoundsState.setKeepClearAreas(restricted, unrestricted);
}
}
};
/**
* Handler for messages from the PIP controller.
*/
private class PipControllerPinnedTaskListener extends
PinnedStackListenerForwarder.PinnedTaskListener {
@Override
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
mPipBoundsState.setImeVisibility(imeVisible, imeHeight);
mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
}
@Override
public void onMovementBoundsChanged(boolean fromImeAdjustment) {
updateMovementBounds(null /* toBounds */,
false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */,
null /* windowContainerTransaction */);
}
@Override
public void onActivityHidden(ComponentName componentName) {
if (componentName.equals(mPipBoundsState.getLastPipComponentName())) {
// The activity was removed, we don't want to restore to the reentry state
// saved for this component anymore.
mPipBoundsState.setLastPipComponentName(null);
}
}
}
/**
* Instantiates {@link PipController}, returns {@code null} if the feature not supported.
*/
@Nullable
public static Pip create(Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
ShellController shellController,
DisplayController displayController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipKeepClearAlgorithm pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionState pipTransitionState,
PipTouchHandler pipTouchHandler,
PipTransitionController pipTransitionController,
WindowManagerShellWrapper windowManagerShellWrapper,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
Optional<OneHandedController> oneHandedController,
ShellExecutor mainExecutor) {
if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Device doesn't support Pip feature", TAG);
return null;
}
return new PipController(context, shellInit, shellCommandHandler, shellController,
displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm,
pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController,
pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController,
windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
oneHandedController, mainExecutor)
.mImpl;
}
protected PipController(Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
ShellController shellController,
DisplayController displayController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipKeepClearAlgorithm pipKeepClearAlgorithm,
@NonNull PipBoundsState pipBoundsState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
PhonePipMenuController phonePipMenuController,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionState pipTransitionState,
PipTouchHandler pipTouchHandler,
PipTransitionController pipTransitionController,
WindowManagerShellWrapper windowManagerShellWrapper,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
Optional<OneHandedController> oneHandedController,
ShellExecutor mainExecutor
) {
// Ensure that we are the primary user's SystemUI.
final int processUser = UserManager.get(context).getProcessUserId();
if (processUser != UserHandle.USER_SYSTEM) {
throw new IllegalStateException("Non-primary Pip component not currently supported.");
}
mContext = context;
mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
mImpl = new PipImpl();
mWindowManagerShellWrapper = windowManagerShellWrapper;
mDisplayController = displayController;
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
mPipBoundsState = pipBoundsState;
mPipMotionHelper = pipMotionHelper;
mPipTaskOrganizer = pipTaskOrganizer;
mPipTransitionState = pipTransitionState;
mMainExecutor = mainExecutor;
mMediaController = pipMediaController;
mMenuController = phonePipMenuController;
mTouchHandler = pipTouchHandler;
mAppOpsListener = pipAppOpsListener;
mOneHandedController = oneHandedController;
mPipTransitionController = pipTransitionController;
mTaskStackListener = taskStackListener;
mEnterAnimationDuration = mContext.getResources()
.getInteger(R.integer.config_pipEnterAnimationDuration);
mPipParamsChangedForwarder = pipParamsChangedForwarder;
shellInit.addInitCallback(this::onInit, this);
}
private void onInit() {
mShellCommandHandler.addDumpCallback(this::dump, this);
mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
INPUT_CONSUMER_PIP, mMainExecutor);
mPipTransitionController.registerPipTransitionCallback(this);
mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> {
mPipBoundsState.setDisplayId(displayId);
onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
false /* saveRestoreSnapFraction */);
});
mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> {
if (mOnIsInPipStateChangedListener != null) {
final boolean wasInPip = PipTransitionState.isInPip(oldState);
final boolean nowInPip = PipTransitionState.isInPip(newState);
if (nowInPip != wasInPip) {
mOnIsInPipStateChangedListener.accept(nowInPip);
}
}
});
mPipBoundsState.setOnMinimalSizeChangeCallback(
() -> {
// The minimal size drives the normal bounds, so they need to be recalculated.
updateMovementBounds(null /* toBounds */, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */,
null /* wct */);
});
mPipBoundsState.setOnShelfVisibilityChangeCallback(
(isShowing, height, updateMovementBounds) -> {
mTouchHandler.onShelfVisibilityChanged(isShowing, height);
if (updateMovementBounds) {
updateMovementBounds(mPipBoundsState.getBounds(),
false /* fromRotation */, false /* fromImeAdjustment */,
true /* fromShelfAdjustment */,
null /* windowContainerTransaction */);
}
});
if (mTouchHandler != null) {
// Register the listener for input consumer touch events. Only for Phone
mPipInputConsumer.setInputListener(mTouchHandler::handleTouchEvent);
mPipInputConsumer.setRegistrationListener(mTouchHandler::onRegistrationChanged);
}
mDisplayController.addDisplayChangingController(mRotationController);
mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
// Ensure that we have the display info in case we get calls to update the bounds before the
// listener calls back
mPipBoundsState.setDisplayId(mContext.getDisplayId());
mPipBoundsState.setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay()));
try {
mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener);
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to register pinned stack listener, %s", TAG, e);
}
try {
ActivityTaskManager.RootTaskInfo taskInfo = ActivityTaskManager.getService()
.getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
if (taskInfo != null) {
// If SystemUI restart, and it already existed a pinned stack,
// register the pip input consumer to ensure touch can send to it.
mPipInputConsumer.registerInputConsumer();
}
} catch (RemoteException | UnsupportedOperationException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to register pinned stack listener, %s", TAG, e);
e.printStackTrace();
}
// Handle for system task stack changes.
mTaskStackListener.addListener(
new TaskStackListenerCallback() {
@Override
public void onActivityPinned(String packageName, int userId, int taskId,
int stackId) {
mTouchHandler.onActivityPinned();
mMediaController.onActivityPinned();
mAppOpsListener.onActivityPinned(packageName);
mPipInputConsumer.registerInputConsumer();
}
@Override
public void onActivityUnpinned() {
final Pair<ComponentName, Integer> topPipActivityInfo =
PipUtils.getTopPipActivity(mContext);
final ComponentName topActivity = topPipActivityInfo.first;
mTouchHandler.onActivityUnpinned(topActivity);
mAppOpsListener.onActivityUnpinned();
mPipInputConsumer.unregisterInputConsumer();
}
@Override
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
return;
}
mTouchHandler.getMotionHelper().expandLeavePip(
clearedTask /* skipAnimation */);
}
});
mPipParamsChangedForwarder.addListener(
new PipParamsChangedForwarder.PipParamsChangedCallback() {
@Override
public void onAspectRatioChanged(float ratio) {
mPipBoundsState.setAspectRatio(ratio);
final Rect destinationBounds =
mPipBoundsAlgorithm.getAdjustedDestinationBounds(
mPipBoundsState.getBounds(),
mPipBoundsState.getAspectRatio());
Objects.requireNonNull(destinationBounds, "Missing destination bounds");
if (!destinationBounds.equals(mPipBoundsState.getBounds())) {
mPipTaskOrganizer.scheduleAnimateResizePip(destinationBounds,
mEnterAnimationDuration,
null /* updateBoundsCallback */);
mTouchHandler.onAspectRatioChanged();
updateMovementBounds(null /* toBounds */, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */,
null /* windowContainerTransaction */);
}
}
@Override
public void onActionsChanged(List<RemoteAction> actions,
RemoteAction closeAction) {
mMenuController.setAppActions(actions, closeAction);
}
});
mOneHandedController.ifPresent(controller -> {
controller.asOneHanded().registerTransitionCallback(
new OneHandedTransitionCallback() {
@Override
public void onStartFinished(Rect bounds) {
mTouchHandler.setOhmOffset(bounds.top);
}
@Override
public void onStopFinished(Rect bounds) {
mTouchHandler.setOhmOffset(bounds.top);
}
});
});
mShellController.addConfigurationChangeListener(this);
mShellController.addKeyguardChangeListener(this);
}
@Override
public Context getContext() {
return mContext;
}
@Override
public ShellExecutor getRemoteCallExecutor() {
return mMainExecutor;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
mTouchHandler.onConfigurationChanged();
mPipBoundsState.onConfigurationChanged();
}
@Override
public void onDensityOrFontScaleChanged() {
mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext);
onPipResourceDimensionsChanged();
}
@Override
public void onThemeChanged() {
mTouchHandler.onOverlayChanged();
onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()),
false /* saveRestoreSnapFraction */);
}
private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) {
if (mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) {
return;
}
Runnable updateDisplayLayout = () -> {
final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
&& mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
mPipBoundsState.setDisplayLayout(layout);
final WindowContainerTransaction wct =
fromRotation ? new WindowContainerTransaction() : null;
updateMovementBounds(null /* toBounds */,
fromRotation, false /* fromImeAdjustment */,
false /* fromShelfAdjustment */,
wct /* windowContainerTransaction */);
if (wct != null) {
mPipTaskOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_SAME,
false /* wasPipTopLeft */);
}
};
if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) {
mMenuController.attachPipMenuView();
// Calculate the snap fraction of the current stack along the old movement bounds
final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm();
final Rect postChangeBounds = new Rect(mPipBoundsState.getBounds());
final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeBounds,
mPipBoundsAlgorithm.getMovementBounds(postChangeBounds),
mPipBoundsState.getStashedState());
// Scale PiP on density dpi change, so it appears to be the same size physically.
final boolean densityDpiChanged = mPipBoundsState.getDisplayLayout().densityDpi() != 0
&& (mPipBoundsState.getDisplayLayout().densityDpi() != layout.densityDpi());
if (densityDpiChanged) {
final float scale = (float) layout.densityDpi()
/ mPipBoundsState.getDisplayLayout().densityDpi();
postChangeBounds.set(0, 0,
(int) (postChangeBounds.width() * scale),
(int) (postChangeBounds.height() * scale));
}
updateDisplayLayout.run();
// Calculate the PiP bounds in the new orientation based on same fraction along the
// rotated movement bounds.
final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds(
postChangeBounds, false /* adjustForIme */);
pipSnapAlgorithm.applySnapFraction(postChangeBounds, postChangeMovementBounds,
snapFraction, mPipBoundsState.getStashedState(),
mPipBoundsState.getStashOffset(),
mPipBoundsState.getDisplayBounds(),
mPipBoundsState.getDisplayLayout().stableInsets());
if (densityDpiChanged) {
// Using PipMotionHelper#movePip directly here may cause race condition since
// the app content in PiP mode may or may not be updated for the new density dpi.
final int duration = mContext.getResources().getInteger(
R.integer.config_pipEnterAnimationDuration);
mPipTaskOrganizer.scheduleAnimateResizePip(
postChangeBounds, duration, null /* updateBoundsCallback */);
} else {
mTouchHandler.getMotionHelper().movePip(postChangeBounds);
}
} else {
updateDisplayLayout.run();
}
}
private void registerSessionListenerForCurrentUser() {
mMediaController.registerSessionListenerForCurrentUser();
}
private void onSystemUiStateChanged(boolean isValidState, int flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
/**
* Expands the PIP.
*/
public void expandPip() {
mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */);
}
/**
* Hides the PIP menu.
*/
public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {
mMenuController.hideMenu(onStartCallback, onEndCallback);
}
/**
* Sent from KEYCODE_WINDOW handler in PhoneWindowManager, to request the menu to be shown.
*/
public void showPictureInPictureMenu() {
mTouchHandler.showPictureInPictureMenu();
}
/**
* If {@param keyguardShowing} is {@code false} and {@param animating} is {@code true},
* we would wait till the dismissing animation of keyguard and surfaces behind to be
* finished first to reset the visibility of PiP window.
* See also {@link #onKeyguardDismissAnimationFinished()}
*/
@Override
public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
boolean animatingDismiss) {
if (!mPipTaskOrganizer.isInPip()) {
return;
}
if (visible) {
mIsKeyguardShowingOrAnimating = true;
hidePipMenu(null /* onStartCallback */, null /* onEndCallback */);
mPipTaskOrganizer.setPipVisibility(false);
} else if (!animatingDismiss) {
mIsKeyguardShowingOrAnimating = false;
mPipTaskOrganizer.setPipVisibility(true);
}
}
@Override
public void onKeyguardDismissAnimationFinished() {
if (mPipTaskOrganizer.isInPip()) {
mIsKeyguardShowingOrAnimating = false;
mPipTaskOrganizer.setPipVisibility(true);
}
}
/**
* Sets a customized touch gesture that replaces the default one.
*/
public void setTouchGesture(PipTouchGesture gesture) {
mTouchHandler.setTouchGesture(gesture);
}
/**
* Sets both shelf visibility and its height.
*/
private void setShelfHeight(boolean visible, int height) {
if (!mIsKeyguardShowingOrAnimating) {
setShelfHeightLocked(visible, height);
}
}
private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
mOnIsInPipStateChangedListener = callback;
if (mOnIsInPipStateChangedListener != null) {
callback.accept(mPipTransitionState.isInPip());
}
}
private void setShelfHeightLocked(boolean visible, int height) {
final int shelfHeight = visible ? height : 0;
mPipBoundsState.setShelfVisibility(visible, shelfHeight);
}
private void setPinnedStackAnimationType(int animationType) {
mPipTaskOrganizer.setOneShotAnimationType(animationType);
mPipTransitionController.setIsFullAnimation(
animationType == PipAnimationController.ANIM_TYPE_BOUNDS);
}
private void setPinnedStackAnimationListener(PipAnimationListener callback) {
mPinnedStackAnimationRecentsCallback = callback;
onPipResourceDimensionsChanged();
}
private void onPipResourceDimensionsChanged() {
if (mPinnedStackAnimationRecentsCallback != null) {
mPinnedStackAnimationRecentsCallback.onPipResourceDimensionsChanged(
mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius),
mContext.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius));
}
}
private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams,
int launcherRotation, int shelfHeight) {
setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight);
onDisplayRotationChangedNotInPip(mContext, launcherRotation);
final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
pictureInPictureParams);
// sync mPipBoundsState with the newly calculated bounds.
mPipBoundsState.setNormalBounds(entryBounds);
return entryBounds;
}
private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
SurfaceControl overlay) {
mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay);
}
private String getTransitionTag(int direction) {
switch (direction) {
case TRANSITION_DIRECTION_TO_PIP:
return "TRANSITION_TO_PIP";
case TRANSITION_DIRECTION_LEAVE_PIP:
return "TRANSITION_LEAVE_PIP";
case TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN:
return "TRANSITION_LEAVE_PIP_TO_SPLIT_SCREEN";
case TRANSITION_DIRECTION_REMOVE_STACK:
return "TRANSITION_REMOVE_STACK";
case TRANSITION_DIRECTION_SNAP_AFTER_RESIZE:
return "TRANSITION_SNAP_AFTER_RESIZE";
case TRANSITION_DIRECTION_USER_RESIZE:
return "TRANSITION_USER_RESIZE";
case TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND:
return "TRANSITION_EXPAND_OR_UNEXPAND";
default:
return "TRANSITION_LEAVE_UNKNOWN";
}
}
@Override
public void onPipTransitionStarted(int direction, Rect pipBounds) {
// Begin InteractionJankMonitor with PIP transition CUJs
final InteractionJankMonitor.Configuration.Builder builder =
InteractionJankMonitor.Configuration.Builder.withSurface(
CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl())
.setTag(getTransitionTag(direction))
.setTimeout(2000);
InteractionJankMonitor.getInstance().begin(builder);
if (isOutPipDirection(direction)) {
// Exiting PIP, save the reentry state to restore to when re-entering.
saveReentryState(pipBounds);
}
// Disable touches while the animation is running
mTouchHandler.setTouchEnabled(false);
if (mPinnedStackAnimationRecentsCallback != null) {
mPinnedStackAnimationRecentsCallback.onPipAnimationStarted();
if (direction == TRANSITION_DIRECTION_LEAVE_PIP) {
mPinnedStackAnimationRecentsCallback.onExpandPip();
}
}
}
/** Save the state to restore to on re-entry. */
public void saveReentryState(Rect pipBounds) {
float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds);
if (mPipBoundsState.hasUserResizedPip()) {
final Rect reentryBounds = mTouchHandler.getUserResizeBounds();
final Size reentrySize = new Size(reentryBounds.width(), reentryBounds.height());
mPipBoundsState.saveReentryState(reentrySize, snapFraction);
} else {
mPipBoundsState.saveReentryState(null /* bounds */, snapFraction);
}
}
@Override
public void onPipTransitionFinished(int direction) {
onPipTransitionFinishedOrCanceled(direction);
}
@Override
public void onPipTransitionCanceled(int direction) {
onPipTransitionFinishedOrCanceled(direction);
}
private void onPipTransitionFinishedOrCanceled(int direction) {
// End InteractionJankMonitor with PIP transition by CUJs
InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION);
// Re-enable touches after the animation completes
mTouchHandler.setTouchEnabled(true);
mTouchHandler.onPinnedStackAnimationEnded(direction);
}
private void updateMovementBounds(@Nullable Rect toBounds, boolean fromRotation,
boolean fromImeAdjustment, boolean fromShelfAdjustment,
WindowContainerTransaction wct) {
// Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before
// passing to mTouchHandler/mPipTaskOrganizer
final Rect outBounds = new Rect(toBounds);
final int rotation = mPipBoundsState.getDisplayLayout().rotation();
mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds);
mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds());
if (outBounds.isEmpty()) {
outBounds.set(mPipBoundsAlgorithm.getDefaultBounds());
}
// mTouchHandler would rely on the bounds populated from mPipTaskOrganizer
mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment,
fromShelfAdjustment, wct);
mPipTaskOrganizer.finishResizeForMenu(outBounds);
mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mPipBoundsState.getNormalBounds(),
outBounds, fromImeAdjustment, fromShelfAdjustment, rotation);
}
/**
* Updates the display info and display layout on rotation change. This is needed even when we
* aren't in PIP because the rotation layout is used to calculate the proper insets for the
* next enter animation into PIP.
*/
private void onDisplayRotationChangedNotInPip(Context context, int toRotation) {
// Update the display layout, note that we have to do this on every rotation even if we
// aren't in PIP since we need to update the display layout to get the right resources
mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation);
}
/**
* Updates the display info, calculating and returning the new stack and movement bounds in the
* new orientation of the device if necessary.
*
* @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise.
*/
private boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds,
Rect outInsetBounds,
int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) {
// Bail early if the event is not sent to current display
if ((displayId != mPipBoundsState.getDisplayId()) || (fromRotation == toRotation)) {
return false;
}
// Bail early if the pinned task is staled.
final ActivityTaskManager.RootTaskInfo pinnedTaskInfo;
try {
pinnedTaskInfo = ActivityTaskManager.getService()
.getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
if (pinnedTaskInfo == null) return false;
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to get RootTaskInfo for pinned task, %s", TAG, e);
return false;
}
final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm();
// Calculate the snap fraction of the current stack along the old movement bounds
final Rect postChangeStackBounds = new Rect(oldBounds);
final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds,
mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds),
mPipBoundsState.getStashedState());
// Update the display layout
mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation);
// Calculate the stack bounds in the new orientation based on same fraction along the
// rotated movement bounds.
final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds(
postChangeStackBounds, false /* adjustForIme */);
pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(),
mPipBoundsState.getDisplayBounds(),
mPipBoundsState.getDisplayLayout().stableInsets());
mPipBoundsAlgorithm.getInsetBounds(outInsetBounds);
outBounds.set(postChangeStackBounds);
t.setBounds(pinnedTaskInfo.token, outBounds);
return true;
}
private void dump(PrintWriter pw, String prefix) {
final String innerPrefix = " ";
pw.println(TAG);
mMenuController.dump(pw, innerPrefix);
mTouchHandler.dump(pw, innerPrefix);
mPipBoundsAlgorithm.dump(pw, innerPrefix);
mPipTaskOrganizer.dump(pw, innerPrefix);
mPipBoundsState.dump(pw, innerPrefix);
mPipInputConsumer.dump(pw, innerPrefix);
}
/**
* The interface for calls from outside the Shell, within the host process.
*/
private class PipImpl implements Pip {
private IPipImpl mIPip;
@Override
public IPip createExternalInterface() {
if (mIPip != null) {
mIPip.invalidate();
}
mIPip = new IPipImpl(PipController.this);
return mIPip;
}
@Override
public void expandPip() {
mMainExecutor.execute(() -> {
PipController.this.expandPip();
});
}
@Override
public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
mMainExecutor.execute(() -> {
PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag);
});
}
@Override
public void registerSessionListenerForCurrentUser() {
mMainExecutor.execute(() -> {
PipController.this.registerSessionListenerForCurrentUser();
});
}
@Override
public void setShelfHeight(boolean visible, int height) {
mMainExecutor.execute(() -> {
PipController.this.setShelfHeight(visible, height);
});
}
@Override
public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
mMainExecutor.execute(() -> {
PipController.this.setOnIsInPipStateChangedListener(callback);
});
}
@Override
public void setPinnedStackAnimationType(int animationType) {
mMainExecutor.execute(() -> {
PipController.this.setPinnedStackAnimationType(animationType);
});
}
@Override
public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) {
mMainExecutor.execute(() -> {
mPipBoundsState.addPipExclusionBoundsChangeCallback(listener);
});
}
@Override
public void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) {
mMainExecutor.execute(() -> {
mPipBoundsState.removePipExclusionBoundsChangeCallback(listener);
});
}
@Override
public void showPictureInPictureMenu() {
mMainExecutor.execute(() -> {
PipController.this.showPictureInPictureMenu();
});
}
}
/**
* The interface for calls from outside the host process.
*/
@BinderThread
private static class IPipImpl extends IPip.Stub {
private PipController mController;
private final SingleInstanceRemoteListener<PipController,
IPipAnimationListener> mListener;
private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() {
@Override
public void onPipAnimationStarted() {
mListener.call(l -> l.onPipAnimationStarted());
}
@Override
public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) {
mListener.call(l -> l.onPipResourceDimensionsChanged(cornerRadius, shadowRadius));
}
@Override
public void onExpandPip() {
mListener.call(l -> l.onExpandPip());
}
};
IPipImpl(PipController controller) {
mController = controller;
mListener = new SingleInstanceRemoteListener<>(mController,
c -> c.setPinnedStackAnimationListener(mPipAnimationListener),
c -> c.setPinnedStackAnimationListener(null));
}
/**
* Invalidates this instance, preventing future calls from updating the controller.
*/
void invalidate() {
mController = null;
}
@Override
public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams, int launcherRotation,
int shelfHeight) {
Rect[] result = new Rect[1];
executeRemoteCallWithTaskPermission(mController, "startSwipePipToHome",
(controller) -> {
result[0] = controller.startSwipePipToHome(componentName, activityInfo,
pictureInPictureParams, launcherRotation, shelfHeight);
}, true /* blocking */);
return result[0];
}
@Override
public void stopSwipePipToHome(int taskId, ComponentName componentName,
Rect destinationBounds, SurfaceControl overlay) {
executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
(controller) -> {
controller.stopSwipePipToHome(taskId, componentName, destinationBounds,
overlay);
});
}
@Override
public void setShelfHeight(boolean visible, int height) {
executeRemoteCallWithTaskPermission(mController, "setShelfHeight",
(controller) -> {
controller.setShelfHeight(visible, height);
});
}
@Override
public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
(controller) -> {
if (listener != null) {
mListener.register(listener);
} else {
mListener.unregister();
}
});
}
}
}