blob: bcb995ae8a09073785121bcbe631dd05c8e3a053 [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.view.WindowManager.SHELL_ROOT_LAYER_PIP;
import android.app.ActivityManager;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SyncRtSurfaceTransactionApplier;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;
import androidx.annotation.Nullable;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.List;
/**
* Manages the visibility of the PiP Menu as user interacts with PiP.
*/
public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener {
private static final String TAG = "TvPipMenuController";
private static final String BACKGROUND_WINDOW_TITLE = "PipBackgroundView";
private final Context mContext;
private final SystemWindows mSystemWindows;
private final TvPipBoundsState mTvPipBoundsState;
private final Handler mMainHandler;
private final TvPipActionsProvider mTvPipActionsProvider;
private Delegate mDelegate;
private SurfaceControl mLeash;
private TvPipMenuView mPipMenuView;
private View mPipBackgroundView;
// User can actively move the PiP via the DPAD.
private boolean mInMoveMode;
// Used when only showing the move menu since we want to close the menu completely when
// exiting the move menu instead of showing the regular button menu.
private boolean mCloseAfterExitMoveMenu;
private SyncRtSurfaceTransactionApplier mApplier;
private SyncRtSurfaceTransactionApplier mBackgroundApplier;
RectF mTmpSourceRectF = new RectF();
RectF mTmpDestinationRectF = new RectF();
Matrix mMoveTransform = new Matrix();
public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
SystemWindows systemWindows, Handler mainHandler,
TvPipActionsProvider tvPipActionsProvider) {
mContext = context;
mTvPipBoundsState = tvPipBoundsState;
mSystemWindows = systemWindows;
mMainHandler = mainHandler;
mTvPipActionsProvider = tvPipActionsProvider;
// We need to "close" the menu the platform call for all the system dialogs to close (for
// example, on the Home button press).
final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
closeMenu();
}
};
context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver,
new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */,
mainHandler, Context.RECEIVER_EXPORTED);
}
void setDelegate(Delegate delegate) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: setDelegate(), delegate=%s", TAG, delegate);
if (mDelegate != null) {
throw new IllegalStateException(
"The delegate has already been set and should not change.");
}
if (delegate == null) {
throw new IllegalArgumentException("The delegate must not be null.");
}
mDelegate = delegate;
}
@Override
public void attach(SurfaceControl leash) {
if (mDelegate == null) {
throw new IllegalStateException("Delegate is not set.");
}
mLeash = leash;
attachPipMenu();
}
private void attachPipMenu() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: attachPipMenu()", TAG);
if (mPipMenuView != null) {
detachPipMenu();
}
attachPipBackgroundView();
attachPipMenuView();
int pipEduTextHeight = mContext.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
int pipMenuBorderWidth = mContext.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_border_width);
mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
-pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
}
private void attachPipMenuView() {
mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider);
setUpViewSurfaceZOrder(mPipMenuView, 1);
addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
}
private void attachPipBackgroundView() {
mPipBackgroundView = LayoutInflater.from(mContext)
.inflate(R.layout.tv_pip_menu_background, null);
setUpViewSurfaceZOrder(mPipBackgroundView, -1);
addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE);
}
private void setUpViewSurfaceZOrder(View v, int zOrderRelativeToPip) {
v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
v.getViewRootImpl().addSurfaceChangedCallback(
new PipMenuSurfaceChangedCallback(v, zOrderRelativeToPip));
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});
}
private void addPipMenuViewToSystemWindows(View v, String title) {
mSystemWindows.addView(v, getPipMenuLayoutParams(mContext, title, 0 /* width */,
0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
}
void onPipTransitionFinished(boolean enterTransition) {
// There is a race between when this is called and when the last frame of the pip transition
// is drawn. To ensure that view updates are applied only when the animation has fully drawn
// and the menu view has been fully remeasured and relaid out, we add a small delay here by
// posting on the handler.
mMainHandler.post(() -> {
if (mPipMenuView != null) {
mPipMenuView.onPipTransitionFinished(enterTransition);
}
});
}
void showMovementMenuOnly() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showMovementMenuOnly()", TAG);
setInMoveMode(true);
mCloseAfterExitMoveMenu = true;
showMenuInternal();
}
@Override
public void showMenu() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
setInMoveMode(false);
mCloseAfterExitMoveMenu = false;
showMenuInternal();
}
private void showMenuInternal() {
if (mPipMenuView == null) {
return;
}
grantPipMenuFocus(true);
if (mInMoveMode) {
mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
} else {
mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false);
}
mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
}
void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
if (mPipMenuView != null) {
mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
}
}
void updateGravity(int gravity) {
mPipMenuView.showMovementHints(gravity);
}
private Rect calculateMenuSurfaceBounds(Rect pipBounds) {
return mPipMenuView.getPipMenuContainerBounds(pipBounds);
}
void closeMenu() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: closeMenu()", TAG);
if (mPipMenuView == null) {
return;
}
mPipMenuView.hideAllUserControls();
grantPipMenuFocus(false);
mDelegate.onMenuClosed();
}
boolean isInMoveMode() {
return mInMoveMode;
}
private void setInMoveMode(boolean moveMode) {
if (mInMoveMode == moveMode) {
return;
}
mInMoveMode = moveMode;
if (mDelegate != null) {
mDelegate.onInMoveModeChanged();
}
}
@Override
public void onEnterMoveMode() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
mCloseAfterExitMoveMenu);
setInMoveMode(true);
mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
}
@Override
public boolean onExitMoveMode() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
mCloseAfterExitMoveMenu);
if (mCloseAfterExitMoveMenu) {
setInMoveMode(false);
mCloseAfterExitMoveMenu = false;
closeMenu();
return true;
}
if (mInMoveMode) {
setInMoveMode(false);
mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true);
return true;
}
return false;
}
@Override
public boolean onPipMovement(int keycode) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipMovement - %b", TAG, mInMoveMode);
if (mInMoveMode) {
mDelegate.movePip(keycode);
}
return mInMoveMode;
}
@Override
public void detach() {
closeMenu();
detachPipMenu();
mLeash = null;
}
@Override
public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) {
// NOOP - handled via the TvPipActionsProvider
}
@Override
public boolean isMenuVisible() {
return true;
}
/**
* Does an immediate window crop of the PiP menu.
*/
@Override
public void resizePipMenu(@Nullable SurfaceControl pipLeash,
@Nullable SurfaceControl.Transaction t,
Rect destinationBounds) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: resizePipMenu: %s", TAG, destinationBounds.toShortString());
if (destinationBounds.isEmpty()) {
return;
}
if (!maybeCreateSyncApplier()) {
return;
}
final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds);
final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams =
new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface)
.withWindowCrop(menuBounds)
.build();
final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
final SyncRtSurfaceTransactionApplier.SurfaceParams backParams =
new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface)
.withWindowCrop(menuBounds)
.build();
// TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the
// animations of the pip surface with the content of the front and back menu surfaces
mBackgroundApplier.scheduleApply(backParams);
if (pipLeash != null && t != null) {
final SyncRtSurfaceTransactionApplier.SurfaceParams
pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
.withMergeTransaction(t)
.build();
mApplier.scheduleApply(frontParams, pipParams);
} else {
mApplier.scheduleApply(frontParams);
}
}
private SurfaceControl getSurfaceControl(View v) {
return mSystemWindows.getViewSurface(v);
}
@Override
public void runWithNextFrame(Runnable runnable) {
if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
runnable.run();
}
mPipMenuView.getViewRootImpl().registerRtFrameCallback(frame -> {
mMainHandler.post(runnable);
});
mPipMenuView.invalidate();
}
@Override
public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction transaction,
Rect pipDestBounds) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePipMenu: %s", TAG, pipDestBounds.toShortString());
if (pipDestBounds.isEmpty()) {
if (transaction == null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: no transaction given", TAG);
}
return;
}
if (!maybeCreateSyncApplier()) {
return;
}
final Rect menuDestBounds = calculateMenuSurfaceBounds(pipDestBounds);
final Rect tmpSourceBounds = new Rect();
// 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 && transaction != null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: tmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG);
mPipMenuView.getBoundsOnScreen(tmpSourceBounds);
} else {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: tmpSourceBounds based on menu width and height", TAG);
tmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height());
}
mTmpSourceRectF.set(tmpSourceBounds);
mTmpDestinationRectF.set(menuDestBounds);
mMoveTransform.setTranslate(mTmpDestinationRectF.left, mTmpDestinationRectF.top);
final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams =
new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface)
.withMatrix(mMoveTransform)
.build();
final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
final SyncRtSurfaceTransactionApplier.SurfaceParams backParams =
new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface)
.withMatrix(mMoveTransform)
.build();
// TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the
// animations of the pip surface with the content of the front and back menu surfaces
mBackgroundApplier.scheduleApply(backParams);
if (pipLeash != null && transaction != null) {
final SyncRtSurfaceTransactionApplier.SurfaceParams pipParams =
new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
.withMergeTransaction(transaction)
.build();
mApplier.scheduleApply(frontParams, pipParams);
} else {
mApplier.scheduleApply(frontParams);
}
updateMenuBounds(pipDestBounds);
}
private boolean maybeCreateSyncApplier() {
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;
}
if (mApplier == null) {
mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
}
if (mBackgroundApplier == null) {
mBackgroundApplier = new SyncRtSurfaceTransactionApplier(mPipBackgroundView);
}
return true;
}
private void detachPipMenu() {
if (mPipMenuView != null) {
mApplier = null;
mSystemWindows.removeView(mPipMenuView);
mPipMenuView = null;
}
if (mPipBackgroundView != null) {
mBackgroundApplier = null;
mSystemWindows.removeView(mPipBackgroundView);
mPipBackgroundView = null;
}
}
@Override
public void updateMenuBounds(Rect destinationBounds) {
final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
mSystemWindows.updateViewLayout(mPipBackgroundView,
getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
menuBounds.height()));
mSystemWindows.updateViewLayout(mPipMenuView,
getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
menuBounds.height()));
if (mPipMenuView != null) {
mPipMenuView.updateBounds(destinationBounds);
}
}
@Override
public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onFocusTaskChanged", TAG);
}
@Override
public void onBackPress() {
if (!onExitMoveMode()) {
closeMenu();
}
}
@Override
public void onCloseButtonClick() {
mDelegate.closePip();
}
@Override
public void onFullscreenButtonClick() {
mDelegate.movePipToFullscreen();
}
@Override
public void onToggleExpandedMode() {
mDelegate.togglePipExpansion();
}
@Override
public void onCloseEduText() {
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
mDelegate.closeEduText();
}
interface Delegate {
void movePipToFullscreen();
void movePip(int keycode);
void onInMoveModeChanged();
int getPipGravity();
void togglePipExpansion();
void onMenuClosed();
void closeEduText();
void closePip();
}
private void grantPipMenuFocus(boolean grantFocus) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: grantWindowFocus(%b)", TAG, grantFocus);
try {
WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
mSystemWindows.getFocusGrantToken(mPipMenuView), grantFocus);
} catch (Exception e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Unable to update focus, %s", TAG, e);
}
}
private class PipMenuSurfaceChangedCallback implements ViewRootImpl.SurfaceChangedCallback {
private final View mView;
private final int mZOrder;
PipMenuSurfaceChangedCallback(View v, int zOrder) {
mView = v;
mZOrder = zOrder;
}
@Override
public void surfaceCreated(SurfaceControl.Transaction t) {
final SurfaceControl sc = getSurfaceControl(mView);
if (sc != null) {
t.setRelativeLayer(sc, mLeash, mZOrder);
}
}
@Override
public void surfaceReplaced(SurfaceControl.Transaction t) {
}
@Override
public void surfaceDestroyed() {
}
}
}