blob: fa48def9c7d735255cc48ac39d1d346740ded6d4 [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.tv;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.TaskInfo;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.RemoteException;
import android.view.Gravity;
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.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
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.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Manages the picture-in-picture (PIP) UI and states.
*/
public class TvPipController implements PipTransitionController.PipTransitionCallback,
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener {
private static final String TAG = "TvPipController";
static final boolean DEBUG = false;
private static final int NONEXISTENT_TASK_ID = -1;
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"STATE_"}, value = {
STATE_NO_PIP,
STATE_PIP,
STATE_PIP_MENU,
})
public @interface State {}
/**
* State when there is no applications in Pip.
*/
private static final int STATE_NO_PIP = 0;
/**
* State when there is an applications in Pip and the Pip window located at its "normal" place
* (usually the bottom right corner).
*/
private static final int STATE_PIP = 1;
/**
* State when there is an applications in Pip and the Pip menu is open. In this state Pip window
* is usually moved from its "normal" position on the screen to the "menu" position - which is
* often at the middle of the screen, and gets slightly scaled up.
*/
private static final int STATE_PIP_MENU = 2;
private final Context mContext;
private final TvPipBoundsState mTvPipBoundsState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
private final PipAppOpsListener mAppOpsListener;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController;
private final TvPipNotificationController mPipNotificationController;
private final TvPipMenuController mTvPipMenuController;
private final ShellExecutor mMainExecutor;
private final TvPipImpl mImpl = new TvPipImpl();
private @State int mState = STATE_NO_PIP;
private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
private RemoteAction mCloseAction;
// How long the shell will wait for the app to close the PiP if a custom action is set.
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
private int mEduTextWindowExitAnimationDurationMs;
public static Pip create(
Context context,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
TvPipMenuController tvPipMenuController,
PipMediaController pipMediaController,
TvPipNotificationController pipNotificationController,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper wmShell,
ShellExecutor mainExecutor) {
return new TvPipController(
context,
tvPipBoundsState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
pipAppOpsListener,
pipTaskOrganizer,
pipTransitionController,
tvPipMenuController,
pipMediaController,
pipNotificationController,
taskStackListener,
pipParamsChangedForwarder,
displayController,
wmShell,
mainExecutor).mImpl;
}
private TvPipController(
Context context,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
TvPipMenuController tvPipMenuController,
PipMediaController pipMediaController,
TvPipNotificationController pipNotificationController,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper wmShell,
ShellExecutor mainExecutor) {
mContext = context;
mMainExecutor = mainExecutor;
mTvPipBoundsState = tvPipBoundsState;
mTvPipBoundsState.setDisplayId(context.getDisplayId());
mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
mTvPipBoundsController = tvPipBoundsController;
mTvPipBoundsController.setListener(this);
mPipMediaController = pipMediaController;
mPipNotificationController = pipNotificationController;
mPipNotificationController.setDelegate(this);
mTvPipMenuController = tvPipMenuController;
mTvPipMenuController.setDelegate(this);
mAppOpsListener = pipAppOpsListener;
mPipTaskOrganizer = pipTaskOrganizer;
pipTransitionController.registerPipTransitionCallback(this);
loadConfigurations();
registerPipParamsChangedListener(pipParamsChangedForwarder);
registerTaskStackListenerCallback(taskStackListener);
registerWmShellPinnedStackListener(wmShell);
displayController.addDisplayWindowListener(this);
}
private void onConfigurationChanged(Configuration newConfig) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
}
if (isPipShown()) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: > closing Pip.", TAG);
}
closePip();
}
loadConfigurations();
mPipNotificationController.onConfigurationChanged(mContext);
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
}
/**
* Returns {@code true} if Pip is shown.
*/
private boolean isPipShown() {
return mState != STATE_NO_PIP;
}
/**
* Starts the process if bringing up the Pip menu if by issuing a command to move Pip
* task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
* task/window is properly positioned in {@link #onPipTransitionFinished(int)}.
*/
@Override
public void showPictureInPictureMenu() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState));
}
if (mState == STATE_NO_PIP) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: > cannot open Menu from the current state.", TAG);
}
return;
}
setState(STATE_PIP_MENU);
mTvPipMenuController.showMenu();
updatePinnedStackBounds();
}
@Override
public void onMenuClosed() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: closeMenu(), state before=%s", TAG, stateToName(mState));
}
setState(STATE_PIP);
updatePinnedStackBounds();
}
@Override
public void onInMoveModeChanged() {
updatePinnedStackBounds();
}
/**
* Opens the "Pip-ed" Activity fullscreen.
*/
@Override
public void movePipToFullscreen() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
}
mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */);
onPipDisappeared();
}
@Override
public void togglePipExpansion() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: togglePipExpansion()", TAG);
}
boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
int saveGravity = mTvPipBoundsAlgorithm
.updateGravityOnExpandToggled(mPreviousGravity, expanding);
if (saveGravity != Gravity.NO_GRAVITY) {
mPreviousGravity = saveGravity;
}
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
mTvPipBoundsState.setTvPipExpanded(expanding);
mPipNotificationController.updateExpansionState();
updatePinnedStackBounds();
}
@Override
public void enterPipMovementMenu() {
setState(STATE_PIP_MENU);
mTvPipMenuController.showMovementMenuOnly();
}
@Override
public void movePip(int keycode) {
if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
mPreviousGravity = Gravity.NO_GRAVITY;
updatePinnedStackBounds();
} else {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Position hasn't changed", TAG);
}
}
}
@Override
public int getPipGravity() {
return mTvPipBoundsState.getTvPipGravity();
}
public int getOrientation() {
return mTvPipBoundsState.getTvFixedPipOrientation();
}
@Override
public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
Set<Rect> unrestricted) {
if (mTvPipBoundsState.getDisplayId() == displayId) {
boolean unrestrictedAreasChanged = !Objects.equals(unrestricted,
mTvPipBoundsState.getUnrestrictedKeepClearAreas());
mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged);
}
}
private void updatePinnedStackBounds() {
updatePinnedStackBounds(mResizeAnimationDuration, true);
}
/**
* Update the PiP bounds based on the state of the PiP and keep clear areas.
*/
private void updatePinnedStackBounds(int animationDuration, boolean immediate) {
if (mState == STATE_NO_PIP) {
return;
}
final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode();
final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition;
mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing,
animationDuration, immediate);
}
@Override
public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
animationDuration, rect -> mTvPipMenuController.updateExpansionState());
mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
}
/**
* Closes Pip window.
*/
@Override
public void closePip() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState),
mCloseAction);
}
if (mCloseAction != null) {
try {
mCloseAction.getActionIntent().send();
} catch (PendingIntent.CanceledException e) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to send close action, %s", TAG, e);
}
mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
} else {
closeCurrentPiP(mPinnedTaskId);
}
}
private void closeCurrentPiP(int pinnedTaskId) {
if (mPinnedTaskId != pinnedTaskId) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: PiP has already been closed by custom close action", TAG);
return;
}
removeTask(mPinnedTaskId);
onPipDisappeared();
}
@Override
public void closeEduText() {
updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
}
private void registerSessionListenerForCurrentUser() {
mPipMediaController.registerSessionListenerForCurrentUser();
}
private void checkIfPinnedTaskAppeared() {
final TaskInfo pinnedTask = getPinnedTaskInfo();
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: checkIfPinnedTaskAppeared(), task=%s", TAG, pinnedTask);
}
if (pinnedTask == null || pinnedTask.topActivity == null) return;
mPinnedTaskId = pinnedTask.taskId;
mPipMediaController.onActivityPinned();
mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
}
private void checkIfPinnedTaskIsGone() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onTaskStackChanged()", TAG);
}
if (isPipShown() && getPinnedTaskInfo() == null) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Pinned task is gone.", TAG);
onPipDisappeared();
}
}
private void onPipDisappeared() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipDisappeared() state=%s", TAG, stateToName(mState));
}
mPipNotificationController.dismiss();
mTvPipMenuController.closeMenu();
mTvPipBoundsState.resetTvPipState();
mTvPipBoundsController.onPipDismissed();
setState(STATE_NO_PIP);
mPinnedTaskId = NONEXISTENT_TASK_ID;
}
@Override
public void onPipTransitionStarted(int direction, Rect pipBounds) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
}
mTvPipMenuController.notifyPipAnimating(true);
}
@Override
public void onPipTransitionCanceled(int direction) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
}
mTvPipMenuController.notifyPipAnimating(false);
}
@Override
public void onPipTransitionFinished(int direction) {
if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
setState(STATE_PIP);
}
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
}
mTvPipMenuController.notifyPipAnimating(false);
}
private void setState(@State int state) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: setState(), state=%s, prev=%s",
TAG, stateToName(state), stateToName(mState));
}
mState = state;
}
private void loadConfigurations() {
final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
mEduTextWindowExitAnimationDurationMs =
res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
}
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
taskStackListener.addListener(new TaskStackListenerCallback() {
@Override
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
checkIfPinnedTaskAppeared();
mAppOpsListener.onActivityPinned(packageName);
}
@Override
public void onActivityUnpinned() {
mAppOpsListener.onActivityUnpinned();
}
@Override
public void onTaskStackChanged() {
checkIfPinnedTaskIsGone();
}
@Override
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
if (task.getWindowingMode() == WINDOWING_MODE_PINNED) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPinnedActivityRestartAttempt()", TAG);
}
// If the "Pip-ed" Activity is launched again by Launcher or intent, make it
// fullscreen.
movePipToFullscreen();
}
}
});
}
private void registerPipParamsChangedListener(PipParamsChangedForwarder provider) {
provider.addListener(new PipParamsChangedForwarder.PipParamsChangedCallback() {
@Override
public void onActionsChanged(List<RemoteAction> actions,
RemoteAction closeAction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onActionsChanged()", TAG);
mTvPipMenuController.setAppActions(actions, closeAction);
mCloseAction = closeAction;
}
@Override
public void onAspectRatioChanged(float ratio) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onAspectRatioChanged: %f", TAG, ratio);
mTvPipBoundsState.setAspectRatio(ratio);
if (!mTvPipBoundsState.isTvPipExpanded()) {
updatePinnedStackBounds();
}
}
@Override
public void onExpandedAspectRatioChanged(float ratio) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onExpandedAspectRatioChanged: %f", TAG, ratio);
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false);
mTvPipMenuController.updateExpansionState();
// 1) PiP is expanded and only aspect ratio changed, but wasn't disabled
// --> update bounds, but don't toggle
if (mTvPipBoundsState.isTvPipExpanded() && ratio != 0) {
mTvPipBoundsAlgorithm.updateExpandedPipSize();
updatePinnedStackBounds();
}
// 2) PiP is expanded, but expanded PiP was disabled
// --> collapse PiP
if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) {
int saveGravity = mTvPipBoundsAlgorithm
.updateGravityOnExpandToggled(mPreviousGravity, false);
if (saveGravity != Gravity.NO_GRAVITY) {
mPreviousGravity = saveGravity;
}
mTvPipBoundsState.setTvPipExpanded(false);
updatePinnedStackBounds();
}
// 3) PiP not expanded and not manually collapsed and expand was enabled
// --> expand to new ratio
if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0
&& !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
mTvPipBoundsAlgorithm.updateExpandedPipSize();
int saveGravity = mTvPipBoundsAlgorithm
.updateGravityOnExpandToggled(mPreviousGravity, true);
if (saveGravity != Gravity.NO_GRAVITY) {
mPreviousGravity = saveGravity;
}
mTvPipBoundsState.setTvPipExpanded(true);
updatePinnedStackBounds();
}
}
});
}
private void registerWmShellPinnedStackListener(WindowManagerShellWrapper wmShell) {
try {
wmShell.addPinnedStackListener(new PinnedStackListenerForwarder.PinnedTaskListener() {
@Override
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onImeVisibilityChanged(), visible=%b, height=%d",
TAG, imeVisible, imeHeight);
}
if (imeVisible == mTvPipBoundsState.isImeShowing()
&& (!imeVisible || imeHeight == mTvPipBoundsState.getImeHeight())) {
// Nothing changed: either IME has been and remains invisible, or remains
// visible with the same height.
return;
}
mTvPipBoundsState.setImeVisibility(imeVisible, imeHeight);
if (mState != STATE_NO_PIP) {
updatePinnedStackBounds();
}
}
});
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to register pinned stack listener, %s", TAG, e);
}
}
private static TaskInfo getPinnedTaskInfo() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: getPinnedTaskInfo()", TAG);
}
try {
final TaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo(
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: taskInfo=%s", TAG, taskInfo);
}
return taskInfo;
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: getRootTaskInfo() failed, %s", TAG, e);
return null;
}
}
private static void removeTask(int taskId) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: removeTask(), taskId=%d", TAG, taskId);
}
try {
ActivityTaskManager.getService().removeTask(taskId);
} catch (Exception e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Atm.removeTask() failed, %s", TAG, e);
}
}
private static String stateToName(@State int state) {
switch (state) {
case STATE_NO_PIP:
return "NO_PIP";
case STATE_PIP:
return "PIP";
case STATE_PIP_MENU:
return "PIP_MENU";
default:
// This can't happen.
throw new IllegalArgumentException("Unknown state " + state);
}
}
private class TvPipImpl implements Pip {
@Override
public void onConfigurationChanged(Configuration newConfig) {
mMainExecutor.execute(() -> {
TvPipController.this.onConfigurationChanged(newConfig);
});
}
@Override
public void registerSessionListenerForCurrentUser() {
mMainExecutor.execute(() -> {
TvPipController.this.registerSessionListenerForCurrentUser();
});
}
}
}