1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.pip.phone; 18 19 import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; 20 21 import android.annotation.Nullable; 22 import android.app.ActivityManager; 23 import android.app.RemoteAction; 24 import android.content.Context; 25 import android.graphics.Matrix; 26 import android.graphics.Rect; 27 import android.graphics.RectF; 28 import android.os.Debug; 29 import android.os.Handler; 30 import android.os.RemoteException; 31 import android.util.Size; 32 import android.view.MotionEvent; 33 import android.view.SurfaceControl; 34 import android.view.View; 35 import android.view.ViewRootImpl; 36 import android.view.WindowManagerGlobal; 37 38 import com.android.internal.protolog.common.ProtoLog; 39 import com.android.wm.shell.common.ShellExecutor; 40 import com.android.wm.shell.common.SystemWindows; 41 import com.android.wm.shell.common.pip.PipBoundsState; 42 import com.android.wm.shell.common.pip.PipMediaController; 43 import com.android.wm.shell.common.pip.PipMediaController.ActionListener; 44 import com.android.wm.shell.common.pip.PipUiEventLogger; 45 import com.android.wm.shell.pip.PipMenuController; 46 import com.android.wm.shell.pip.PipSurfaceTransactionHelper; 47 import com.android.wm.shell.protolog.ShellProtoLogGroup; 48 import com.android.wm.shell.splitscreen.SplitScreenController; 49 50 import java.io.PrintWriter; 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Optional; 54 55 /** 56 * Manages the PiP menu view which can show menu options or a scrim. 57 * 58 * The current media session provides actions whenever there are no valid actions provided by the 59 * current PiP activity. Otherwise, those actions always take precedence. 60 */ 61 public class PhonePipMenuController implements PipMenuController { 62 63 private static final String TAG = "PhonePipMenuController"; 64 private static final boolean DEBUG = false; 65 66 public static final int MENU_STATE_NONE = 0; 67 public static final int MENU_STATE_FULL = 1; 68 69 /** 70 * A listener interface to receive notification on changes in PIP. 71 */ 72 public interface Listener { 73 /** 74 * Called when the PIP menu visibility change has started. 75 * 76 * @param menuState the new, about-to-change state of the menu 77 * @param resize whether or not to resize the PiP with the state change 78 */ onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback)79 void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback); 80 81 /** 82 * Called when the PIP menu state has finished changing/animating. 83 * 84 * @param menuState the new state of the menu. 85 */ onPipMenuStateChangeFinish(int menuState)86 void onPipMenuStateChangeFinish(int menuState); 87 88 /** 89 * Called when the PIP requested to be expanded. 90 */ onPipExpand()91 void onPipExpand(); 92 93 /** 94 * Called when the PIP requested to be dismissed. 95 */ onPipDismiss()96 void onPipDismiss(); 97 98 /** 99 * Called when the PIP requested to show the menu. 100 */ onPipShowMenu()101 void onPipShowMenu(); 102 103 /** 104 * Called when the PIP requested to enter Split. 105 */ onEnterSplit()106 void onEnterSplit(); 107 } 108 109 private final Matrix mMoveTransform = new Matrix(); 110 private final Rect mTmpSourceBounds = new Rect(); 111 private final RectF mTmpSourceRectF = new RectF(); 112 private final RectF mTmpDestinationRectF = new RectF(); 113 private final Context mContext; 114 private final PipBoundsState mPipBoundsState; 115 private final PipMediaController mMediaController; 116 private final ShellExecutor mMainExecutor; 117 private final Handler mMainHandler; 118 119 private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory 120 mSurfaceControlTransactionFactory; 121 private final float[] mTmpTransform = new float[9]; 122 123 private final ArrayList<Listener> mListeners = new ArrayList<>(); 124 private final SystemWindows mSystemWindows; 125 private final Optional<SplitScreenController> mSplitScreenController; 126 private final PipUiEventLogger mPipUiEventLogger; 127 128 private List<RemoteAction> mAppActions; 129 private RemoteAction mCloseAction; 130 private List<RemoteAction> mMediaActions; 131 132 private int mMenuState; 133 134 private PipMenuView mPipMenuView; 135 136 private SurfaceControl mLeash; 137 138 private ActionListener mMediaActionListener = new ActionListener() { 139 @Override 140 public void onMediaActionsChanged(List<RemoteAction> mediaActions) { 141 mMediaActions = new ArrayList<>(mediaActions); 142 updateMenuActions(); 143 } 144 }; 145 PhonePipMenuController(Context context, PipBoundsState pipBoundsState, PipMediaController mediaController, SystemWindows systemWindows, Optional<SplitScreenController> splitScreenOptional, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor, Handler mainHandler)146 public PhonePipMenuController(Context context, PipBoundsState pipBoundsState, 147 PipMediaController mediaController, SystemWindows systemWindows, 148 Optional<SplitScreenController> splitScreenOptional, 149 PipUiEventLogger pipUiEventLogger, 150 ShellExecutor mainExecutor, Handler mainHandler) { 151 mContext = context; 152 mPipBoundsState = pipBoundsState; 153 mMediaController = mediaController; 154 mSystemWindows = systemWindows; 155 mMainExecutor = mainExecutor; 156 mMainHandler = mainHandler; 157 mSplitScreenController = splitScreenOptional; 158 mPipUiEventLogger = pipUiEventLogger; 159 160 mSurfaceControlTransactionFactory = 161 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); 162 } 163 isMenuVisible()164 public boolean isMenuVisible() { 165 return mPipMenuView != null && mMenuState != MENU_STATE_NONE; 166 } 167 168 /** 169 * Attach the menu when the PiP task first appears. 170 */ 171 @Override attach(SurfaceControl leash)172 public void attach(SurfaceControl leash) { 173 mLeash = leash; 174 attachPipMenuView(); 175 } 176 177 /** 178 * Detach the menu when the PiP task is gone. 179 */ 180 @Override detach()181 public void detach() { 182 hideMenu(); 183 detachPipMenuView(); 184 mLeash = null; 185 } 186 attachPipMenuView()187 void attachPipMenuView() { 188 // In case detach was not called (e.g. PIP unexpectedly closed) 189 if (mPipMenuView != null) { 190 detachPipMenuView(); 191 } 192 mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler, 193 mSplitScreenController, mPipUiEventLogger); 194 mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 195 @Override 196 public void onViewAttachedToWindow(View v) { 197 v.getViewRootImpl().addSurfaceChangedCallback( 198 new ViewRootImpl.SurfaceChangedCallback() { 199 @Override 200 public void surfaceCreated(SurfaceControl.Transaction t) { 201 final SurfaceControl sc = getSurfaceControl(); 202 if (sc != null) { 203 t.reparent(sc, mLeash); 204 // make menu on top of the surface 205 t.setLayer(sc, Integer.MAX_VALUE); 206 } 207 } 208 209 @Override 210 public void surfaceReplaced(SurfaceControl.Transaction t) { 211 } 212 213 @Override 214 public void surfaceDestroyed() { 215 } 216 }); 217 } 218 219 @Override 220 public void onViewDetachedFromWindow(View v) { 221 } 222 }); 223 224 mSystemWindows.addView(mPipMenuView, 225 getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), 226 0, SHELL_ROOT_LAYER_PIP); 227 setShellRootAccessibilityWindow(); 228 229 // Make sure the initial actions are set 230 updateMenuActions(); 231 } 232 detachPipMenuView()233 private void detachPipMenuView() { 234 if (mPipMenuView == null) { 235 return; 236 } 237 238 mSystemWindows.removeView(mPipMenuView); 239 mPipMenuView = null; 240 } 241 242 /** 243 * Updates the layout parameters of the menu. 244 * @param destinationBounds New Menu bounds. 245 */ 246 @Override updateMenuBounds(Rect destinationBounds)247 public void updateMenuBounds(Rect destinationBounds) { 248 mSystemWindows.updateViewLayout(mPipMenuView, 249 getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(), 250 destinationBounds.height())); 251 updateMenuLayout(destinationBounds); 252 } 253 254 @Override onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo)255 public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { 256 if (mPipMenuView != null) { 257 mPipMenuView.onFocusTaskChanged(taskInfo); 258 } 259 } 260 261 /** 262 * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some 263 * reason (ie. the window isn't ready yet, thus {@link android.view.ViewRootImpl} is 264 * {@code null}), it will get the leash that the WindowlessWM has assigned to it. 265 */ getSurfaceControl()266 public SurfaceControl getSurfaceControl() { 267 return mSystemWindows.getViewSurface(mPipMenuView); 268 } 269 270 /** 271 * Adds a new menu activity listener. 272 */ addListener(Listener listener)273 public void addListener(Listener listener) { 274 if (!mListeners.contains(listener)) { 275 mListeners.add(listener); 276 } 277 } 278 279 @Nullable getEstimatedMinMenuSize()280 Size getEstimatedMinMenuSize() { 281 return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize(); 282 } 283 284 /** 285 * When other components requests the menu controller directly to show the menu, we must 286 * first fire off the request to the other listeners who will then propagate the call 287 * back to the controller with the right parameters. 288 */ 289 @Override showMenu()290 public void showMenu() { 291 mListeners.forEach(Listener::onPipShowMenu); 292 } 293 294 /** 295 * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu 296 * upon PiP window transition is finished. 297 */ showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)298 public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, 299 boolean willResizeMenu, boolean showResizeHandle) { 300 if (willResizeMenu) { 301 // hide all visible controls including close button and etc. first, this is to ensure 302 // menu is totally invisible during the transition to eliminate unpleasant artifacts 303 fadeOutMenu(); 304 } 305 showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, 306 willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle); 307 } 308 309 /** 310 * Shows the menu activity immediately. 311 */ showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)312 public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, 313 boolean willResizeMenu, boolean showResizeHandle) { 314 showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, 315 false /* withDelay */, showResizeHandle); 316 } 317 showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean withDelay, boolean showResizeHandle)318 private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, 319 boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { 320 if (DEBUG) { 321 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 322 "%s: showMenu() state=%s" 323 + " isMenuVisible=%s" 324 + " allowMenuTimeout=%s" 325 + " willResizeMenu=%s" 326 + " withDelay=%s" 327 + " showResizeHandle=%s" 328 + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout, 329 willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " ")); 330 } 331 332 if (!checkPipMenuState()) { 333 return; 334 } 335 336 // Sync the menu bounds before showing it in case it is out of sync. 337 movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds, 338 PipMenuController.ALPHA_NO_CHANGE); 339 updateMenuBounds(stackBounds); 340 341 mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, 342 showResizeHandle); 343 } 344 345 /** 346 * Move the PiP menu, which does a translation and possibly a scale transformation. 347 */ 348 @Override movePipMenu(@ullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds, float alpha)349 public void movePipMenu(@Nullable SurfaceControl pipLeash, 350 @Nullable SurfaceControl.Transaction t, 351 Rect destinationBounds, float alpha) { 352 if (destinationBounds.isEmpty()) { 353 return; 354 } 355 356 if (!checkPipMenuState()) { 357 return; 358 } 359 360 // TODO(b/286307861) transaction should be applied outside of PiP menu controller 361 if (pipLeash != null && t != null) { 362 t.apply(); 363 } 364 } 365 366 /** 367 * Does an immediate window crop of the PiP menu. 368 */ 369 @Override resizePipMenu(@ullable SurfaceControl pipLeash, @Nullable SurfaceControl.Transaction t, Rect destinationBounds)370 public void resizePipMenu(@Nullable SurfaceControl pipLeash, 371 @Nullable SurfaceControl.Transaction t, 372 Rect destinationBounds) { 373 if (destinationBounds.isEmpty()) { 374 return; 375 } 376 377 if (!checkPipMenuState()) { 378 return; 379 } 380 381 // TODO(b/286307861) transaction should be applied outside of PiP menu controller 382 if (pipLeash != null && t != null) { 383 t.apply(); 384 } 385 } 386 checkPipMenuState()387 private boolean checkPipMenuState() { 388 if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { 389 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 390 "%s: Not going to move PiP, either menu or its parent is not created.", TAG); 391 return false; 392 } 393 394 return true; 395 } 396 397 /** 398 * Pokes the menu, indicating that the user is interacting with it. 399 */ pokeMenu()400 public void pokeMenu() { 401 final boolean isMenuVisible = isMenuVisible(); 402 if (DEBUG) { 403 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 404 "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible); 405 } 406 if (isMenuVisible) { 407 mPipMenuView.pokeMenu(); 408 } 409 } 410 fadeOutMenu()411 private void fadeOutMenu() { 412 final boolean isMenuVisible = isMenuVisible(); 413 if (DEBUG) { 414 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 415 "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible); 416 } 417 if (isMenuVisible) { 418 mPipMenuView.fadeOutMenu(); 419 } 420 } 421 422 /** 423 * Hides the menu view. 424 */ hideMenu()425 public void hideMenu() { 426 final boolean isMenuVisible = isMenuVisible(); 427 if (isMenuVisible) { 428 mPipMenuView.hideMenu(); 429 } 430 } 431 432 /** 433 * Hides the menu view. 434 * 435 * @param animationType the animation type to use upon hiding the menu 436 * @param resize whether or not to resize the PiP with the state change 437 */ hideMenu(@ipMenuView.AnimationType int animationType, boolean resize)438 public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) { 439 final boolean isMenuVisible = isMenuVisible(); 440 if (DEBUG) { 441 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 442 "%s: hideMenu() state=%s" 443 + " isMenuVisible=%s" 444 + " animationType=%s" 445 + " resize=%s" 446 + " callers=\n%s", TAG, mMenuState, isMenuVisible, 447 animationType, resize, 448 Debug.getCallers(5, " ")); 449 } 450 if (isMenuVisible) { 451 mPipMenuView.hideMenu(resize, animationType); 452 } 453 } 454 455 /** 456 * Hides the menu activity. 457 */ hideMenu(Runnable onStartCallback, Runnable onEndCallback)458 public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { 459 if (isMenuVisible()) { 460 // If the menu is visible in either the closed or full state, then hide the menu and 461 // trigger the animation trigger afterwards 462 if (onStartCallback != null) { 463 onStartCallback.run(); 464 } 465 mPipMenuView.hideMenu(onEndCallback); 466 } 467 } 468 469 /** 470 * Sets the menu actions to the actions provided by the current PiP menu. 471 */ 472 @Override setAppActions(List<RemoteAction> appActions, RemoteAction closeAction)473 public void setAppActions(List<RemoteAction> appActions, 474 RemoteAction closeAction) { 475 mAppActions = appActions; 476 mCloseAction = closeAction; 477 updateMenuActions(); 478 } 479 onPipExpand()480 void onPipExpand() { 481 mListeners.forEach(Listener::onPipExpand); 482 } 483 onPipDismiss()484 void onPipDismiss() { 485 mListeners.forEach(Listener::onPipDismiss); 486 } 487 onEnterSplit()488 void onEnterSplit() { 489 mListeners.forEach(Listener::onEnterSplit); 490 } 491 492 /** 493 * @return the best set of actions to show in the PiP menu. 494 */ resolveMenuActions()495 private List<RemoteAction> resolveMenuActions() { 496 if (isValidActions(mAppActions)) { 497 return mAppActions; 498 } 499 return mMediaActions; 500 } 501 502 /** 503 * Updates the PiP menu with the best set of actions provided. 504 */ updateMenuActions()505 private void updateMenuActions() { 506 if (mPipMenuView != null) { 507 mPipMenuView.setActions(mPipBoundsState.getBounds(), 508 resolveMenuActions(), mCloseAction); 509 } 510 } 511 512 /** 513 * Returns whether the set of actions are valid. 514 */ isValidActions(List<?> actions)515 private static boolean isValidActions(List<?> actions) { 516 return actions != null && actions.size() > 0; 517 } 518 519 /** 520 * Handles changes in menu visibility. 521 */ onMenuStateChangeStart(int menuState, boolean resize, Runnable callback)522 void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { 523 if (DEBUG) { 524 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 525 "%s: onMenuStateChangeStart() mMenuState=%s" 526 + " menuState=%s resize=%s" 527 + " callers=\n%s", TAG, mMenuState, menuState, resize, 528 Debug.getCallers(5, " ")); 529 } 530 531 if (menuState != mMenuState) { 532 mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback)); 533 if (menuState == MENU_STATE_FULL) { 534 // Once visible, start listening for media action changes. This call will trigger 535 // the menu actions to be updated again. 536 mMediaController.addActionListener(mMediaActionListener); 537 } else { 538 // Once hidden, stop listening for media action changes. This call will trigger 539 // the menu actions to be updated again. 540 mMediaController.removeActionListener(mMediaActionListener); 541 } 542 543 try { 544 WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, 545 mSystemWindows.getFocusGrantToken(mPipMenuView), 546 menuState != MENU_STATE_NONE /* grantFocus */); 547 } catch (RemoteException e) { 548 ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 549 "%s: Unable to update focus as menu appears/disappears, %s", TAG, e); 550 } 551 } 552 } 553 onMenuStateChangeFinish(int menuState)554 void onMenuStateChangeFinish(int menuState) { 555 if (menuState != mMenuState) { 556 mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState)); 557 } 558 mMenuState = menuState; 559 setShellRootAccessibilityWindow(); 560 } 561 setShellRootAccessibilityWindow()562 private void setShellRootAccessibilityWindow() { 563 switch (mMenuState) { 564 case MENU_STATE_NONE: 565 mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null); 566 break; 567 default: 568 mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, 569 mPipMenuView); 570 break; 571 } 572 } 573 574 /** 575 * Handles a pointer event sent from pip input consumer. 576 */ handlePointerEvent(MotionEvent ev)577 void handlePointerEvent(MotionEvent ev) { 578 if (mPipMenuView == null) { 579 return; 580 } 581 582 if (ev.isTouchEvent()) { 583 mPipMenuView.dispatchTouchEvent(ev); 584 } else { 585 mPipMenuView.dispatchGenericMotionEvent(ev); 586 } 587 } 588 589 /** 590 * Tell the PIP Menu to recalculate its layout given its current position on the display. 591 */ updateMenuLayout(Rect bounds)592 public void updateMenuLayout(Rect bounds) { 593 final boolean isMenuVisible = isMenuVisible(); 594 if (DEBUG) { 595 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 596 "%s: updateMenuLayout() state=%s" 597 + " isMenuVisible=%s" 598 + " callers=\n%s", TAG, mMenuState, isMenuVisible, 599 Debug.getCallers(5, " ")); 600 } 601 if (isMenuVisible) { 602 mPipMenuView.updateMenuLayout(bounds); 603 } 604 } 605 dump(PrintWriter pw, String prefix)606 void dump(PrintWriter pw, String prefix) { 607 final String innerPrefix = prefix + " "; 608 pw.println(prefix + TAG); 609 pw.println(innerPrefix + "mMenuState=" + mMenuState); 610 pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView); 611 pw.println(innerPrefix + "mListeners=" + mListeners.size()); 612 } 613 } 614