| /* |
| * 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() { |
| } |
| } |
| } |