PipManager.java revision c552b04cb4aac9d31dbaf9744f32ddc14886e222
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.StackInfo;
20import android.app.ActivityManagerNative;
21import android.app.ActivityOptions;
22import android.app.IActivityManager;
23import android.app.ITaskStackListener;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.res.Resources;
29import android.graphics.Rect;
30import android.os.Handler;
31import android.os.RemoteException;
32import android.util.Log;
33
34import java.util.ArrayList;
35import java.util.List;
36
37import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
38import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
39
40import android.app.ActivityManager.RunningTaskInfo;
41
42/**
43 * Manages the picture-in-picture (PIP) UI and states.
44 */
45public class PipManager {
46    private static final String TAG = "PipManager";
47    private static final boolean DEBUG = false;
48
49    private static PipManager sPipManager;
50
51    private static final int MAX_RUNNING_TASKS_COUNT = 10;
52
53    private static final int STATE_NO_PIP = 0;
54    private static final int STATE_PIP_OVERLAY = 1;
55    private static final int STATE_PIP_MENU = 2;
56
57    private static final int TASK_ID_NO_PIP = -1;
58    private static final int INVALID_RESOURCE_TYPE = -1;
59
60    private Context mContext;
61    private IActivityManager mActivityManager;
62    private int mState = STATE_NO_PIP;
63    private final Handler mHandler = new Handler();
64    private List<Listener> mListeners = new ArrayList<>();
65    private Rect mPipBound;
66    private Rect mMenuModePipBound;
67    private boolean mInitialized;
68    private int mPipTaskId = TASK_ID_NO_PIP;
69
70    private final Runnable mOnActivityPinnedRunnable = new Runnable() {
71        @Override
72        public void run() {
73            StackInfo stackInfo = null;
74            try {
75                stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
76                if (stackInfo == null) {
77                    Log.w(TAG, "There is no pinned stack");
78                    return;
79                }
80            } catch (RemoteException e) {
81                Log.e(TAG, "getStackInfo failed", e);
82                return;
83            }
84            if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo);
85            mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
86            showPipOverlay(false);
87        }
88    };
89    private final Runnable mOnTaskStackChanged = new Runnable() {
90        @Override
91        public void run() {
92            if (mState != STATE_NO_PIP) {
93                // TODO: check whether PIP task is closed.
94            }
95        }
96    };
97    private final Runnable mOnPinnedActivityRestartAttempt = new Runnable() {
98        @Override
99        public void run() {
100            movePipToFullscreen();
101        }
102    };
103
104    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
105        @Override
106        public void onReceive(Context context, Intent intent) {
107            String action = intent.getAction();
108            if (Intent.ACTION_MEDIA_RESOURCE_GRANTED.equals(action)) {
109                String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
110                int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE,
111                        INVALID_RESOURCE_TYPE);
112                if (mState != STATE_NO_PIP && packageNames != null && packageNames.length > 0
113                        && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) {
114                    handleMediaResourceGranted(packageNames);
115                }
116            }
117
118        }
119    };
120
121    private PipManager() { }
122
123    /**
124     * Initializes {@link PipManager}.
125     */
126    public void initialize(Context context) {
127        if (mInitialized) {
128            return;
129        }
130        mInitialized = true;
131        mContext = context;
132        Resources res = context.getResources();
133        mPipBound = Rect.unflattenFromString(res.getString(
134                com.android.internal.R.string.config_defaultPictureInPictureBounds));
135        mMenuModePipBound = Rect.unflattenFromString(res.getString(
136                com.android.internal.R.string.config_centeredPictureInPictureBounds));
137
138        mActivityManager = ActivityManagerNative.getDefault();
139        TaskStackListener taskStackListener = new TaskStackListener();
140        IActivityManager iam = ActivityManagerNative.getDefault();
141        try {
142            iam.registerTaskStackListener(taskStackListener);
143        } catch (RemoteException e) {
144            Log.e(TAG, "registerTaskStackListener failed", e);
145        }
146        IntentFilter intentFilter = new IntentFilter();
147        intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED);
148        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
149    }
150
151    /**
152     * Request PIP.
153     * It could either start PIP if there's none, and show PIP menu otherwise.
154     */
155    public void requestTvPictureInPicture() {
156        if (DEBUG) Log.d(TAG, "requestTvPictureInPicture()");
157        if (!hasPipTasks()) {
158            startPip();
159        } else if (mState == STATE_PIP_OVERLAY) {
160            showPipMenu();
161        }
162    }
163
164    private void startPip() {
165        try {
166            mActivityManager.moveTopActivityToPinnedStack(FULLSCREEN_WORKSPACE_STACK_ID, mPipBound);
167        } catch (RemoteException|IllegalArgumentException e) {
168            Log.e(TAG, "moveTopActivityToPinnedStack failed", e);
169        }
170    }
171
172    /**
173     * Closes PIP (PIPped activity and PIP system UI).
174     */
175    public void closePip() {
176        mState = STATE_NO_PIP;
177        mPipTaskId = TASK_ID_NO_PIP;
178        StackInfo stackInfo = null;
179        try {
180            stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
181            if (stackInfo == null) {
182                return;
183            }
184        } catch (RemoteException e) {
185            Log.e(TAG, "getStackInfo failed", e);
186            return;
187        }
188        for (int taskId : stackInfo.taskIds) {
189            try {
190                mActivityManager.removeTask(taskId);
191            } catch (RemoteException e) {
192                Log.e(TAG, "removeTask failed", e);
193            }
194        }
195    }
196
197    /**
198     * Moves the PIPped activity to the fullscreen and closes PIP system UI.
199     */
200    public void movePipToFullscreen() {
201        mState = STATE_NO_PIP;
202        mPipTaskId = TASK_ID_NO_PIP;
203        for (int i = mListeners.size() - 1; i >= 0; --i) {
204            mListeners.get(i).onMoveToFullscreen();
205        }
206        try {
207            mActivityManager.moveTasksToFullscreenStack(PINNED_STACK_ID, true);
208        } catch (RemoteException e) {
209            Log.e(TAG, "moveTasksToFullscreenStack failed", e);
210        }
211    }
212
213    /**
214     * Shows PIP overlay UI by launching {@link PipOverlayActivity}. It also locates the pinned
215     * stack to the default PIP bound {@link com.android.internal.R.string
216     * .config_defaultPictureInPictureBounds}.
217     */
218    public void showPipOverlay(boolean resizeStack) {
219        if (DEBUG) Log.d(TAG, "showPipOverlay()");
220        mState = STATE_PIP_OVERLAY;
221        Intent intent = new Intent(mContext, PipOverlayActivity.class);
222        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
223        final ActivityOptions options = ActivityOptions.makeBasic();
224        options.setLaunchStackId(PINNED_STACK_ID);
225        if (resizeStack) {
226            options.setLaunchBounds(mPipBound);
227        }
228        mContext.startActivity(intent, options.toBundle());
229    }
230
231    /**
232     * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
233     * stack to the centered PIP bound {@link com.android.internal.R.string
234     * .config_centeredPictureInPictureBounds}.
235     */
236    public void showPipMenu() {
237        if (DEBUG) Log.d(TAG, "showPipMenu()");
238        mState = STATE_PIP_MENU;
239        for (int i = mListeners.size() - 1; i >= 0; --i) {
240            mListeners.get(i).onShowPipMenu();
241        }
242        Intent intent = new Intent(mContext, PipMenuActivity.class);
243        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
244        final ActivityOptions options = ActivityOptions.makeBasic();
245        options.setLaunchStackId(PINNED_STACK_ID);
246        options.setLaunchBounds(mMenuModePipBound);
247        mContext.startActivity(intent, options.toBundle());
248    }
249
250    /**
251     * Adds {@link Listener}.
252     */
253    public void addListener(Listener listener) {
254        mListeners.add(listener);
255    }
256
257    /**
258     * Removes {@link Listener}.
259     */
260    public void removeListener(Listener listener) {
261        mListeners.remove(listener);
262    }
263
264    private boolean hasPipTasks() {
265        try {
266            StackInfo stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
267            return stackInfo != null;
268        } catch (RemoteException e) {
269            Log.e(TAG, "getStackInfo failed", e);
270            return false;
271        }
272    }
273
274    private void handleMediaResourceGranted(String[] packageNames) {
275        StackInfo fullscreenStack = null;
276        try {
277            fullscreenStack = mActivityManager.getStackInfo(FULLSCREEN_WORKSPACE_STACK_ID);
278        } catch (RemoteException e) {
279            Log.e(TAG, "getStackInfo failed", e);
280        }
281        if (fullscreenStack == null) {
282            return;
283        }
284        int fullscreenTopTaskId = fullscreenStack.taskIds[fullscreenStack.taskIds.length - 1];
285        List<RunningTaskInfo> tasks = null;
286        try {
287            tasks = mActivityManager.getTasks(MAX_RUNNING_TASKS_COUNT, 0);
288        } catch (RemoteException e) {
289            Log.e(TAG, "getTasks failed", e);
290        }
291        if (tasks == null) {
292            return;
293        }
294        boolean wasGrantedInFullscreen = false;
295        boolean wasGrantedInPip = false;
296        for (int i = tasks.size() - 1; i >= 0; --i) {
297            RunningTaskInfo task = tasks.get(i);
298            for (int j = packageNames.length - 1; j >= 0; --j) {
299                if (task.topActivity.getPackageName().equals(packageNames[j])) {
300                    if (task.id == fullscreenTopTaskId) {
301                        wasGrantedInFullscreen = true;
302                    } else if (task.id == mPipTaskId) {
303                        wasGrantedInPip= true;
304                    }
305                }
306            }
307        }
308        if (wasGrantedInFullscreen && !wasGrantedInPip) {
309            closePip();
310        }
311    }
312
313    private class TaskStackListener extends ITaskStackListener.Stub {
314        @Override
315        public void onTaskStackChanged() throws RemoteException {
316            // Post the message back to the UI thread.
317            mHandler.post(mOnTaskStackChanged);
318        }
319
320        @Override
321        public void onActivityPinned()  throws RemoteException {
322            // Post the message back to the UI thread.
323            mHandler.post(mOnActivityPinnedRunnable);
324        }
325
326        @Override
327        public void onPinnedActivityRestartAttempt() {
328            // Post the message back to the UI thread.
329            mHandler.post(mOnPinnedActivityRestartAttempt);
330        }
331    }
332
333    /**
334     * A listener interface to receive notification on changes in PIP.
335     */
336    public interface Listener {
337        /**
338         * Invoked when a PIPped activity is closed.
339         */
340        void onPipActivityClosed();
341        /**
342         * Invoked when the PIP menu gets shown.
343         */
344        void onShowPipMenu();
345        /**
346         * Invoked when the PIPped activity is returned back to the fullscreen.
347         */
348        void onMoveToFullscreen();
349    }
350
351    /**
352     * Gets an instance of {@link PipManager}.
353     */
354    public static PipManager getInstance() {
355        if (sPipManager == null) {
356            sPipManager = new PipManager();
357        }
358        return sPipManager;
359    }
360}
361