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