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.tv; 18 19 import android.app.ActivityManager; 20 import android.app.ActivityManager.RunningTaskInfo; 21 import android.app.ActivityManager.StackInfo; 22 import android.app.IActivityManager; 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.ParceledListSlice; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.graphics.Rect; 32 import android.media.session.MediaController; 33 import android.media.session.MediaSessionManager; 34 import android.media.session.PlaybackState; 35 import android.os.Debug; 36 import android.os.Handler; 37 import android.os.RemoteException; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.util.Pair; 41 import android.view.IPinnedStackController; 42 import android.view.IPinnedStackListener; 43 import android.view.IWindowManager; 44 import android.view.WindowManagerGlobal; 45 46 import com.android.systemui.R; 47 import com.android.systemui.pip.BasePipManager; 48 import com.android.systemui.recents.misc.SysUiTaskStackChangeListener; 49 import com.android.systemui.recents.misc.SystemServicesProxy; 50 import com.android.systemui.shared.system.ActivityManagerWrapper; 51 52 import java.io.PrintWriter; 53 import java.util.ArrayList; 54 import java.util.List; 55 56 import static android.app.ActivityManager.StackId.INVALID_STACK_ID; 57 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 58 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 59 import static android.view.Display.DEFAULT_DISPLAY; 60 61 /** 62 * Manages the picture-in-picture (PIP) UI and states. 63 */ 64 public class PipManager implements BasePipManager { 65 private static final String TAG = "PipManager"; 66 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 67 68 private static final String SETTINGS_PACKAGE_AND_CLASS_DELIMITER = "/"; 69 70 private static PipManager sPipManager; 71 private static List<Pair<String, String>> sSettingsPackageAndClassNamePairList; 72 73 /** 74 * State when there's no PIP. 75 */ 76 public static final int STATE_NO_PIP = 0; 77 /** 78 * State when PIP is shown. This is used as default PIP state. 79 */ 80 public static final int STATE_PIP = 1; 81 /** 82 * State when PIP menu dialog is shown. 83 */ 84 public static final int STATE_PIP_MENU = 2; 85 86 private static final int TASK_ID_NO_PIP = -1; 87 private static final int INVALID_RESOURCE_TYPE = -1; 88 89 public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1; 90 public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_OVERLAY_ACTIVITY_FINISH = 0x2; 91 92 /** 93 * PIPed activity is playing a media and it can be paused. 94 */ 95 static final int PLAYBACK_STATE_PLAYING = 0; 96 /** 97 * PIPed activity has a paused media and it can be played. 98 */ 99 static final int PLAYBACK_STATE_PAUSED = 1; 100 /** 101 * Users are unable to control PIPed activity's media playback. 102 */ 103 static final int PLAYBACK_STATE_UNAVAILABLE = 2; 104 105 private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000; 106 107 private int mSuspendPipResizingReason; 108 109 private Context mContext; 110 private IActivityManager mActivityManager; 111 private IWindowManager mWindowManager; 112 private MediaSessionManager mMediaSessionManager; 113 private int mState = STATE_NO_PIP; 114 private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP; 115 private final Handler mHandler = new Handler(); 116 private List<Listener> mListeners = new ArrayList<>(); 117 private List<MediaListener> mMediaListeners = new ArrayList<>(); 118 private Rect mCurrentPipBounds; 119 private Rect mPipBounds; 120 private Rect mDefaultPipBounds = new Rect(); 121 private Rect mSettingsPipBounds; 122 private Rect mMenuModePipBounds; 123 private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; 124 private boolean mInitialized; 125 private int mPipTaskId = TASK_ID_NO_PIP; 126 private int mPinnedStackId = INVALID_STACK_ID; 127 private ComponentName mPipComponentName; 128 private MediaController mPipMediaController; 129 private String[] mLastPackagesResourceGranted; 130 private PipNotification mPipNotification; 131 private ParceledListSlice mCustomActions; 132 133 // Keeps track of the IME visibility to adjust the PiP when the IME is visible 134 private boolean mImeVisible; 135 private int mImeHeightAdjustment; 136 137 private final PinnedStackListener mPinnedStackListener = new PinnedStackListener(); 138 139 private final Runnable mResizePinnedStackRunnable = new Runnable() { 140 @Override 141 public void run() { 142 resizePinnedStack(mResumeResizePinnedStackRunnableState); 143 } 144 }; 145 private final Runnable mClosePipRunnable = new Runnable() { 146 @Override 147 public void run() { 148 closePip(); 149 } 150 }; 151 152 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 153 @Override 154 public void onReceive(Context context, Intent intent) { 155 String action = intent.getAction(); 156 if (Intent.ACTION_MEDIA_RESOURCE_GRANTED.equals(action)) { 157 String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); 158 int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE, 159 INVALID_RESOURCE_TYPE); 160 if (packageNames != null && packageNames.length > 0 161 && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) { 162 handleMediaResourceGranted(packageNames); 163 } 164 } 165 166 } 167 }; 168 private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener = 169 new MediaSessionManager.OnActiveSessionsChangedListener() { 170 @Override 171 public void onActiveSessionsChanged(List<MediaController> controllers) { 172 updateMediaController(controllers); 173 } 174 }; 175 176 /** 177 * Handler for messages from the PIP controller. 178 */ 179 private class PinnedStackListener extends IPinnedStackListener.Stub { 180 181 @Override onListenerRegistered(IPinnedStackController controller)182 public void onListenerRegistered(IPinnedStackController controller) {} 183 184 @Override onImeVisibilityChanged(boolean imeVisible, int imeHeight)185 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 186 if (mState == STATE_PIP) { 187 if (mImeVisible != imeVisible) { 188 if (imeVisible) { 189 // Save the IME height adjustment, and offset to not occlude the IME 190 mPipBounds.offset(0, -imeHeight); 191 mImeHeightAdjustment = imeHeight; 192 } else { 193 // Apply the inverse adjustment when the IME is hidden 194 mPipBounds.offset(0, mImeHeightAdjustment); 195 } 196 mImeVisible = imeVisible; 197 resizePinnedStack(STATE_PIP); 198 } 199 } 200 } 201 202 @Override onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)203 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {} 204 205 @Override onMinimizedStateChanged(boolean isMinimized)206 public void onMinimizedStateChanged(boolean isMinimized) {} 207 208 @Override onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)209 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, 210 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, 211 int displayRotation) { 212 mHandler.post(() -> { 213 mDefaultPipBounds.set(normalBounds); 214 }); 215 } 216 217 @Override onActionsChanged(ParceledListSlice actions)218 public void onActionsChanged(ParceledListSlice actions) { 219 mCustomActions = actions; 220 mHandler.post(() -> { 221 for (int i = mListeners.size() - 1; i >= 0; --i) { 222 mListeners.get(i).onPipMenuActionsChanged(mCustomActions); 223 } 224 }); 225 } 226 } 227 PipManager()228 private PipManager() { } 229 230 /** 231 * Initializes {@link PipManager}. 232 */ initialize(Context context)233 public void initialize(Context context) { 234 if (mInitialized) { 235 return; 236 } 237 mInitialized = true; 238 mContext = context; 239 240 mActivityManager = ActivityManager.getService(); 241 mWindowManager = WindowManagerGlobal.getWindowManagerService(); 242 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); 243 IntentFilter intentFilter = new IntentFilter(); 244 intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED); 245 mContext.registerReceiver(mBroadcastReceiver, intentFilter); 246 247 if (sSettingsPackageAndClassNamePairList == null) { 248 String[] settings = mContext.getResources().getStringArray( 249 R.array.tv_pip_settings_class_name); 250 sSettingsPackageAndClassNamePairList = new ArrayList<>(); 251 if (settings != null) { 252 for (int i = 0; i < settings.length; i++) { 253 Pair<String, String> entry = null; 254 String[] packageAndClassName = 255 settings[i].split(SETTINGS_PACKAGE_AND_CLASS_DELIMITER); 256 switch (packageAndClassName.length) { 257 case 1: 258 entry = Pair.<String, String>create(packageAndClassName[0], null); 259 break; 260 case 2: 261 if (packageAndClassName[1] != null 262 && packageAndClassName[1].startsWith(".")) { 263 entry = Pair.<String, String>create( 264 packageAndClassName[0], 265 packageAndClassName[0] + packageAndClassName[1]); 266 } 267 } 268 if (entry != null) { 269 sSettingsPackageAndClassNamePairList.add(entry); 270 } else { 271 Log.w(TAG, "Ignoring malformed settings name " + settings[i]); 272 } 273 } 274 } 275 } 276 277 // Initialize the last orientation and apply the current configuration 278 Configuration initialConfig = mContext.getResources().getConfiguration(); 279 mLastOrientation = initialConfig.orientation; 280 loadConfigurationsAndApply(initialConfig); 281 282 mMediaSessionManager = 283 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE); 284 285 try { 286 mWindowManager.registerPinnedStackListener(DEFAULT_DISPLAY, mPinnedStackListener); 287 } catch (RemoteException e) { 288 Log.e(TAG, "Failed to register pinned stack listener", e); 289 } 290 291 mPipNotification = new PipNotification(context); 292 } 293 loadConfigurationsAndApply(Configuration newConfig)294 private void loadConfigurationsAndApply(Configuration newConfig) { 295 if (mLastOrientation != newConfig.orientation) { 296 // Don't resize the pinned stack on orientation change. TV does not care about this case 297 // and this could clobber the existing animation to the new bounds calculated by WM. 298 mLastOrientation = newConfig.orientation; 299 return; 300 } 301 302 Resources res = mContext.getResources(); 303 mSettingsPipBounds = Rect.unflattenFromString(res.getString( 304 R.string.pip_settings_bounds)); 305 mMenuModePipBounds = Rect.unflattenFromString(res.getString( 306 R.string.pip_menu_bounds)); 307 308 // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons. 309 // 1. Configuration changed due to the language change (RTL <-> RTL) 310 // 2. SystemUI restarts after the crash 311 mPipBounds = isSettingsShown() ? mSettingsPipBounds : mDefaultPipBounds; 312 resizePinnedStack(getPinnedStackInfo() == null ? STATE_NO_PIP : STATE_PIP); 313 } 314 315 /** 316 * Updates the PIP per configuration changed. 317 */ onConfigurationChanged(Configuration newConfig)318 public void onConfigurationChanged(Configuration newConfig) { 319 loadConfigurationsAndApply(newConfig); 320 mPipNotification.onConfigurationChanged(mContext); 321 } 322 323 /** 324 * Shows the picture-in-picture menu if an activity is in picture-in-picture mode. 325 */ showPictureInPictureMenu()326 public void showPictureInPictureMenu() { 327 if (getState() == STATE_PIP) { 328 resizePinnedStack(STATE_PIP_MENU); 329 } 330 } 331 332 /** 333 * Closes PIP (PIPed activity and PIP system UI). 334 */ closePip()335 public void closePip() { 336 closePipInternal(true); 337 } 338 closePipInternal(boolean removePipStack)339 private void closePipInternal(boolean removePipStack) { 340 mState = STATE_NO_PIP; 341 mPipTaskId = TASK_ID_NO_PIP; 342 mPipMediaController = null; 343 mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener); 344 if (removePipStack) { 345 try { 346 mActivityManager.removeStack(mPinnedStackId); 347 } catch (RemoteException e) { 348 Log.e(TAG, "removeStack failed", e); 349 } finally { 350 mPinnedStackId = INVALID_STACK_ID; 351 } 352 } 353 for (int i = mListeners.size() - 1; i >= 0; --i) { 354 mListeners.get(i).onPipActivityClosed(); 355 } 356 mHandler.removeCallbacks(mClosePipRunnable); 357 updatePipVisibility(false); 358 } 359 360 /** 361 * Moves the PIPed activity to the fullscreen and closes PIP system UI. 362 */ movePipToFullscreen()363 void movePipToFullscreen() { 364 mPipTaskId = TASK_ID_NO_PIP; 365 for (int i = mListeners.size() - 1; i >= 0; --i) { 366 mListeners.get(i).onMoveToFullscreen(); 367 } 368 resizePinnedStack(STATE_NO_PIP); 369 updatePipVisibility(false); 370 } 371 372 /** 373 * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called 374 * @param reason The reason for suspending resizing operations on the Pip. 375 */ suspendPipResizing(int reason)376 public void suspendPipResizing(int reason) { 377 if (DEBUG) Log.d(TAG, 378 "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); 379 mSuspendPipResizingReason |= reason; 380 } 381 382 /** 383 * Resumes resizing operation on the Pip that was previously suspended. 384 * @param reason The reason resizing operations on the Pip was suspended. 385 */ resumePipResizing(int reason)386 public void resumePipResizing(int reason) { 387 if ((mSuspendPipResizingReason & reason) == 0) { 388 return; 389 } 390 if (DEBUG) Log.d(TAG, 391 "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); 392 mSuspendPipResizingReason &= ~reason; 393 mHandler.post(mResizePinnedStackRunnable); 394 } 395 396 /** 397 * Resize the Pip to the appropriate size for the input state. 398 * @param state In Pip state also used to determine the new size for the Pip. 399 */ resizePinnedStack(int state)400 void resizePinnedStack(int state) { 401 if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state, new Exception()); 402 boolean wasStateNoPip = (mState == STATE_NO_PIP); 403 for (int i = mListeners.size() - 1; i >= 0; --i) { 404 mListeners.get(i).onPipResizeAboutToStart(); 405 } 406 if (mSuspendPipResizingReason != 0) { 407 mResumeResizePinnedStackRunnableState = state; 408 if (DEBUG) Log.d(TAG, "resizePinnedStack() deferring" 409 + " mSuspendPipResizingReason=" + mSuspendPipResizingReason 410 + " mResumeResizePinnedStackRunnableState=" 411 + mResumeResizePinnedStackRunnableState); 412 return; 413 } 414 mState = state; 415 switch (mState) { 416 case STATE_NO_PIP: 417 mCurrentPipBounds = null; 418 // If the state was already STATE_NO_PIP, then do not resize the stack below as it 419 // will not exist 420 if (wasStateNoPip) { 421 return; 422 } 423 break; 424 case STATE_PIP_MENU: 425 mCurrentPipBounds = mMenuModePipBounds; 426 break; 427 case STATE_PIP: 428 mCurrentPipBounds = mPipBounds; 429 break; 430 default: 431 mCurrentPipBounds = mPipBounds; 432 break; 433 } 434 try { 435 int animationDurationMs = -1; 436 mActivityManager.resizeStack(mPinnedStackId, mCurrentPipBounds, 437 true, true, true, animationDurationMs); 438 } catch (RemoteException e) { 439 Log.e(TAG, "resizeStack failed", e); 440 } 441 } 442 443 /** 444 * @return the current state, or the pending state if the state change was previously suspended. 445 */ getState()446 private int getState() { 447 if (mSuspendPipResizingReason != 0) { 448 return mResumeResizePinnedStackRunnableState; 449 } 450 return mState; 451 } 452 453 /** 454 * Returns the default PIP bound. 455 */ getPipBounds()456 public Rect getPipBounds() { 457 return mPipBounds; 458 } 459 460 /** 461 * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned 462 * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}. 463 */ showPipMenu()464 private void showPipMenu() { 465 if (DEBUG) Log.d(TAG, "showPipMenu()"); 466 mState = STATE_PIP_MENU; 467 for (int i = mListeners.size() - 1; i >= 0; --i) { 468 mListeners.get(i).onShowPipMenu(); 469 } 470 Intent intent = new Intent(mContext, PipMenuActivity.class); 471 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 472 intent.putExtra(PipMenuActivity.EXTRA_CUSTOM_ACTIONS, mCustomActions); 473 mContext.startActivity(intent); 474 } 475 476 /** 477 * Adds a {@link Listener} to PipManager. 478 */ addListener(Listener listener)479 public void addListener(Listener listener) { 480 mListeners.add(listener); 481 } 482 483 /** 484 * Removes a {@link Listener} from PipManager. 485 */ removeListener(Listener listener)486 public void removeListener(Listener listener) { 487 mListeners.remove(listener); 488 } 489 490 /** 491 * Adds a {@link MediaListener} to PipManager. 492 */ addMediaListener(MediaListener listener)493 public void addMediaListener(MediaListener listener) { 494 mMediaListeners.add(listener); 495 } 496 497 /** 498 * Removes a {@link MediaListener} from PipManager. 499 */ removeMediaListener(MediaListener listener)500 public void removeMediaListener(MediaListener listener) { 501 mMediaListeners.remove(listener); 502 } 503 504 /** 505 * Returns {@code true} if PIP is shown. 506 */ isPipShown()507 public boolean isPipShown() { 508 return mState != STATE_NO_PIP; 509 } 510 getPinnedStackInfo()511 private StackInfo getPinnedStackInfo() { 512 StackInfo stackInfo = null; 513 try { 514 stackInfo = mActivityManager.getStackInfo( 515 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); 516 } catch (RemoteException e) { 517 Log.e(TAG, "getStackInfo failed", e); 518 } 519 return stackInfo; 520 } 521 handleMediaResourceGranted(String[] packageNames)522 private void handleMediaResourceGranted(String[] packageNames) { 523 if (getState() == STATE_NO_PIP) { 524 mLastPackagesResourceGranted = packageNames; 525 } else { 526 boolean requestedFromLastPackages = false; 527 if (mLastPackagesResourceGranted != null) { 528 for (String packageName : mLastPackagesResourceGranted) { 529 for (String newPackageName : packageNames) { 530 if (TextUtils.equals(newPackageName, packageName)) { 531 requestedFromLastPackages = true; 532 break; 533 } 534 } 535 } 536 } 537 mLastPackagesResourceGranted = packageNames; 538 if (!requestedFromLastPackages) { 539 closePip(); 540 } 541 } 542 } 543 updateMediaController(List<MediaController> controllers)544 private void updateMediaController(List<MediaController> controllers) { 545 MediaController mediaController = null; 546 if (controllers != null && getState() != STATE_NO_PIP && mPipComponentName != null) { 547 for (int i = controllers.size() - 1; i >= 0; i--) { 548 MediaController controller = controllers.get(i); 549 // We assumes that an app with PIPable activity 550 // keeps the single instance of media controller especially when PIP is on. 551 if (controller.getPackageName().equals(mPipComponentName.getPackageName())) { 552 mediaController = controller; 553 break; 554 } 555 } 556 } 557 if (mPipMediaController != mediaController) { 558 mPipMediaController = mediaController; 559 for (int i = mMediaListeners.size() - 1; i >= 0; i--) { 560 mMediaListeners.get(i).onMediaControllerChanged(); 561 } 562 if (mPipMediaController == null) { 563 mHandler.postDelayed(mClosePipRunnable, 564 CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS); 565 } else { 566 mHandler.removeCallbacks(mClosePipRunnable); 567 } 568 } 569 } 570 571 /** 572 * Gets the {@link android.media.session.MediaController} for the PIPed activity. 573 */ getMediaController()574 MediaController getMediaController() { 575 return mPipMediaController; 576 } 577 578 /** 579 * Returns the PIPed activity's playback state. 580 * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, 581 * or {@link #PLAYBACK_STATE_UNAVAILABLE}. 582 */ getPlaybackState()583 int getPlaybackState() { 584 if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) { 585 return PLAYBACK_STATE_UNAVAILABLE; 586 } 587 int state = mPipMediaController.getPlaybackState().getState(); 588 boolean isPlaying = (state == PlaybackState.STATE_BUFFERING 589 || state == PlaybackState.STATE_CONNECTING 590 || state == PlaybackState.STATE_PLAYING 591 || state == PlaybackState.STATE_FAST_FORWARDING 592 || state == PlaybackState.STATE_REWINDING 593 || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS 594 || state == PlaybackState.STATE_SKIPPING_TO_NEXT); 595 long actions = mPipMediaController.getPlaybackState().getActions(); 596 if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { 597 return PLAYBACK_STATE_PAUSED; 598 } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { 599 return PLAYBACK_STATE_PLAYING; 600 } 601 return PLAYBACK_STATE_UNAVAILABLE; 602 } 603 isSettingsShown()604 private boolean isSettingsShown() { 605 List<RunningTaskInfo> runningTasks; 606 try { 607 runningTasks = mActivityManager.getTasks(1); 608 if (runningTasks.isEmpty()) { 609 return false; 610 } 611 } catch (RemoteException e) { 612 Log.d(TAG, "Failed to detect top activity", e); 613 return false; 614 } 615 ComponentName topActivity = runningTasks.get(0).topActivity; 616 for (Pair<String, String> componentName : sSettingsPackageAndClassNamePairList) { 617 String packageName = componentName.first; 618 if (topActivity.getPackageName().equals(packageName)) { 619 String className = componentName.second; 620 if (className == null || topActivity.getClassName().equals(className)) { 621 return true; 622 } 623 } 624 } 625 return false; 626 } 627 628 private SysUiTaskStackChangeListener mTaskStackListener = new SysUiTaskStackChangeListener() { 629 @Override 630 public void onTaskStackChanged() { 631 if (DEBUG) Log.d(TAG, "onTaskStackChanged()"); 632 633 if (getState() != STATE_NO_PIP) { 634 boolean hasPip = false; 635 636 StackInfo stackInfo = getPinnedStackInfo(); 637 if (stackInfo == null || stackInfo.taskIds == null) { 638 Log.w(TAG, "There is nothing in pinned stack"); 639 closePipInternal(false); 640 return; 641 } 642 for (int i = stackInfo.taskIds.length - 1; i >= 0; --i) { 643 if (stackInfo.taskIds[i] == mPipTaskId) { 644 // PIP task is still alive. 645 hasPip = true; 646 break; 647 } 648 } 649 if (!hasPip) { 650 // PIP task doesn't exist anymore in PINNED_STACK. 651 closePipInternal(true); 652 return; 653 } 654 } 655 if (getState() == STATE_PIP) { 656 Rect bounds = isSettingsShown() ? mSettingsPipBounds : mDefaultPipBounds; 657 if (mPipBounds != bounds) { 658 mPipBounds = bounds; 659 resizePinnedStack(STATE_PIP); 660 } 661 } 662 } 663 664 @Override 665 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { 666 if (DEBUG) Log.d(TAG, "onActivityPinned()"); 667 668 StackInfo stackInfo = getPinnedStackInfo(); 669 if (stackInfo == null) { 670 Log.w(TAG, "Cannot find pinned stack"); 671 return; 672 } 673 if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo); 674 mPinnedStackId = stackInfo.stackId; 675 mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1]; 676 mPipComponentName = ComponentName.unflattenFromString( 677 stackInfo.taskNames[stackInfo.taskNames.length - 1]); 678 // Set state to STATE_PIP so we show it when the pinned stack animation ends. 679 mState = STATE_PIP; 680 mCurrentPipBounds = mPipBounds; 681 mMediaSessionManager.addOnActiveSessionsChangedListener( 682 mActiveMediaSessionListener, null); 683 updateMediaController(mMediaSessionManager.getActiveSessions(null)); 684 for (int i = mListeners.size() - 1; i >= 0; i--) { 685 mListeners.get(i).onPipEntered(); 686 } 687 updatePipVisibility(true); 688 } 689 690 @Override 691 public void onPinnedActivityRestartAttempt(boolean clearedTask) { 692 if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()"); 693 694 // If PIPed activity is launched again by Launcher or intent, make it fullscreen. 695 movePipToFullscreen(); 696 } 697 698 @Override 699 public void onPinnedStackAnimationEnded() { 700 if (DEBUG) Log.d(TAG, "onPinnedStackAnimationEnded()"); 701 702 switch (getState()) { 703 case STATE_PIP_MENU: 704 showPipMenu(); 705 break; 706 } 707 } 708 }; 709 710 /** 711 * A listener interface to receive notification on changes in PIP. 712 */ 713 public interface Listener { 714 /** 715 * Invoked when an activity is pinned and PIP manager is set corresponding information. 716 * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned} 717 * because there's no guarantee for the PIP manager be return relavent information 718 * correctly. (e.g. {@link isPipShown}). 719 */ onPipEntered()720 void onPipEntered(); 721 /** Invoked when a PIPed activity is closed. */ onPipActivityClosed()722 void onPipActivityClosed(); 723 /** Invoked when the PIP menu gets shown. */ onShowPipMenu()724 void onShowPipMenu(); 725 /** Invoked when the PIP menu actions change. */ onPipMenuActionsChanged(ParceledListSlice actions)726 void onPipMenuActionsChanged(ParceledListSlice actions); 727 /** Invoked when the PIPed activity is about to return back to the fullscreen. */ onMoveToFullscreen()728 void onMoveToFullscreen(); 729 /** Invoked when we are above to start resizing the Pip. */ onPipResizeAboutToStart()730 void onPipResizeAboutToStart(); 731 } 732 733 /** 734 * A listener interface to receive change in PIP's media controller 735 */ 736 public interface MediaListener { 737 /** Invoked when the MediaController on PIPed activity is changed. */ onMediaControllerChanged()738 void onMediaControllerChanged(); 739 } 740 741 /** 742 * Gets an instance of {@link PipManager}. 743 */ getInstance()744 public static PipManager getInstance() { 745 if (sPipManager == null) { 746 sPipManager = new PipManager(); 747 } 748 return sPipManager; 749 } 750 updatePipVisibility(final boolean visible)751 private void updatePipVisibility(final boolean visible) { 752 SystemServicesProxy.getInstance(mContext).setPipVisibility(visible); 753 } 754 755 @Override dump(PrintWriter pw)756 public void dump(PrintWriter pw) { 757 // Do nothing 758 } 759 } 760