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