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
17package com.android.systemui.tv.pip;
18
19import android.app.ActivityManager.RunningTaskInfo;
20import android.app.ActivityManager.StackInfo;
21import android.app.ActivityManagerNative;
22import android.app.ActivityOptions;
23import android.app.IActivityManager;
24import android.content.BroadcastReceiver;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.res.Resources;
30import android.graphics.Rect;
31import android.media.session.MediaController;
32import android.media.session.MediaSessionManager;
33import android.media.session.PlaybackState;
34import android.os.Debug;
35import android.os.Handler;
36import android.os.RemoteException;
37import android.os.SystemProperties;
38import android.text.TextUtils;
39import android.util.Log;
40import android.util.Pair;
41
42import com.android.systemui.Prefs;
43import com.android.systemui.R;
44import com.android.systemui.SystemUIApplication;
45import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
46import com.android.systemui.recents.misc.SystemServicesProxy;
47import com.android.systemui.statusbar.tv.TvStatusBar;
48
49import java.util.ArrayList;
50import java.util.List;
51
52import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
53import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
54import static com.android.systemui.Prefs.Key.TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN;
55
56/**
57 * Manages the picture-in-picture (PIP) UI and states.
58 */
59public 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);
64
65    private static PipManager sPipManager;
66
67    private static final int MAX_RUNNING_TASKS_COUNT = 10;
68
69    /**
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.
72     */
73    private static final List<Pair<String, String>> sSettingsPackageAndClassNamePairList;
74    static {
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"));
81    }
82
83    /**
84     * State when there's no PIP.
85     */
86    public static final int STATE_NO_PIP = 0;
87    /**
88     * State when PIP is shown with an overlay message on top of it.
89     * This is used as default PIP state.
90     */
91    public static final int STATE_PIP_OVERLAY = 1;
92    /**
93     * State when PIP menu dialog is shown.
94     */
95    public static final int STATE_PIP_MENU = 2;
96    /**
97     * State when PIP is shown in Recents.
98     */
99    public static final int STATE_PIP_RECENTS = 3;
100    /**
101     * State when PIP is shown in Recents and it's focused to allow an user to control.
102     */
103    public static final int STATE_PIP_RECENTS_FOCUSED = 4;
104
105    private static final int TASK_ID_NO_PIP = -1;
106    private static final int INVALID_RESOURCE_TYPE = -1;
107
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;
110
111    /**
112     * PIPed activity is playing a media and it can be paused.
113     */
114    static final int PLAYBACK_STATE_PLAYING = 0;
115    /**
116     * PIPed activity has a paused media and it can be played.
117     */
118    static final int PLAYBACK_STATE_PAUSED = 1;
119    /**
120     * Users are unable to control PIPed activity's media playback.
121     */
122    static final int PLAYBACK_STATE_UNAVAILABLE = 2;
123
124    private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000;
125
126    private int mSuspendPipResizingReason;
127
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;
150
151    private final Runnable mResizePinnedStackRunnable = new Runnable() {
152        @Override
153        public void run() {
154            resizePinnedStack(mState);
155        }
156    };
157    private final Runnable mClosePipRunnable = new Runnable() {
158        @Override
159        public void run() {
160            closePip();
161        }
162    };
163
164    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
165        @Override
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);
175                }
176            }
177
178        }
179    };
180    private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener =
181            new MediaSessionManager.OnActiveSessionsChangedListener() {
182                @Override
183                public void onActiveSessionsChanged(List<MediaController> controllers) {
184                    updateMediaController(controllers);
185                }
186            };
187
188    private PipManager() { }
189
190    /**
191     * Initializes {@link PipManager}.
192     */
193    public void initialize(Context context) {
194        if (mInitialized) {
195            return;
196        }
197        mInitialized = true;
198        mContext = context;
199        Resources res = context.getResources();
200        mDefaultPipBounds = Rect.unflattenFromString(res.getString(
201                com.android.internal.R.string.config_defaultPictureInPictureBounds));
202        mSettingsPipBounds = Rect.unflattenFromString(res.getString(
203                R.string.pip_settings_bounds));
204        mMenuModePipBounds = Rect.unflattenFromString(res.getString(
205                R.string.pip_menu_bounds));
206        mRecentsPipBounds = Rect.unflattenFromString(res.getString(
207                R.string.pip_recents_bounds));
208        mRecentsFocusedPipBounds = Rect.unflattenFromString(res.getString(
209                R.string.pip_recents_focused_bounds));
210        mRecentsFocusChangedAnimationDurationMs = res.getInteger(
211                R.integer.recents_tv_pip_focus_anim_duration);
212        mPipBounds = mDefaultPipBounds;
213
214        mActivityManager = ActivityManagerNative.getDefault();
215        SystemServicesProxy.getInstance(context).registerTaskStackListener(mTaskStackListener);
216        IntentFilter intentFilter = new IntentFilter();
217        intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED);
218        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
219        mOnboardingShown = Prefs.getBoolean(
220                mContext, TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN, false);
221
222        mPipRecentsOverlayManager = new PipRecentsOverlayManager(context);
223        mMediaSessionManager =
224                (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
225    }
226
227    /**
228     * Updates the PIP per configuration changed.
229     */
230    void onConfigurationChanged() {
231        mPipRecentsOverlayManager.onConfigurationChanged(mContext);
232    }
233
234    /**
235     * Shows the picture-in-picture menu if an activity is in picture-in-picture mode.
236     */
237    public void showTvPictureInPictureMenu() {
238        if (mState == STATE_PIP_OVERLAY) {
239            resizePinnedStack(STATE_PIP_MENU);
240        }
241    }
242
243    /**
244     * Closes PIP (PIPed activity and PIP system UI).
245     */
246    public void closePip() {
247        closePipInternal(true);
248    }
249
250    private void closePipInternal(boolean removePipStack) {
251        mState = STATE_NO_PIP;
252        mPipTaskId = TASK_ID_NO_PIP;
253        mPipMediaController = null;
254        mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
255        if (removePipStack) {
256            try {
257                mActivityManager.removeStack(PINNED_STACK_ID);
258            } catch (RemoteException e) {
259                Log.e(TAG, "removeStack failed", e);
260            }
261        }
262        for (int i = mListeners.size() - 1; i >= 0; --i) {
263            mListeners.get(i).onPipActivityClosed();
264        }
265        mHandler.removeCallbacks(mClosePipRunnable);
266        updatePipVisibility(false);
267    }
268
269    /**
270     * Moves the PIPed activity to the fullscreen and closes PIP system UI.
271     */
272    void movePipToFullscreen() {
273        mState = STATE_NO_PIP;
274        mPipTaskId = TASK_ID_NO_PIP;
275        for (int i = mListeners.size() - 1; i >= 0; --i) {
276            mListeners.get(i).onMoveToFullscreen();
277        }
278        resizePinnedStack(mState);
279    }
280
281    /**
282     * Shows PIP overlay UI by launching {@link PipOverlayActivity}. It also locates the pinned
283     * stack to the default PIP bound {@link com.android.internal.R.string
284     * .config_defaultPictureInPictureBounds}.
285     */
286    private void showPipOverlay() {
287        if (DEBUG) Log.d(TAG, "showPipOverlay()");
288        PipOverlayActivity.showPipOverlay(mContext);
289    }
290
291    /**
292     * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called
293     * @param reason The reason for suspending resizing operations on the Pip.
294     */
295    public void suspendPipResizing(int reason) {
296        if (DEBUG) Log.d(TAG,
297                "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
298        mSuspendPipResizingReason |= reason;
299    }
300
301    /**
302     * Resumes resizing operation on the Pip that was previously suspended.
303     * @param reason The reason resizing operations on the Pip was suspended.
304     */
305    public void resumePipResizing(int reason) {
306        if ((mSuspendPipResizingReason & reason) == 0) {
307            return;
308        }
309        if (DEBUG) Log.d(TAG,
310                "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
311        mSuspendPipResizingReason &= ~reason;
312        mHandler.post(mResizePinnedStackRunnable);
313    }
314
315    /**
316     * Resize the Pip to the appropriate size for the input state.
317     * @param state In Pip state also used to determine the new size for the Pip.
318     */
319    void resizePinnedStack(int state) {
320        if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state);
321        boolean wasRecentsShown =
322                (mState == STATE_PIP_RECENTS || mState == STATE_PIP_RECENTS_FOCUSED);
323        mState = state;
324        for (int i = mListeners.size() - 1; i >= 0; --i) {
325            mListeners.get(i).onPipResizeAboutToStart();
326        }
327        if (mSuspendPipResizingReason != 0) {
328            if (DEBUG) Log.d(TAG,
329                    "resizePinnedStack() deferring mSuspendPipResizingReason=" +
330                            mSuspendPipResizingReason);
331            return;
332        }
333        switch (mState) {
334            case STATE_NO_PIP:
335                mCurrentPipBounds = null;
336                break;
337            case STATE_PIP_MENU:
338                mCurrentPipBounds = mMenuModePipBounds;
339                break;
340            case STATE_PIP_OVERLAY:
341                mCurrentPipBounds = mPipBounds;
342                break;
343            case STATE_PIP_RECENTS:
344                mCurrentPipBounds = mRecentsPipBounds;
345                break;
346            case STATE_PIP_RECENTS_FOCUSED:
347                mCurrentPipBounds = mRecentsFocusedPipBounds;
348                break;
349            default:
350                mCurrentPipBounds = mPipBounds;
351                break;
352        }
353        try {
354            int animationDurationMs = -1;
355            if (wasRecentsShown
356                    && (mState == STATE_PIP_RECENTS || mState == STATE_PIP_RECENTS_FOCUSED)) {
357                animationDurationMs = mRecentsFocusChangedAnimationDurationMs;
358            }
359            mActivityManager.resizeStack(PINNED_STACK_ID, mCurrentPipBounds,
360                    true, true, true, animationDurationMs);
361        } catch (RemoteException e) {
362            Log.e(TAG, "resizeStack failed", e);
363        }
364    }
365
366    /**
367     * Returns the default PIP bound.
368     */
369    public Rect getPipBounds() {
370        return mPipBounds;
371    }
372
373    /**
374     * Returns the focused PIP bound while Recents is shown.
375     * This is used to place PIP controls in Recents.
376     */
377    public Rect getRecentsFocusedPipBounds() {
378        return mRecentsFocusedPipBounds;
379    }
380
381    /**
382     * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
383     * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}.
384     */
385    private void showPipMenu() {
386        if (DEBUG) Log.d(TAG, "showPipMenu()");
387        if (mPipRecentsOverlayManager.isRecentsShown()) {
388            if (DEBUG) Log.d(TAG, "Ignore showing PIP menu");
389            return;
390        }
391        mState = STATE_PIP_MENU;
392        for (int i = mListeners.size() - 1; i >= 0; --i) {
393            mListeners.get(i).onShowPipMenu();
394        }
395        Intent intent = new Intent(mContext, PipMenuActivity.class);
396        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
397        mContext.startActivity(intent);
398    }
399
400    /**
401     * Adds a {@link Listener} to PipManager.
402     */
403    public void addListener(Listener listener) {
404        mListeners.add(listener);
405    }
406
407    /**
408     * Removes a {@link Listener} from PipManager.
409     */
410    public void removeListener(Listener listener) {
411        mListeners.remove(listener);
412    }
413
414    /**
415     * Adds a {@link MediaListener} to PipManager.
416     */
417    public void addMediaListener(MediaListener listener) {
418        mMediaListeners.add(listener);
419    }
420
421    /**
422     * Removes a {@link MediaListener} from PipManager.
423     */
424    public void removeMediaListener(MediaListener listener) {
425        mMediaListeners.remove(listener);
426    }
427
428    private void launchPipOnboardingActivityIfNeeded() {
429        if (DEBUG_FORCE_ONBOARDING || !mOnboardingShown) {
430            mOnboardingShown = true;
431            Prefs.putBoolean(mContext, TV_PICTURE_IN_PICTURE_ONBOARDING_SHOWN, true);
432
433            Intent intent = new Intent(mContext, PipOnboardingActivity.class);
434            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
435            mContext.startActivity(intent);
436        }
437    }
438
439    /**
440     * Returns {@code true} if PIP is shown.
441     */
442    public boolean isPipShown() {
443        return mState != STATE_NO_PIP;
444    }
445
446    private void handleMediaResourceGranted(String[] packageNames) {
447        if (mState == STATE_NO_PIP) {
448            mLastPackagesResourceGranted = packageNames;
449        } else {
450            boolean requestedFromLastPackages = false;
451            if (mLastPackagesResourceGranted != null) {
452                for (String packageName : mLastPackagesResourceGranted) {
453                    for (String newPackageName : packageNames) {
454                        if (TextUtils.equals(newPackageName, packageName)) {
455                            requestedFromLastPackages = true;
456                            break;
457                        }
458                    }
459                }
460            }
461            mLastPackagesResourceGranted = packageNames;
462            if (!requestedFromLastPackages) {
463                closePip();
464            }
465        }
466    }
467
468    private void updateMediaController(List<MediaController> controllers) {
469        MediaController mediaController = null;
470        if (controllers != null && mState != STATE_NO_PIP && mPipComponentName != null) {
471            for (int i = controllers.size() - 1; i >= 0; i--) {
472                MediaController controller = controllers.get(i);
473                // We assumes that an app with PIPable activity
474                // keeps the single instance of media controller especially when PIP is on.
475                if (controller.getPackageName().equals(mPipComponentName.getPackageName())) {
476                    mediaController = controller;
477                    break;
478                }
479            }
480        }
481        if (mPipMediaController != mediaController) {
482            mPipMediaController = mediaController;
483            for (int i = mMediaListeners.size() - 1; i >= 0; i--) {
484                mMediaListeners.get(i).onMediaControllerChanged();
485            }
486            if (mPipMediaController == null) {
487                mHandler.postDelayed(mClosePipRunnable,
488                        CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS);
489            } else {
490                mHandler.removeCallbacks(mClosePipRunnable);
491            }
492        }
493    }
494
495    /**
496     * Gets the {@link android.media.session.MediaController} for the PIPed activity.
497     */
498    MediaController getMediaController() {
499        return mPipMediaController;
500    }
501
502    /**
503     * Returns the PIPed activity's playback state.
504     * This returns one of {@link PLAYBACK_STATE_PLAYING}, {@link PLAYBACK_STATE_PAUSED},
505     * or {@link PLAYBACK_STATE_UNAVAILABLE}.
506     */
507    int getPlaybackState() {
508        if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) {
509            return PLAYBACK_STATE_UNAVAILABLE;
510        }
511        int state = mPipMediaController.getPlaybackState().getState();
512        boolean isPlaying = (state == PlaybackState.STATE_BUFFERING
513                || state == PlaybackState.STATE_CONNECTING
514                || state == PlaybackState.STATE_PLAYING
515                || state == PlaybackState.STATE_FAST_FORWARDING
516                || state == PlaybackState.STATE_REWINDING
517                || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
518                || state == PlaybackState.STATE_SKIPPING_TO_NEXT);
519        long actions = mPipMediaController.getPlaybackState().getActions();
520        if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
521            return PLAYBACK_STATE_PAUSED;
522        } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
523            return PLAYBACK_STATE_PLAYING;
524        }
525        return PLAYBACK_STATE_UNAVAILABLE;
526    }
527
528    private static boolean isSettingsShown(ComponentName topActivity) {
529        for (Pair<String, String> componentName : sSettingsPackageAndClassNamePairList) {
530            String packageName = componentName.first;
531            if (topActivity.getPackageName().equals(componentName.first)) {
532                String className = componentName.second;
533                if (className == null || topActivity.getClassName().equals(className)) {
534                    return true;
535                }
536            }
537        }
538        return false;
539    }
540
541    private TaskStackListener mTaskStackListener = new TaskStackListener() {
542        @Override
543        public void onTaskStackChanged() {
544            if (mState != STATE_NO_PIP) {
545                boolean hasPip = false;
546
547                StackInfo stackInfo = null;
548                try {
549                    stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
550                    if (stackInfo == null) {
551                        Log.w(TAG, "There is no pinned stack");
552                        closePipInternal(false);
553                        return;
554                    }
555                } catch (RemoteException e) {
556                    Log.e(TAG, "getStackInfo failed", e);
557                    return;
558                }
559                for (int i = stackInfo.taskIds.length - 1; i >= 0; --i) {
560                    if (stackInfo.taskIds[i] == mPipTaskId) {
561                        // PIP task is still alive.
562                        hasPip = true;
563                        break;
564                    }
565                }
566                if (!hasPip) {
567                    // PIP task doesn't exist anymore in PINNED_STACK.
568                    closePipInternal(true);
569                    return;
570                }
571            }
572            if (mState == STATE_PIP_OVERLAY) {
573                try {
574                    List<RunningTaskInfo> runningTasks = mActivityManager.getTasks(1, 0);
575                    if (runningTasks == null || runningTasks.size() == 0) {
576                        return;
577                    }
578                    RunningTaskInfo topTask = runningTasks.get(0);
579                    Rect bounds = isSettingsShown(topTask.topActivity)
580                          ? mSettingsPipBounds : mDefaultPipBounds;
581                    if (mPipBounds != bounds) {
582                        mPipBounds = bounds;
583                        resizePinnedStack(STATE_PIP_OVERLAY);
584                    }
585                } catch (RemoteException e) {
586                    Log.d(TAG, "Failed to detect top activity", e);
587                }
588            }
589        }
590
591        @Override
592        public void onActivityPinned() {
593            if (DEBUG) Log.d(TAG, "onActivityPinned()");
594            StackInfo stackInfo = null;
595            try {
596                stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
597                if (stackInfo == null) {
598                    Log.w(TAG, "Cannot find pinned stack");
599                    return;
600                }
601            } catch (RemoteException e) {
602                Log.e(TAG, "getStackInfo failed", e);
603                return;
604            }
605            if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo);
606            mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
607            mPipComponentName = ComponentName.unflattenFromString(
608                    stackInfo.taskNames[stackInfo.taskNames.length - 1]);
609            // Set state to overlay so we show it when the pinned stack animation ends.
610            mState = STATE_PIP_OVERLAY;
611            mCurrentPipBounds = mPipBounds;
612            launchPipOnboardingActivityIfNeeded();
613            mMediaSessionManager.addOnActiveSessionsChangedListener(
614                    mActiveMediaSessionListener, null);
615            updateMediaController(mMediaSessionManager.getActiveSessions(null));
616            if (mPipRecentsOverlayManager.isRecentsShown()) {
617                // If an activity becomes PIPed again after the fullscreen, the Recents is shown
618                // behind so we need to resize the pinned stack and show the correct overlay.
619                resizePinnedStack(STATE_PIP_RECENTS);
620            }
621            for (int i = mListeners.size() - 1; i >= 0; i--) {
622                mListeners.get(i).onPipEntered();
623            }
624            updatePipVisibility(true);
625        }
626
627        @Override
628        public void onPinnedActivityRestartAttempt() {
629            if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
630            // If PIPed activity is launched again by Launcher or intent, make it fullscreen.
631            movePipToFullscreen();
632        }
633
634        @Override
635        public void onPinnedStackAnimationEnded() {
636            if (DEBUG) Log.d(TAG, "onPinnedStackAnimationEnded()");
637            switch (mState) {
638                case STATE_PIP_OVERLAY:
639                    if (!mPipRecentsOverlayManager.isRecentsShown()) {
640                        showPipOverlay();
641                        break;
642                    } else {
643                        // This happens only if an activity is PIPed after the Recents is shown.
644                        // See {@link PipRecentsOverlayManager.requestFocus} for more details.
645                        resizePinnedStack(mState);
646                        break;
647                    }
648                case STATE_PIP_RECENTS:
649                case STATE_PIP_RECENTS_FOCUSED:
650                    mPipRecentsOverlayManager.addPipRecentsOverlayView();
651                    break;
652                case STATE_PIP_MENU:
653                    showPipMenu();
654                    break;
655            }
656        }
657    };
658
659    /**
660     * A listener interface to receive notification on changes in PIP.
661     */
662    public interface Listener {
663        /**
664         * Invoked when an activity is pinned and PIP manager is set corresponding information.
665         * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned}
666         * because there's no guarantee for the PIP manager be return relavent information
667         * correctly. (e.g. {@link isPipShown}).
668         */
669        void onPipEntered();
670        /** Invoked when a PIPed activity is closed. */
671        void onPipActivityClosed();
672        /** Invoked when the PIP menu gets shown. */
673        void onShowPipMenu();
674        /** Invoked when the PIPed activity is about to return back to the fullscreen. */
675        void onMoveToFullscreen();
676        /** Invoked when we are above to start resizing the Pip. */
677        void onPipResizeAboutToStart();
678    }
679
680    /**
681     * A listener interface to receive change in PIP's media controller
682     */
683    public interface MediaListener {
684        /** Invoked when the MediaController on PIPed activity is changed. */
685        void onMediaControllerChanged();
686    }
687
688    /**
689     * Gets an instance of {@link PipManager}.
690     */
691    public static PipManager getInstance() {
692        if (sPipManager == null) {
693            sPipManager = new PipManager();
694        }
695        return sPipManager;
696    }
697
698    /**
699     * Gets an instance of {@link PipRecentsOverlayManager}.
700     */
701    public PipRecentsOverlayManager getPipRecentsOverlayManager() {
702        return mPipRecentsOverlayManager;
703    }
704
705    private void updatePipVisibility(boolean visible) {
706        TvStatusBar statusBar = ((SystemUIApplication) mContext).getComponent(TvStatusBar.class);
707        if (statusBar != null) {
708            statusBar.updatePipVisibility(visible);
709        }
710    }
711}
712