2 * Copyright (C) 2016 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.systemui.tv.pip;
19 import android.app.ActivityManager.RunningTaskInfo;
20 import android.app.ActivityManager.StackInfo;
21 import android.app.ActivityManagerNative;
22 import android.app.ActivityOptions;
23 import android.app.IActivityManager;
24 import android.content.BroadcastReceiver;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.res.Resources;
30 import android.graphics.Rect;
31 import android.media.session.MediaController;
32 import android.media.session.MediaSessionManager;
33 import android.media.session.PlaybackState;
34 import android.os.Debug;
35 import android.os.Handler;
36 import android.os.RemoteException;
37 import android.os.SystemProperties;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import android.util.Pair;
42 import com.android.systemui.Prefs;
43 import com.android.systemui.R;
44 import com.android.systemui.SystemUIApplication;
45 import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
46 import com.android.systemui.recents.misc.SystemServicesProxy;
47 import com.android.systemui.statusbar.tv.TvStatusBar;
49 import java.util.ArrayList;
50 import java.util.List;
52 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
53 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
54 import static com.android.systemui.Prefs.Key.TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN;
57 * Manages the picture-in-picture (PIP) UI and states.
59 public class PipManager {
60 private static final String TAG = "PipManager";
61 private static final boolean DEBUG = false;
62 private static final boolean DEBUG_FORCE_ONBOARDING =
63 SystemProperties.getBoolean("debug.tv.pip_force_onboarding", false);
65 private static PipManager sPipManager;
67 private static final int MAX_RUNNING_TASKS_COUNT = 10;
70 * List of package and class name which are considered as Settings,
71 * so PIP location should be adjusted to the left of the side panel.
73 private static final List<Pair<String, String>> sSettingsPackageAndClassNamePairList;
75 sSettingsPackageAndClassNamePairList = new ArrayList<>();
76 sSettingsPackageAndClassNamePairList.add(new Pair<String, String>(
77 "com.android.tv.settings", null));
78 sSettingsPackageAndClassNamePairList.add(new Pair<String, String>(
79 "com.google.android.leanbacklauncher",
80 "com.google.android.leanbacklauncher.settings.HomeScreenSettingsActivity"));
84 * State when there's no PIP.
86 public static final int STATE_NO_PIP = 0;
88 * State when PIP is shown with an overlay message on top of it.
89 * This is used as default PIP state.
91 public static final int STATE_PIP_OVERLAY = 1;
93 * State when PIP menu dialog is shown.
95 public static final int STATE_PIP_MENU = 2;
97 * State when PIP is shown in Recents.
99 public static final int STATE_PIP_RECENTS = 3;
101 * State when PIP is shown in Recents and it's focused to allow an user to control.
103 public static final int STATE_PIP_RECENTS_FOCUSED = 4;
105 private static final int TASK_ID_NO_PIP = -1;
106 private static final int INVALID_RESOURCE_TYPE = -1;
108 public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1;
109 public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_OVERLAY_ACTIVITY_FINISH = 0x2;
112 * PIPed activity is playing a media and it can be paused.
114 static final int PLAYBACK_STATE_PLAYING = 0;
116 * PIPed activity has a paused media and it can be played.
118 static final int PLAYBACK_STATE_PAUSED = 1;
120 * Users are unable to control PIPed activity's media playback.
122 static final int PLAYBACK_STATE_UNAVAILABLE = 2;
124 private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000;
126 private int mSuspendPipResizingReason;
128 private Context mContext;
129 private PipRecentsOverlayManager mPipRecentsOverlayManager;
130 private IActivityManager mActivityManager;
131 private MediaSessionManager mMediaSessionManager;
132 private int mState = STATE_NO_PIP;
133 private final Handler mHandler = new Handler();
134 private List<Listener> mListeners = new ArrayList<>();
135 private List<MediaListener> mMediaListeners = new ArrayList<>();
136 private Rect mCurrentPipBounds;
137 private Rect mPipBounds;
138 private Rect mDefaultPipBounds;
139 private Rect mSettingsPipBounds;
140 private Rect mMenuModePipBounds;
141 private Rect mRecentsPipBounds;
142 private Rect mRecentsFocusedPipBounds;
143 private int mRecentsFocusChangedAnimationDurationMs;
144 private boolean mInitialized;
145 private int mPipTaskId = TASK_ID_NO_PIP;
146 private ComponentName mPipComponentName;
147 private MediaController mPipMediaController;
148 private boolean mOnboardingShown;
149 private String[] mLastPackagesResourceGranted;
151 private final Runnable mResizePinnedStackRunnable = new Runnable() {
154 resizePinnedStack(mState);
157 private final Runnable mClosePipRunnable = new Runnable() {
164 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
166 public void onReceive(Context context, Intent intent) {
167 String action = intent.getAction();
168 if (Intent.ACTION_MEDIA_RESOURCE_GRANTED.equals(action)) {
169 String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
170 int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE,
171 INVALID_RESOURCE_TYPE);
172 if (packageNames != null && packageNames.length > 0
173 && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) {
174 handleMediaResourceGranted(packageNames);
180 private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener =
181 new MediaSessionManager.OnActiveSessionsChangedListener() {
183 public void onActiveSessionsChanged(List<MediaController> controllers) {
184 updateMediaController(controllers);
188 private PipManager() { }
191 * Initializes {@link PipManager}.
193 public void initialize(Context context) {
200 mActivityManager = ActivityManagerNative.getDefault();
201 SystemServicesProxy.getInstance(context).registerTaskStackListener(mTaskStackListener);
202 IntentFilter intentFilter = new IntentFilter();
203 intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED);
204 mContext.registerReceiver(mBroadcastReceiver, intentFilter);
205 mOnboardingShown = Prefs.getBoolean(
206 mContext, TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN, false);
208 loadConfigurationsAndApply();
209 mPipRecentsOverlayManager = new PipRecentsOverlayManager(context);
210 mMediaSessionManager =
211 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
214 private void loadConfigurationsAndApply() {
215 Resources res = mContext.getResources();
216 mDefaultPipBounds = Rect.unflattenFromString(res.getString(
217 com.android.internal.R.string.config_defaultPictureInPictureBounds));
218 mSettingsPipBounds = Rect.unflattenFromString(res.getString(
219 R.string.pip_settings_bounds));
220 mMenuModePipBounds = Rect.unflattenFromString(res.getString(
221 R.string.pip_menu_bounds));
222 mRecentsPipBounds = Rect.unflattenFromString(res.getString(
223 R.string.pip_recents_bounds));
224 mRecentsFocusedPipBounds = Rect.unflattenFromString(res.getString(
225 R.string.pip_recents_focused_bounds));
226 mRecentsFocusChangedAnimationDurationMs = res.getInteger(
227 R.integer.recents_tv_pip_focus_anim_duration);
229 // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons.
230 // 1. Configuration changed due to the language change (RTL <-> RTL)
231 // 2. SystemUI restarts after the crash
232 mPipBounds = isSettingsShown() ? mSettingsPipBounds : mDefaultPipBounds;
233 resizePinnedStack(getPinnedStackInfo() == null ? STATE_NO_PIP : STATE_PIP_OVERLAY);
237 * Updates the PIP per configuration changed.
239 void onConfigurationChanged() {
240 loadConfigurationsAndApply();
241 mPipRecentsOverlayManager.onConfigurationChanged(mContext);
245 * Shows the picture-in-picture menu if an activity is in picture-in-picture mode.
247 public void showTvPictureInPictureMenu() {
248 if (mState == STATE_PIP_OVERLAY) {
249 resizePinnedStack(STATE_PIP_MENU);
254 * Closes PIP (PIPed activity and PIP system UI).
256 public void closePip() {
257 closePipInternal(true);
260 private void closePipInternal(boolean removePipStack) {
261 mState = STATE_NO_PIP;
262 mPipTaskId = TASK_ID_NO_PIP;
263 mPipMediaController = null;
264 mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
265 if (removePipStack) {
267 mActivityManager.removeStack(PINNED_STACK_ID);
268 } catch (RemoteException e) {
269 Log.e(TAG, "removeStack failed", e);
272 for (int i = mListeners.size() - 1; i >= 0; --i) {
273 mListeners.get(i).onPipActivityClosed();
275 mHandler.removeCallbacks(mClosePipRunnable);
276 updatePipVisibility(false);
280 * Moves the PIPed activity to the fullscreen and closes PIP system UI.
282 void movePipToFullscreen() {
283 mState = STATE_NO_PIP;
284 mPipTaskId = TASK_ID_NO_PIP;
285 for (int i = mListeners.size() - 1; i >= 0; --i) {
286 mListeners.get(i).onMoveToFullscreen();
288 resizePinnedStack(mState);
292 * Shows PIP overlay UI by launching {@link PipOverlayActivity}. It also locates the pinned
293 * stack to the default PIP bound {@link com.android.internal.R.string
294 * .config_defaultPictureInPictureBounds}.
296 private void showPipOverlay() {
297 if (DEBUG) Log.d(TAG, "showPipOverlay()");
298 PipOverlayActivity.showPipOverlay(mContext);
302 * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called
303 * @param reason The reason for suspending resizing operations on the Pip.
305 public void suspendPipResizing(int reason) {
306 if (DEBUG) Log.d(TAG,
307 "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
308 mSuspendPipResizingReason |= reason;
312 * Resumes resizing operation on the Pip that was previously suspended.
313 * @param reason The reason resizing operations on the Pip was suspended.
315 public void resumePipResizing(int reason) {
316 if ((mSuspendPipResizingReason & reason) == 0) {
319 if (DEBUG) Log.d(TAG,
320 "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
321 mSuspendPipResizingReason &= ~reason;
322 mHandler.post(mResizePinnedStackRunnable);
326 * Resize the Pip to the appropriate size for the input state.
327 * @param state In Pip state also used to determine the new size for the Pip.
329 void resizePinnedStack(int state) {
330 if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state);
331 boolean wasRecentsShown =
332 (mState == STATE_PIP_RECENTS || mState == STATE_PIP_RECENTS_FOCUSED);
334 for (int i = mListeners.size() - 1; i >= 0; --i) {
335 mListeners.get(i).onPipResizeAboutToStart();
337 if (mSuspendPipResizingReason != 0) {
338 if (DEBUG) Log.d(TAG,
339 "resizePinnedStack() deferring mSuspendPipResizingReason=" +
340 mSuspendPipResizingReason);
345 mCurrentPipBounds = null;
348 mCurrentPipBounds = mMenuModePipBounds;
350 case STATE_PIP_OVERLAY:
351 mCurrentPipBounds = mPipBounds;
353 case STATE_PIP_RECENTS:
354 mCurrentPipBounds = mRecentsPipBounds;
356 case STATE_PIP_RECENTS_FOCUSED:
357 mCurrentPipBounds = mRecentsFocusedPipBounds;
360 mCurrentPipBounds = mPipBounds;
364 int animationDurationMs = -1;
366 && (mState == STATE_PIP_RECENTS || mState == STATE_PIP_RECENTS_FOCUSED)) {
367 animationDurationMs = mRecentsFocusChangedAnimationDurationMs;
369 mActivityManager.resizeStack(PINNED_STACK_ID, mCurrentPipBounds,
370 true, true, true, animationDurationMs);
371 } catch (RemoteException e) {
372 Log.e(TAG, "resizeStack failed", e);
377 * Returns the default PIP bound.
379 public Rect getPipBounds() {
384 * Returns the focused PIP bound while Recents is shown.
385 * This is used to place PIP controls in Recents.
387 public Rect getRecentsFocusedPipBounds() {
388 return mRecentsFocusedPipBounds;
392 * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
393 * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}.
395 private void showPipMenu() {
396 if (DEBUG) Log.d(TAG, "showPipMenu()");
397 if (mPipRecentsOverlayManager.isRecentsShown()) {
398 if (DEBUG) Log.d(TAG, "Ignore showing PIP menu");
401 mState = STATE_PIP_MENU;
402 for (int i = mListeners.size() - 1; i >= 0; --i) {
403 mListeners.get(i).onShowPipMenu();
405 Intent intent = new Intent(mContext, PipMenuActivity.class);
406 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
407 mContext.startActivity(intent);
411 * Adds a {@link Listener} to PipManager.
413 public void addListener(Listener listener) {
414 mListeners.add(listener);
418 * Removes a {@link Listener} from PipManager.
420 public void removeListener(Listener listener) {
421 mListeners.remove(listener);
425 * Adds a {@link MediaListener} to PipManager.
427 public void addMediaListener(MediaListener listener) {
428 mMediaListeners.add(listener);
432 * Removes a {@link MediaListener} from PipManager.
434 public void removeMediaListener(MediaListener listener) {
435 mMediaListeners.remove(listener);
438 private void launchPipOnboardingActivityIfNeeded() {
439 if (DEBUG_FORCE_ONBOARDING || !mOnboardingShown) {
440 mOnboardingShown = true;
441 Prefs.putBoolean(mContext, TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN, true);
443 Intent intent = new Intent(mContext, PipOnboardingActivity.class);
444 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
445 mContext.startActivity(intent);
450 * Returns {@code true} if PIP is shown.
452 public boolean isPipShown() {
453 return mState != STATE_NO_PIP;
456 private StackInfo getPinnedStackInfo() {
457 StackInfo stackInfo = null;
459 stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
460 } catch (RemoteException e) {
461 Log.e(TAG, "getStackInfo failed", e);
466 private void handleMediaResourceGranted(String[] packageNames) {
467 if (mState == STATE_NO_PIP) {
468 mLastPackagesResourceGranted = packageNames;
470 boolean requestedFromLastPackages = false;
471 if (mLastPackagesResourceGranted != null) {
472 for (String packageName : mLastPackagesResourceGranted) {
473 for (String newPackageName : packageNames) {
474 if (TextUtils.equals(newPackageName, packageName)) {
475 requestedFromLastPackages = true;
481 mLastPackagesResourceGranted = packageNames;
482 if (!requestedFromLastPackages) {
488 private void updateMediaController(List<MediaController> controllers) {
489 MediaController mediaController = null;
490 if (controllers != null && mState != STATE_NO_PIP && mPipComponentName != null) {
491 for (int i = controllers.size() - 1; i >= 0; i--) {
492 MediaController controller = controllers.get(i);
493 // We assumes that an app with PIPable activity
494 // keeps the single instance of media controller especially when PIP is on.
495 if (controller.getPackageName().equals(mPipComponentName.getPackageName())) {
496 mediaController = controller;
501 if (mPipMediaController != mediaController) {
502 mPipMediaController = mediaController;
503 for (int i = mMediaListeners.size() - 1; i >= 0; i--) {
504 mMediaListeners.get(i).onMediaControllerChanged();
506 if (mPipMediaController == null) {
507 mHandler.postDelayed(mClosePipRunnable,
508 CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS);
510 mHandler.removeCallbacks(mClosePipRunnable);
516 * Gets the {@link android.media.session.MediaController} for the PIPed activity.
518 MediaController getMediaController() {
519 return mPipMediaController;
523 * Returns the PIPed activity's playback state.
524 * This returns one of {@link PLAYBACK_STATE_PLAYING}, {@link PLAYBACK_STATE_PAUSED},
525 * or {@link PLAYBACK_STATE_UNAVAILABLE}.
527 int getPlaybackState() {
528 if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) {
529 return PLAYBACK_STATE_UNAVAILABLE;
531 int state = mPipMediaController.getPlaybackState().getState();
532 boolean isPlaying = (state == PlaybackState.STATE_BUFFERING
533 || state == PlaybackState.STATE_CONNECTING
534 || state == PlaybackState.STATE_PLAYING
535 || state == PlaybackState.STATE_FAST_FORWARDING
536 || state == PlaybackState.STATE_REWINDING
537 || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
538 || state == PlaybackState.STATE_SKIPPING_TO_NEXT);
539 long actions = mPipMediaController.getPlaybackState().getActions();
540 if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
541 return PLAYBACK_STATE_PAUSED;
542 } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
543 return PLAYBACK_STATE_PLAYING;
545 return PLAYBACK_STATE_UNAVAILABLE;
548 private boolean isSettingsShown() {
549 List<RunningTaskInfo> runningTasks;
551 runningTasks = mActivityManager.getTasks(1, 0);
552 if (runningTasks == null || runningTasks.size() == 0) {
555 } catch (RemoteException e) {
556 Log.d(TAG, "Failed to detect top activity", e);
559 ComponentName topActivity = runningTasks.get(0).topActivity;
560 for (Pair<String, String> componentName : sSettingsPackageAndClassNamePairList) {
561 String packageName = componentName.first;
562 if (topActivity.getPackageName().equals(packageName)) {
563 String className = componentName.second;
564 if (className == null || topActivity.getClassName().equals(className)) {
572 private TaskStackListener mTaskStackListener = new TaskStackListener() {
574 public void onTaskStackChanged() {
575 if (mState != STATE_NO_PIP) {
576 boolean hasPip = false;
578 StackInfo stackInfo = getPinnedStackInfo();
579 if (stackInfo == null || stackInfo.taskIds == null) {
580 Log.w(TAG, "There is nothing in pinned stack");
581 closePipInternal(false);
584 for (int i = stackInfo.taskIds.length - 1; i >= 0; --i) {
585 if (stackInfo.taskIds[i] == mPipTaskId) {
586 // PIP task is still alive.
592 // PIP task doesn't exist anymore in PINNED_STACK.
593 closePipInternal(true);
597 if (mState == STATE_PIP_OVERLAY) {
598 Rect bounds = isSettingsShown() ? mSettingsPipBounds : mDefaultPipBounds;
599 if (mPipBounds != bounds) {
601 resizePinnedStack(STATE_PIP_OVERLAY);
607 public void onActivityPinned() {
608 if (DEBUG) Log.d(TAG, "onActivityPinned()");
609 StackInfo stackInfo = getPinnedStackInfo();
610 if (stackInfo == null) {
611 Log.w(TAG, "Cannot find pinned stack");
614 if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo);
615 mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
616 mPipComponentName = ComponentName.unflattenFromString(
617 stackInfo.taskNames[stackInfo.taskNames.length - 1]);
618 // Set state to overlay so we show it when the pinned stack animation ends.
619 mState = STATE_PIP_OVERLAY;
620 mCurrentPipBounds = mPipBounds;
621 launchPipOnboardingActivityIfNeeded();
622 mMediaSessionManager.addOnActiveSessionsChangedListener(
623 mActiveMediaSessionListener, null);
624 updateMediaController(mMediaSessionManager.getActiveSessions(null));
625 if (mPipRecentsOverlayManager.isRecentsShown()) {
626 // If an activity becomes PIPed again after the fullscreen, the Recents is shown
627 // behind so we need to resize the pinned stack and show the correct overlay.
628 resizePinnedStack(STATE_PIP_RECENTS);
630 for (int i = mListeners.size() - 1; i >= 0; i--) {
631 mListeners.get(i).onPipEntered();
633 updatePipVisibility(true);
637 public void onPinnedActivityRestartAttempt() {
638 if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
639 // If PIPed activity is launched again by Launcher or intent, make it fullscreen.
640 movePipToFullscreen();
644 public void onPinnedStackAnimationEnded() {
645 if (DEBUG) Log.d(TAG, "onPinnedStackAnimationEnded()");
647 case STATE_PIP_OVERLAY:
648 if (!mPipRecentsOverlayManager.isRecentsShown()) {
652 // This happens only if an activity is PIPed after the Recents is shown.
653 // See {@link PipRecentsOverlayManager.requestFocus} for more details.
654 resizePinnedStack(mState);
657 case STATE_PIP_RECENTS:
658 case STATE_PIP_RECENTS_FOCUSED:
659 mPipRecentsOverlayManager.addPipRecentsOverlayView();
669 * A listener interface to receive notification on changes in PIP.
671 public interface Listener {
673 * Invoked when an activity is pinned and PIP manager is set corresponding information.
674 * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned}
675 * because there's no guarantee for the PIP manager be return relavent information
676 * correctly. (e.g. {@link isPipShown}).
679 /** Invoked when a PIPed activity is closed. */
680 void onPipActivityClosed();
681 /** Invoked when the PIP menu gets shown. */
682 void onShowPipMenu();
683 /** Invoked when the PIPed activity is about to return back to the fullscreen. */
684 void onMoveToFullscreen();
685 /** Invoked when we are above to start resizing the Pip. */
686 void onPipResizeAboutToStart();
690 * A listener interface to receive change in PIP's media controller
692 public interface MediaListener {
693 /** Invoked when the MediaController on PIPed activity is changed. */
694 void onMediaControllerChanged();
698 * Gets an instance of {@link PipManager}.
700 public static PipManager getInstance() {
701 if (sPipManager == null) {
702 sPipManager = new PipManager();
708 * Gets an instance of {@link PipRecentsOverlayManager}.
710 public PipRecentsOverlayManager getPipRecentsOverlayManager() {
711 return mPipRecentsOverlayManager;
714 private void updatePipVisibility(boolean visible) {
715 TvStatusBar statusBar = ((SystemUIApplication) mContext).getComponent(TvStatusBar.class);
716 if (statusBar != null) {
717 statusBar.updatePipVisibility(visible);