blob: 49129b77b4a501af0c6359e7f933cb9b6bf93a3a [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.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ParceledListSlice;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
import android.view.DisplayInfo;
import android.view.IPinnedStackController;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.ShellExecutor;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipBoundsHandler;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipTaskOrganizer;
import java.io.PrintWriter;
import java.util.function.Consumer;
/**
* Manages the picture-in-picture (PIP) UI and states for Phones.
*/
public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback {
private static final String TAG = "PipController";
private Context mContext;
private ShellExecutor mMainExecutor;
private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
private final Rect mTmpInsetBounds = new Rect();
private final Rect mTmpNormalBounds = new Rect();
protected final Rect mReentryBounds = new Rect();
private DisplayController mDisplayController;
private PipAppOpsListener mAppOpsListener;
private PipBoundsHandler mPipBoundsHandler;
private @NonNull PipBoundsState mPipBoundsState;
private PipMediaController mMediaController;
private PipTouchHandler mTouchHandler;
private Consumer<Boolean> mPinnedStackAnimationRecentsCallback;
private WindowManagerShellWrapper mWindowManagerShellWrapper;
private boolean mIsInFixedRotation;
protected PipMenuActivityController mMenuController;
protected PipTaskOrganizer mPipTaskOrganizer;
protected PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener =
new PipControllerPinnedStackListener();
/**
* Handler for display rotation changes.
*/
private final DisplayChangeController.OnDisplayChangingListener mRotationController = (
int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> {
if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isDeferringEnterPipAnimation()) {
// Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update
// the display layout in the bounds handler in this case.
mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, toRotation);
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 boolean changed = mPipBoundsHandler.onDisplayRotationChanged(mContext,
mTmpNormalBounds, 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(mTmpNormalBounds,
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) {
mPipBoundsHandler.setShelfHeight(false, 0);
mPipBoundsHandler.onImeVisibilityChanged(false, 0);
mTouchHandler.onShelfVisibilityChanged(false, 0);
mTouchHandler.onImeVisibilityChanged(false, 0);
}
updateMovementBounds(mTmpNormalBounds, true /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t);
}
};
private DisplayController.OnDisplaysChangedListener mFixedRotationListener =
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) {
mPipBoundsState.setDisplayLayout(
mDisplayController.getDisplayLayout(displayId));
}
};
/**
* Handler for messages from the PIP controller.
*/
private class PipControllerPinnedStackListener extends
PinnedStackListenerForwarder.PinnedStackListener {
@Override
public void onListenerRegistered(IPinnedStackController controller) {
mMainExecutor.execute(() -> mTouchHandler.setPinnedStackController(controller));
}
@Override
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
mMainExecutor.execute(() -> {
mPipBoundsHandler.onImeVisibilityChanged(imeVisible, imeHeight);
mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
});
}
@Override
public void onMovementBoundsChanged(boolean fromImeAdjustment) {
mMainExecutor.execute(() -> updateMovementBounds(null /* toBounds */,
false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */,
null /* windowContainerTransaction */));
}
@Override
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
mMainExecutor.execute(() -> mMenuController.setAppActions(actions));
}
@Override
public void onActivityHidden(ComponentName componentName) {
mMainExecutor.execute(() -> {
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);
}
});
}
@Override
public void onDisplayInfoChanged(DisplayInfo displayInfo) {
mMainExecutor.execute(() -> mPipBoundsState.setDisplayInfo(displayInfo));
}
@Override
public void onConfigurationChanged() {
mMainExecutor.execute(() -> {
mPipBoundsHandler.onConfigurationChanged(mContext);
mTouchHandler.onConfigurationChanged();
});
}
@Override
public void onAspectRatioChanged(float aspectRatio) {
// TODO(b/169373982): Remove this callback as it is redundant with PipTaskOrg params
// change.
mMainExecutor.execute(() -> {
mPipBoundsState.setAspectRatio(aspectRatio);
mTouchHandler.onAspectRatioChanged();
});
}
}
protected PipController(Context context,
DisplayController displayController,
PipAppOpsListener pipAppOpsListener,
PipBoundsHandler pipBoundsHandler,
@NonNull PipBoundsState pipBoundsState,
PipMediaController pipMediaController,
PipMenuActivityController pipMenuActivityController,
PipTaskOrganizer pipTaskOrganizer,
PipTouchHandler pipTouchHandler,
WindowManagerShellWrapper windowManagerShellWrapper,
ShellExecutor mainExecutor
) {
// Ensure that we are the primary user's SystemUI.
final int processUser = UserManager.get(context).getUserHandle();
if (processUser != UserHandle.USER_SYSTEM) {
throw new IllegalStateException("Non-primary Pip component not currently supported.");
}
mContext = context;
mWindowManagerShellWrapper = windowManagerShellWrapper;
mDisplayController = displayController;
mPipBoundsHandler = pipBoundsHandler;
mPipBoundsState = pipBoundsState;
mPipTaskOrganizer = pipTaskOrganizer;
mMainExecutor = mainExecutor;
mPipTaskOrganizer.registerPipTransitionCallback(this);
mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> {
final DisplayInfo newDisplayInfo = new DisplayInfo();
displayController.getDisplay(displayId).getDisplayInfo(newDisplayInfo);
mPipBoundsState.setDisplayInfo(newDisplayInfo);
updateMovementBounds(null /* toBounds */, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdustment */,
null /* wct */);
});
mMediaController = pipMediaController;
mMenuController = pipMenuActivityController;
mTouchHandler = pipTouchHandler;
mAppOpsListener = pipAppOpsListener;
displayController.addDisplayChangingController(mRotationController);
displayController.addDisplayWindowListener(mFixedRotationListener);
// Ensure that we have the display info in case we get calls to update the bounds before the
// listener calls back
final DisplayInfo displayInfo = new DisplayInfo();
context.getDisplay().getDisplayInfo(displayInfo);
mPipBoundsState.setDisplayInfo(displayInfo);
try {
mWindowManagerShellWrapper.addPinnedStackListener(mPinnedStackListener);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to register pinned stack listener", e);
}
}
@Override
public void onDensityOrFontScaleChanged() {
mMainExecutor.execute(() -> {
mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext);
});
}
@Override
public void onActivityPinned(String packageName) {
mMainExecutor.execute(() -> {
mTouchHandler.onActivityPinned();
mMediaController.onActivityPinned();
mAppOpsListener.onActivityPinned(packageName);
});
}
@Override
public void onActivityUnpinned(ComponentName topActivity) {
mMainExecutor.execute(() -> {
mTouchHandler.onActivityUnpinned(topActivity);
mAppOpsListener.onActivityUnpinned();
});
}
@Override
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
boolean clearedTask) {
if (task.configuration.windowConfiguration.getWindowingMode()
!= WINDOWING_MODE_PINNED) {
return;
}
mTouchHandler.getMotionHelper().expandLeavePip(clearedTask /* skipAnimation */);
}
@Override
public void onOverlayChanged() {
mMainExecutor.execute(() -> {
mPipBoundsState.setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay()));
updateMovementBounds(null /* toBounds */,
false /* fromRotation */, false /* fromImeAdjustment */,
false /* fromShelfAdjustment */,
null /* windowContainerTransaction */);
});
}
@Override
public void registerSessionListenerForCurrentUser() {
mMediaController.registerSessionListenerForCurrentUser();
}
@Override
public void onSystemUiStateChanged(boolean isValidState, int flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
/**
* Expands the PIP.
*/
@Override
public void expandPip() {
mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */);
}
@Override
public PipTouchHandler getPipTouchHandler() {
return mTouchHandler;
}
/**
* Hides the PIP menu.
*/
@Override
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();
}
/**
* 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.
*/
@Override
public void setShelfHeight(boolean visible, int height) {
mMainExecutor.execute(() -> setShelfHeightLocked(visible, height));
}
private void setShelfHeightLocked(boolean visible, int height) {
final int shelfHeight = visible ? height : 0;
final boolean changed = mPipBoundsHandler.setShelfHeight(visible, shelfHeight);
if (changed) {
mTouchHandler.onShelfVisibilityChanged(visible, shelfHeight);
updateMovementBounds(mPipBoundsState.getBounds(),
false /* fromRotation */, false /* fromImeAdjustment */,
true /* fromShelfAdjustment */, null /* windowContainerTransaction */);
}
}
@Override
public void setPinnedStackAnimationType(int animationType) {
mMainExecutor.execute(() -> mPipTaskOrganizer.setOneShotAnimationType(animationType));
}
@Override
public void setPinnedStackAnimationListener(Consumer<Boolean> callback) {
mMainExecutor.execute(() -> mPinnedStackAnimationRecentsCallback = callback);
}
@Override
public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams,
int launcherRotation, int shelfHeight) {
setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight);
mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, launcherRotation);
return mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
pictureInPictureParams);
}
@Override
public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) {
mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds);
}
@Override
public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {
if (isOutPipDirection(direction)) {
// Exiting PIP, save the reentry bounds to restore to when re-entering.
updateReentryBounds(pipBounds);
final float snapFraction = mPipBoundsHandler.getSnapFraction(mReentryBounds);
mPipBoundsState.saveReentryState(mReentryBounds, snapFraction);
}
// Disable touches while the animation is running
mTouchHandler.setTouchEnabled(false);
if (mPinnedStackAnimationRecentsCallback != null) {
mPinnedStackAnimationRecentsCallback.accept(true);
}
}
/**
* Update the bounds used to save the re-entry size and snap fraction when exiting PIP.
*/
public void updateReentryBounds(Rect bounds) {
final Rect reentryBounds = mTouchHandler.getUserResizeBounds();
float snapFraction = mPipBoundsHandler.getSnapFraction(bounds);
mPipBoundsHandler.applySnapFraction(reentryBounds, snapFraction);
mReentryBounds.set(reentryBounds);
}
@Override
public void onPipTransitionFinished(ComponentName activity, int direction) {
onPipTransitionFinishedOrCanceled(direction);
}
@Override
public void onPipTransitionCanceled(ComponentName activity, int direction) {
onPipTransitionFinishedOrCanceled(direction);
}
private void onPipTransitionFinishedOrCanceled(int direction) {
// Re-enable touches after the animation completes
mTouchHandler.setTouchEnabled(true);
mTouchHandler.onPinnedStackAnimationEnded(direction);
mMenuController.onPinnedStackAnimationEnded();
}
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);
mTmpDisplayInfo.copyFrom(mPipBoundsState.getDisplayInfo());
mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds,
outBounds);
// mTouchHandler would rely on the bounds populated from mPipTaskOrganizer
mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment,
fromShelfAdjustment, wct);
mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds,
outBounds, fromImeAdjustment, fromShelfAdjustment,
mTmpDisplayInfo.rotation);
}
@Override
public void dump(PrintWriter pw) {
final String innerPrefix = " ";
pw.println(TAG);
mMenuController.dump(pw, innerPrefix);
mTouchHandler.dump(pw, innerPrefix);
mPipBoundsHandler.dump(pw, innerPrefix);
mPipTaskOrganizer.dump(pw, innerPrefix);
mPipBoundsState.dump(pw, innerPrefix);
}
/**
* Instantiates {@link PipController}, returns {@code null} if the feature not supported.
*/
@Nullable
public static PipController create(Context context, DisplayController displayController,
PipAppOpsListener pipAppOpsListener, PipBoundsHandler pipBoundsHandler,
PipBoundsState pipBoundsState, PipMediaController pipMediaController,
PipMenuActivityController pipMenuActivityController,
PipTaskOrganizer pipTaskOrganizer, PipTouchHandler pipTouchHandler,
WindowManagerShellWrapper windowManagerShellWrapper,
ShellExecutor mainExecutor) {
if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
Slog.w(TAG, "Device doesn't support Pip feature");
return null;
}
return new PipController(context, displayController, pipAppOpsListener, pipBoundsHandler,
pipBoundsState, pipMediaController, pipMenuActivityController,
pipTaskOrganizer, pipTouchHandler, windowManagerShellWrapper, mainExecutor);
}
}