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