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