blob: e7a1395f541cfcd6ce19df84920ad19b7a568299 [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.view.WindowManager.SHELL_ROOT_LAYER_PIP;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.RemoteAction;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Debug;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Size;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.WindowManagerGlobal;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipMediaController.ActionListener;
import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Manages the PiP menu view which can show menu options or a scrim.
*
* The current media session provides actions whenever there are no valid actions provided by the
* current PiP activity. Otherwise, those actions always take precedence.
*/
public class PhonePipMenuController implements PipMenuController {
private static final String TAG = "PhonePipMenuController";
private static final boolean DEBUG = false;
public static final int MENU_STATE_NONE = 0;
public static final int MENU_STATE_FULL = 1;
/**
* A listener interface to receive notification on changes in PIP.
*/
public interface Listener {
/**
* Called when the PIP menu visibility change has started.
*
* @param menuState the new, about-to-change state of the menu
* @param resize whether or not to resize the PiP with the state change
*/
void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback);
/**
* Called when the PIP menu state has finished changing/animating.
*
* @param menuState the new state of the menu.
*/
void onPipMenuStateChangeFinish(int menuState);
/**
* Called when the PIP requested to be expanded.
*/
void onPipExpand();
/**
* Called when the PIP requested to be dismissed.
*/
void onPipDismiss();
/**
* Called when the PIP requested to show the menu.
*/
void onPipShowMenu();
/**
* Called when the PIP requested to enter Split.
*/
void onEnterSplit();
}
private final Matrix mMoveTransform = new Matrix();
private final Rect mTmpSourceBounds = new Rect();
private final RectF mTmpSourceRectF = new RectF();
private final RectF mTmpDestinationRectF = new RectF();
private final Context mContext;
private final PipBoundsState mPipBoundsState;
private final PipMediaController mMediaController;
private final ShellExecutor mMainExecutor;
private final Handler mMainHandler;
private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
private final float[] mTmpTransform = new float[9];
private final ArrayList<Listener> mListeners = new ArrayList<>();
private final SystemWindows mSystemWindows;
private final Optional<SplitScreenController> mSplitScreenController;
private final PipUiEventLogger mPipUiEventLogger;
private List<RemoteAction> mAppActions;
private RemoteAction mCloseAction;
private List<RemoteAction> mMediaActions;
private int mMenuState;
private PipMenuView mPipMenuView;
private ActionListener mMediaActionListener = new ActionListener() {
@Override
public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
mMediaActions = new ArrayList<>(mediaActions);
updateMenuActions();
}
};
public PhonePipMenuController(Context context, PipBoundsState pipBoundsState,
PipMediaController mediaController, SystemWindows systemWindows,
Optional<SplitScreenController> splitScreenOptional,
PipUiEventLogger pipUiEventLogger,
ShellExecutor mainExecutor, Handler mainHandler) {
mContext = context;
mPipBoundsState = pipBoundsState;
mMediaController = mediaController;
mSystemWindows = systemWindows;
mMainExecutor = mainExecutor;
mMainHandler = mainHandler;
mSplitScreenController = splitScreenOptional;
mPipUiEventLogger = pipUiEventLogger;
mSurfaceControlTransactionFactory =
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
}
public boolean isMenuVisible() {
return mPipMenuView != null && mMenuState != MENU_STATE_NONE;
}
/**
* Attach the menu when the PiP task first appears.
*/
@Override
public void attach(SurfaceControl leash) {
attachPipMenuView();
}
/**
* Detach the menu when the PiP task is gone.
*/
@Override
public void detach() {
hideMenu();
detachPipMenuView();
}
void attachPipMenuView() {
// In case detach was not called (e.g. PIP unexpectedly closed)
if (mPipMenuView != null) {
detachPipMenuView();
}
mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
mSplitScreenController, mPipUiEventLogger);
mSystemWindows.addView(mPipMenuView,
getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
0, SHELL_ROOT_LAYER_PIP);
setShellRootAccessibilityWindow();
// Make sure the initial actions are set
updateMenuActions();
}
private void detachPipMenuView() {
if (mPipMenuView == null) {
return;
}
mSystemWindows.removeView(mPipMenuView);
mPipMenuView = null;
}
/**
* Updates the layout parameters of the menu.
* @param destinationBounds New Menu bounds.
*/
@Override
public void updateMenuBounds(Rect destinationBounds) {
mSystemWindows.updateViewLayout(mPipMenuView,
getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(),
destinationBounds.height()));
updateMenuLayout(destinationBounds);
}
@Override
public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (mPipMenuView != null) {
mPipMenuView.onFocusTaskChanged(taskInfo);
}
}
/**
* Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some
* reason (ie. the window isn't ready yet, thus {@link android.view.ViewRootImpl} is
* {@code null}), it will get the leash that the WindowlessWM has assigned to it.
*/
public SurfaceControl getSurfaceControl() {
return mSystemWindows.getViewSurface(mPipMenuView);
}
/**
* Adds a new menu activity listener.
*/
public void addListener(Listener listener) {
if (!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
@Nullable
Size getEstimatedMinMenuSize() {
return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize();
}
/**
* When other components requests the menu controller directly to show the menu, we must
* first fire off the request to the other listeners who will then propagate the call
* back to the controller with the right parameters.
*/
@Override
public void showMenu() {
mListeners.forEach(Listener::onPipShowMenu);
}
/**
* Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
* upon PiP window transition is finished.
*/
public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
boolean willResizeMenu, boolean showResizeHandle) {
if (willResizeMenu) {
// hide all visible controls including close button and etc. first, this is to ensure
// menu is totally invisible during the transition to eliminate unpleasant artifacts
fadeOutMenu();
}
showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle);
}
/**
* Shows the menu activity immediately.
*/
public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
boolean willResizeMenu, boolean showResizeHandle) {
showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
false /* withDelay */, showResizeHandle);
}
private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showMenu() state=%s"
+ " isMenuVisible=%s"
+ " allowMenuTimeout=%s"
+ " willResizeMenu=%s"
+ " withDelay=%s"
+ " showResizeHandle=%s"
+ " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout,
willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " "));
}
if (!checkPipMenuState()) {
return;
}
// Sync the menu bounds before showing it in case it is out of sync.
movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds,
PipMenuController.ALPHA_NO_CHANGE);
updateMenuBounds(stackBounds);
mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
showResizeHandle);
}
/**
* Move the PiP menu, which does a translation and possibly a scale transformation.
*/
@Override
public void movePipMenu(@Nullable SurfaceControl pipLeash,
@Nullable SurfaceControl.Transaction t,
Rect destinationBounds, float alpha) {
if (destinationBounds.isEmpty()) {
return;
}
if (!checkPipMenuState()) {
return;
}
// If there is no pip leash supplied, that means the PiP leash is already finalized
// resizing and the PiP menu is also resized. We then want to do a scale from the current
// new menu bounds.
if (pipLeash != null && t != null) {
mPipMenuView.getBoundsOnScreen(mTmpSourceBounds);
} else {
mTmpSourceBounds.set(0, 0, destinationBounds.width(), destinationBounds.height());
}
mTmpSourceRectF.set(mTmpSourceBounds);
mTmpDestinationRectF.set(destinationBounds);
mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
final SurfaceControl surfaceControl = getSurfaceControl();
if (surfaceControl == null) {
return;
}
final SurfaceControl.Transaction menuTx =
mSurfaceControlTransactionFactory.getTransaction();
menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
if (pipLeash != null && t != null) {
// Merge the two transactions, vsyncId has been set on menuTx.
menuTx.merge(t);
}
menuTx.apply();
}
/**
* Does an immediate window crop of the PiP menu.
*/
@Override
public void resizePipMenu(@Nullable SurfaceControl pipLeash,
@Nullable SurfaceControl.Transaction t,
Rect destinationBounds) {
if (destinationBounds.isEmpty()) {
return;
}
if (!checkPipMenuState()) {
return;
}
final SurfaceControl surfaceControl = getSurfaceControl();
if (surfaceControl == null) {
return;
}
final SurfaceControl.Transaction menuTx =
mSurfaceControlTransactionFactory.getTransaction();
menuTx.setCrop(surfaceControl, destinationBounds);
if (pipLeash != null && t != null) {
// Merge the two transactions, vsyncId has been set on menuTx.
menuTx.merge(t);
}
menuTx.apply();
}
private boolean checkPipMenuState() {
if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Not going to move PiP, either menu or its parent is not created.", TAG);
return false;
}
return true;
}
/**
* Pokes the menu, indicating that the user is interacting with it.
*/
public void pokeMenu() {
final boolean isMenuVisible = isMenuVisible();
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible);
}
if (isMenuVisible) {
mPipMenuView.pokeMenu();
}
}
private void fadeOutMenu() {
final boolean isMenuVisible = isMenuVisible();
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible);
}
if (isMenuVisible) {
mPipMenuView.fadeOutMenu();
}
}
/**
* Hides the menu view.
*/
public void hideMenu() {
final boolean isMenuVisible = isMenuVisible();
if (isMenuVisible) {
mPipMenuView.hideMenu();
}
}
/**
* Hides the menu view.
*
* @param animationType the animation type to use upon hiding the menu
* @param resize whether or not to resize the PiP with the state change
*/
public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) {
final boolean isMenuVisible = isMenuVisible();
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideMenu() state=%s"
+ " isMenuVisible=%s"
+ " animationType=%s"
+ " resize=%s"
+ " callers=\n%s", TAG, mMenuState, isMenuVisible,
animationType, resize,
Debug.getCallers(5, " "));
}
if (isMenuVisible) {
mPipMenuView.hideMenu(resize, animationType);
}
}
/**
* Hides the menu activity.
*/
public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
if (isMenuVisible()) {
// If the menu is visible in either the closed or full state, then hide the menu and
// trigger the animation trigger afterwards
if (onStartCallback != null) {
onStartCallback.run();
}
mPipMenuView.hideMenu(onEndCallback);
}
}
/**
* Sets the menu actions to the actions provided by the current PiP menu.
*/
@Override
public void setAppActions(List<RemoteAction> appActions,
RemoteAction closeAction) {
mAppActions = appActions;
mCloseAction = closeAction;
updateMenuActions();
}
void onPipExpand() {
mListeners.forEach(Listener::onPipExpand);
}
void onPipDismiss() {
mListeners.forEach(Listener::onPipDismiss);
}
void onEnterSplit() {
mListeners.forEach(Listener::onEnterSplit);
}
/**
* @return the best set of actions to show in the PiP menu.
*/
private List<RemoteAction> resolveMenuActions() {
if (isValidActions(mAppActions)) {
return mAppActions;
}
return mMediaActions;
}
/**
* Updates the PiP menu with the best set of actions provided.
*/
private void updateMenuActions() {
if (mPipMenuView != null) {
mPipMenuView.setActions(mPipBoundsState.getBounds(),
resolveMenuActions(), mCloseAction);
}
}
/**
* Returns whether the set of actions are valid.
*/
private static boolean isValidActions(List<?> actions) {
return actions != null && actions.size() > 0;
}
/**
* Handles changes in menu visibility.
*/
void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onMenuStateChangeStart() mMenuState=%s"
+ " menuState=%s resize=%s"
+ " callers=\n%s", TAG, mMenuState, menuState, resize,
Debug.getCallers(5, " "));
}
if (menuState != mMenuState) {
mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback));
if (menuState == MENU_STATE_FULL) {
// Once visible, start listening for media action changes. This call will trigger
// the menu actions to be updated again.
mMediaController.addActionListener(mMediaActionListener);
} else {
// Once hidden, stop listening for media action changes. This call will trigger
// the menu actions to be updated again.
mMediaController.removeActionListener(mMediaActionListener);
}
try {
WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
mSystemWindows.getFocusGrantToken(mPipMenuView),
menuState != MENU_STATE_NONE /* grantFocus */);
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Unable to update focus as menu appears/disappears, %s", TAG, e);
}
}
}
void onMenuStateChangeFinish(int menuState) {
if (menuState != mMenuState) {
mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState));
}
mMenuState = menuState;
setShellRootAccessibilityWindow();
}
private void setShellRootAccessibilityWindow() {
switch (mMenuState) {
case MENU_STATE_NONE:
mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null);
break;
default:
mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP,
mPipMenuView);
break;
}
}
/**
* Handles a pointer event sent from pip input consumer.
*/
void handlePointerEvent(MotionEvent ev) {
if (mPipMenuView == null) {
return;
}
if (ev.isTouchEvent()) {
mPipMenuView.dispatchTouchEvent(ev);
} else {
mPipMenuView.dispatchGenericMotionEvent(ev);
}
}
/**
* Tell the PIP Menu to recalculate its layout given its current position on the display.
*/
public void updateMenuLayout(Rect bounds) {
final boolean isMenuVisible = isMenuVisible();
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: updateMenuLayout() state=%s"
+ " isMenuVisible=%s"
+ " callers=\n%s", TAG, mMenuState, isMenuVisible,
Debug.getCallers(5, " "));
}
if (isMenuVisible) {
mPipMenuView.updateMenuLayout(bounds);
}
}
void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "mMenuState=" + mMenuState);
pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView);
pw.println(innerPrefix + "mListeners=" + mListeners.size());
}
}