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