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