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