1 /* 2 * Copyright (C) 2016 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.systemui.pip.phone; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 21 22 import android.annotation.Nullable; 23 import android.app.ActivityManager.StackInfo; 24 import android.app.ActivityOptions; 25 import android.app.ActivityTaskManager; 26 import android.app.RemoteAction; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.ParceledListSlice; 30 import android.graphics.Rect; 31 import android.os.Bundle; 32 import android.os.Debug; 33 import android.os.Handler; 34 import android.os.Looper; 35 import android.os.Message; 36 import android.os.Messenger; 37 import android.os.RemoteException; 38 import android.os.SystemClock; 39 import android.os.UserHandle; 40 import android.util.Log; 41 import android.view.MotionEvent; 42 43 import com.android.systemui.pip.phone.PipMediaController.ActionListener; 44 import com.android.systemui.shared.system.InputConsumerController; 45 46 import java.io.PrintWriter; 47 import java.util.ArrayList; 48 import java.util.List; 49 50 /** 51 * Manages the PiP menu activity which can show menu options or a scrim. 52 * 53 * The current media session provides actions whenever there are no valid actions provided by the 54 * current PiP activity. Otherwise, those actions always take precedence. 55 */ 56 public class PipMenuActivityController { 57 58 private static final String TAG = "PipMenuActController"; 59 private static final boolean DEBUG = false; 60 61 public static final String EXTRA_CONTROLLER_MESSENGER = "messenger"; 62 public static final String EXTRA_ACTIONS = "actions"; 63 public static final String EXTRA_STACK_BOUNDS = "stack_bounds"; 64 public static final String EXTRA_ALLOW_TIMEOUT = "allow_timeout"; 65 public static final String EXTRA_WILL_RESIZE_MENU = "resize_menu_on_show"; 66 public static final String EXTRA_DISMISS_FRACTION = "dismiss_fraction"; 67 public static final String EXTRA_MENU_STATE = "menu_state"; 68 public static final String EXTRA_SHOW_MENU_WITH_DELAY = "show_menu_with_delay"; 69 public static final String EXTRA_SHOW_RESIZE_HANDLE = "show_resize_handle"; 70 public static final String EXTRA_MESSAGE_CALLBACK_WHAT = "message_callback_what"; 71 72 public static final int MESSAGE_MENU_STATE_CHANGED = 100; 73 public static final int MESSAGE_EXPAND_PIP = 101; 74 public static final int MESSAGE_DISMISS_PIP = 103; 75 public static final int MESSAGE_UPDATE_ACTIVITY_CALLBACK = 104; 76 public static final int MESSAGE_SHOW_MENU = 107; 77 78 public static final int MENU_STATE_NONE = 0; 79 public static final int MENU_STATE_CLOSE = 1; 80 public static final int MENU_STATE_FULL = 2; 81 82 // The duration to wait before we consider the start activity as having timed out 83 private static final long START_ACTIVITY_REQUEST_TIMEOUT_MS = 300; 84 85 /** 86 * A listener interface to receive notification on changes in PIP. 87 */ 88 public interface Listener { 89 /** 90 * Called when the PIP menu visibility changes. 91 * 92 * @param menuState the current state of the menu 93 * @param resize whether or not to resize the PiP with the state change 94 */ onPipMenuStateChanged(int menuState, boolean resize, Runnable callback)95 void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback); 96 97 /** 98 * Called when the PIP requested to be expanded. 99 */ onPipExpand()100 void onPipExpand(); 101 102 /** 103 * Called when the PIP requested to be dismissed. 104 */ onPipDismiss()105 void onPipDismiss(); 106 107 /** 108 * Called when the PIP requested to show the menu. 109 */ onPipShowMenu()110 void onPipShowMenu(); 111 } 112 113 private Context mContext; 114 private PipMediaController mMediaController; 115 private InputConsumerController mInputConsumerController; 116 117 private ArrayList<Listener> mListeners = new ArrayList<>(); 118 private ParceledListSlice mAppActions; 119 private ParceledListSlice mMediaActions; 120 private int mMenuState; 121 122 // The dismiss fraction update is sent frequently, so use a temporary bundle for the message 123 private Bundle mTmpDismissFractionData = new Bundle(); 124 125 private Runnable mOnAnimationEndRunnable; 126 private boolean mStartActivityRequested; 127 private long mStartActivityRequestedTime; 128 private Messenger mToActivityMessenger; 129 private Handler mHandler = new Handler(Looper.getMainLooper()) { 130 @Override 131 public void handleMessage(Message msg) { 132 switch (msg.what) { 133 case MESSAGE_MENU_STATE_CHANGED: { 134 final int menuState = msg.arg1; 135 final boolean resize = msg.arg2 != 0; 136 onMenuStateChanged(menuState, resize, 137 getMenuStateChangeFinishedCallback(msg.replyTo, (Bundle) msg.obj)); 138 break; 139 } 140 case MESSAGE_EXPAND_PIP: { 141 mListeners.forEach(Listener::onPipExpand); 142 break; 143 } 144 case MESSAGE_DISMISS_PIP: { 145 mListeners.forEach(Listener::onPipDismiss); 146 break; 147 } 148 case MESSAGE_SHOW_MENU: { 149 mListeners.forEach(Listener::onPipShowMenu); 150 break; 151 } 152 case MESSAGE_UPDATE_ACTIVITY_CALLBACK: { 153 mToActivityMessenger = msg.replyTo; 154 setStartActivityRequested(false); 155 if (mOnAnimationEndRunnable != null) { 156 mOnAnimationEndRunnable.run(); 157 mOnAnimationEndRunnable = null; 158 } 159 // Mark the menu as invisible once the activity finishes as well 160 if (mToActivityMessenger == null) { 161 final boolean resize = msg.arg1 != 0; 162 onMenuStateChanged(MENU_STATE_NONE, resize, null /* callback */); 163 } 164 break; 165 } 166 } 167 } 168 }; 169 private Messenger mMessenger = new Messenger(mHandler); 170 171 private Runnable mStartActivityRequestedTimeoutRunnable = () -> { 172 setStartActivityRequested(false); 173 if (mOnAnimationEndRunnable != null) { 174 mOnAnimationEndRunnable.run(); 175 mOnAnimationEndRunnable = null; 176 } 177 Log.e(TAG, "Expected start menu activity request timed out"); 178 }; 179 180 private ActionListener mMediaActionListener = new ActionListener() { 181 @Override 182 public void onMediaActionsChanged(List<RemoteAction> mediaActions) { 183 mMediaActions = new ParceledListSlice<>(mediaActions); 184 updateMenuActions(); 185 } 186 }; 187 PipMenuActivityController(Context context, PipMediaController mediaController, InputConsumerController inputConsumerController)188 public PipMenuActivityController(Context context, 189 PipMediaController mediaController, InputConsumerController inputConsumerController) { 190 mContext = context; 191 mMediaController = mediaController; 192 mInputConsumerController = inputConsumerController; 193 } 194 isMenuActivityVisible()195 public boolean isMenuActivityVisible() { 196 return mToActivityMessenger != null; 197 } 198 onActivityPinned()199 public void onActivityPinned() { 200 mInputConsumerController.registerInputConsumer(true /* withSfVsync */); 201 } 202 onActivityUnpinned()203 public void onActivityUnpinned() { 204 hideMenu(); 205 mInputConsumerController.unregisterInputConsumer(); 206 setStartActivityRequested(false); 207 } 208 onPinnedStackAnimationEnded()209 public void onPinnedStackAnimationEnded() { 210 // Note: Only active menu activities care about this event 211 if (mToActivityMessenger != null) { 212 Message m = Message.obtain(); 213 m.what = PipMenuActivity.MESSAGE_ANIMATION_ENDED; 214 try { 215 mToActivityMessenger.send(m); 216 } catch (RemoteException e) { 217 Log.e(TAG, "Could not notify menu pinned animation ended", e); 218 } 219 } 220 } 221 222 /** 223 * Adds a new menu activity listener. 224 */ addListener(Listener listener)225 public void addListener(Listener listener) { 226 if (!mListeners.contains(listener)) { 227 mListeners.add(listener); 228 } 229 } 230 231 /** 232 * Updates the appearance of the menu and scrim on top of the PiP while dismissing. 233 */ setDismissFraction(float fraction)234 public void setDismissFraction(float fraction) { 235 if (DEBUG) { 236 Log.d(TAG, "setDismissFraction() hasActivity=" + (mToActivityMessenger != null) 237 + " fraction=" + fraction); 238 } 239 if (mToActivityMessenger != null) { 240 mTmpDismissFractionData.clear(); 241 mTmpDismissFractionData.putFloat(EXTRA_DISMISS_FRACTION, fraction); 242 Message m = Message.obtain(); 243 m.what = PipMenuActivity.MESSAGE_UPDATE_DISMISS_FRACTION; 244 m.obj = mTmpDismissFractionData; 245 try { 246 mToActivityMessenger.send(m); 247 } catch (RemoteException e) { 248 Log.e(TAG, "Could not notify menu to update dismiss fraction", e); 249 } 250 } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) { 251 // If we haven't requested the start activity, or if it previously took too long to 252 // start, then start it 253 startMenuActivity(MENU_STATE_NONE, null /* stackBounds */, 254 false /* allowMenuTimeout */, false /* resizeMenuOnShow */, 255 false /* withDelay */, false /* showResizeHandle */); 256 } 257 } 258 259 /** 260 * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu 261 * upon PiP window transition is finished. 262 */ showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)263 public void showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, 264 boolean willResizeMenu, boolean showResizeHandle) { 265 // hide all visible controls including close button and etc. first, this is to ensure 266 // menu is totally invisible during the transition to eliminate unpleasant artifacts 267 fadeOutMenu(); 268 showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, 269 true /* withDelay */, showResizeHandle); 270 } 271 272 /** 273 * Shows the menu activity immediately. 274 */ showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean willResizeMenu, boolean showResizeHandle)275 public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, 276 boolean willResizeMenu, boolean showResizeHandle) { 277 showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, 278 false /* withDelay */, showResizeHandle); 279 } 280 getMenuStateChangeFinishedCallback(@ullable Messenger replyTo, @Nullable Bundle data)281 private Runnable getMenuStateChangeFinishedCallback(@Nullable Messenger replyTo, 282 @Nullable Bundle data) { 283 if (replyTo == null || data == null) { 284 return null; 285 } 286 return () -> { 287 try { 288 final Message m = Message.obtain(); 289 m.what = data.getInt(EXTRA_MESSAGE_CALLBACK_WHAT); 290 replyTo.send(m); 291 } catch (RemoteException e) { 292 // ignored 293 } 294 }; 295 } 296 297 private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, 298 boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { 299 if (DEBUG) { 300 Log.d(TAG, "showMenu() state=" + menuState 301 + " hasActivity=" + (mToActivityMessenger != null) 302 + " allowMenuTimeout=" + allowMenuTimeout 303 + " willResizeMenu=" + willResizeMenu 304 + " withDelay=" + withDelay 305 + " showResizeHandle=" + showResizeHandle 306 + " callers=\n" + Debug.getCallers(5, " ")); 307 } 308 309 if (mToActivityMessenger != null) { 310 Bundle data = new Bundle(); 311 data.putInt(EXTRA_MENU_STATE, menuState); 312 if (stackBounds != null) { 313 data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds); 314 } 315 data.putBoolean(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout); 316 data.putBoolean(EXTRA_WILL_RESIZE_MENU, willResizeMenu); 317 data.putBoolean(EXTRA_SHOW_MENU_WITH_DELAY, withDelay); 318 data.putBoolean(EXTRA_SHOW_RESIZE_HANDLE, showResizeHandle); 319 Message m = Message.obtain(); 320 m.what = PipMenuActivity.MESSAGE_SHOW_MENU; 321 m.obj = data; 322 try { 323 mToActivityMessenger.send(m); 324 } catch (RemoteException e) { 325 Log.e(TAG, "Could not notify menu to show", e); 326 } 327 } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) { 328 // If we haven't requested the start activity, or if it previously took too long to 329 // start, then start it 330 startMenuActivity(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, 331 showResizeHandle); 332 } 333 } 334 335 /** 336 * Pokes the menu, indicating that the user is interacting with it. 337 */ 338 public void pokeMenu() { 339 if (DEBUG) { 340 Log.d(TAG, "pokeMenu() hasActivity=" + (mToActivityMessenger != null)); 341 } 342 if (mToActivityMessenger != null) { 343 Message m = Message.obtain(); 344 m.what = PipMenuActivity.MESSAGE_POKE_MENU; 345 try { 346 mToActivityMessenger.send(m); 347 } catch (RemoteException e) { 348 Log.e(TAG, "Could not notify poke menu", e); 349 } 350 } 351 } 352 353 private void fadeOutMenu() { 354 if (DEBUG) { 355 Log.d(TAG, "fadeOutMenu() state=" + mMenuState 356 + " hasActivity=" + (mToActivityMessenger != null) 357 + " callers=\n" + Debug.getCallers(5, " ")); 358 } 359 if (mToActivityMessenger != null) { 360 Message m = Message.obtain(); 361 m.what = PipMenuActivity.MESSAGE_FADE_OUT_MENU; 362 try { 363 mToActivityMessenger.send(m); 364 } catch (RemoteException e) { 365 Log.e(TAG, "Could not notify menu to fade out", e); 366 } 367 } 368 } 369 370 /** 371 * Hides the menu activity. 372 */ 373 public void hideMenu() { 374 if (DEBUG) { 375 Log.d(TAG, "hideMenu() state=" + mMenuState 376 + " hasActivity=" + (mToActivityMessenger != null) 377 + " callers=\n" + Debug.getCallers(5, " ")); 378 } 379 if (mToActivityMessenger != null) { 380 Message m = Message.obtain(); 381 m.what = PipMenuActivity.MESSAGE_HIDE_MENU; 382 try { 383 mToActivityMessenger.send(m); 384 } catch (RemoteException e) { 385 Log.e(TAG, "Could not notify menu to hide", e); 386 } 387 } 388 } 389 390 /** 391 * Hides the menu activity. 392 */ 393 public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { 394 if (mStartActivityRequested) { 395 // If the menu has been start-requested, but not actually started, then we defer the 396 // trigger callback until the menu has started and called back to the controller. 397 mOnAnimationEndRunnable = onEndCallback; 398 onStartCallback.run(); 399 400 // Fallback for b/63752800, we have started the PipMenuActivity but it has not made any 401 // callbacks. Don't continue to wait for the menu to show past some timeout. 402 mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable); 403 mHandler.postDelayed(mStartActivityRequestedTimeoutRunnable, 404 START_ACTIVITY_REQUEST_TIMEOUT_MS); 405 } else if (mMenuState != MENU_STATE_NONE && mToActivityMessenger != null) { 406 // If the menu is visible in either the closed or full state, then hide the menu and 407 // trigger the animation trigger afterwards 408 onStartCallback.run(); 409 Message m = Message.obtain(); 410 m.what = PipMenuActivity.MESSAGE_HIDE_MENU; 411 m.obj = onEndCallback; 412 try { 413 mToActivityMessenger.send(m); 414 } catch (RemoteException e) { 415 Log.e(TAG, "Could not notify hide menu", e); 416 } 417 } 418 } 419 420 /** 421 * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned 422 * stack and don't want to trigger a resize which can animate the stack in a conflicting way 423 * (ie. when manually expanding or dismissing). 424 */ 425 public void hideMenuWithoutResize() { 426 onMenuStateChanged(MENU_STATE_NONE, false /* resize */, null /* callback */); 427 } 428 429 /** 430 * Sets the menu actions to the actions provided by the current PiP activity. 431 */ 432 public void setAppActions(ParceledListSlice appActions) { 433 mAppActions = appActions; 434 updateMenuActions(); 435 } 436 437 /** 438 * @return the best set of actions to show in the PiP menu. 439 */ 440 private ParceledListSlice resolveMenuActions() { 441 if (isValidActions(mAppActions)) { 442 return mAppActions; 443 } 444 return mMediaActions; 445 } 446 447 /** 448 * Starts the menu activity on the top task of the pinned stack. 449 */ 450 private void startMenuActivity(int menuState, Rect stackBounds, boolean allowMenuTimeout, 451 boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { 452 try { 453 StackInfo pinnedStackInfo = ActivityTaskManager.getService().getStackInfo( 454 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); 455 if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null && 456 pinnedStackInfo.taskIds.length > 0) { 457 Intent intent = new Intent(mContext, PipMenuActivity.class); 458 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 459 intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger); 460 intent.putExtra(EXTRA_ACTIONS, resolveMenuActions()); 461 if (stackBounds != null) { 462 intent.putExtra(EXTRA_STACK_BOUNDS, stackBounds); 463 } 464 intent.putExtra(EXTRA_MENU_STATE, menuState); 465 intent.putExtra(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout); 466 intent.putExtra(EXTRA_WILL_RESIZE_MENU, willResizeMenu); 467 intent.putExtra(EXTRA_SHOW_MENU_WITH_DELAY, withDelay); 468 intent.putExtra(EXTRA_SHOW_RESIZE_HANDLE, showResizeHandle); 469 ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0); 470 options.setLaunchTaskId( 471 pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]); 472 options.setTaskOverlay(true, true /* canResume */); 473 mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT); 474 setStartActivityRequested(true); 475 } else { 476 Log.e(TAG, "No PIP tasks found"); 477 } 478 } catch (RemoteException e) { 479 setStartActivityRequested(false); 480 Log.e(TAG, "Error showing PIP menu activity", e); 481 } 482 } 483 484 /** 485 * Updates the PiP menu activity with the best set of actions provided. 486 */ 487 private void updateMenuActions() { 488 if (mToActivityMessenger != null) { 489 // Fetch the pinned stack bounds 490 Rect stackBounds = null; 491 try { 492 StackInfo pinnedStackInfo = ActivityTaskManager.getService().getStackInfo( 493 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); 494 if (pinnedStackInfo != null) { 495 stackBounds = pinnedStackInfo.bounds; 496 } 497 } catch (RemoteException e) { 498 Log.e(TAG, "Error showing PIP menu activity", e); 499 } 500 501 Bundle data = new Bundle(); 502 data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds); 503 data.putParcelable(EXTRA_ACTIONS, resolveMenuActions()); 504 Message m = Message.obtain(); 505 m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS; 506 m.obj = data; 507 try { 508 mToActivityMessenger.send(m); 509 } catch (RemoteException e) { 510 Log.e(TAG, "Could not notify menu activity to update actions", e); 511 } 512 } 513 } 514 515 /** 516 * Returns whether the set of actions are valid. 517 */ 518 private boolean isValidActions(ParceledListSlice actions) { 519 return actions != null && actions.getList().size() > 0; 520 } 521 522 /** 523 * @return whether the time of the activity request has exceeded the timeout. 524 */ 525 private boolean isStartActivityRequestedElapsed() { 526 return (SystemClock.uptimeMillis() - mStartActivityRequestedTime) 527 >= START_ACTIVITY_REQUEST_TIMEOUT_MS; 528 } 529 530 /** 531 * Handles changes in menu visibility. 532 */ 533 private void onMenuStateChanged(int menuState, boolean resize, Runnable callback) { 534 if (DEBUG) { 535 Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState 536 + " menuState=" + menuState + " resize=" + resize 537 + " callers=\n" + Debug.getCallers(5, " ")); 538 } 539 540 if (menuState != mMenuState) { 541 mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize, callback)); 542 if (menuState == MENU_STATE_FULL) { 543 // Once visible, start listening for media action changes. This call will trigger 544 // the menu actions to be updated again. 545 mMediaController.addListener(mMediaActionListener); 546 } else { 547 // Once hidden, stop listening for media action changes. This call will trigger 548 // the menu actions to be updated again. 549 mMediaController.removeListener(mMediaActionListener); 550 } 551 } 552 mMenuState = menuState; 553 } 554 555 private void setStartActivityRequested(boolean requested) { 556 mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable); 557 mStartActivityRequested = requested; 558 mStartActivityRequestedTime = requested ? SystemClock.uptimeMillis() : 0; 559 } 560 561 /** 562 * Handles a pointer event sent from pip input consumer. 563 */ 564 void handlePointerEvent(MotionEvent ev) { 565 if (mToActivityMessenger != null) { 566 Message m = Message.obtain(); 567 m.what = PipMenuActivity.MESSAGE_POINTER_EVENT; 568 m.obj = ev; 569 try { 570 mToActivityMessenger.send(m); 571 } catch (RemoteException e) { 572 Log.e(TAG, "Could not dispatch touch event", e); 573 } 574 } 575 } 576 577 public void dump(PrintWriter pw, String prefix) { 578 final String innerPrefix = prefix + " "; 579 pw.println(prefix + TAG); 580 pw.println(innerPrefix + "mMenuState=" + mMenuState); 581 pw.println(innerPrefix + "mToActivityMessenger=" + mToActivityMessenger); 582 pw.println(innerPrefix + "mListeners=" + mListeners.size()); 583 pw.println(innerPrefix + "mStartActivityRequested=" + mStartActivityRequested); 584 pw.println(innerPrefix + "mStartActivityRequestedTime=" + mStartActivityRequestedTime); 585 } 586 } 587