1/*
2 * Copyright (C) 2017 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.phone;
18
19import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
20
21import android.app.IActivityManager;
22import android.app.PendingIntent;
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.graphics.drawable.Drawable;
30import android.graphics.drawable.Icon;
31import android.media.session.MediaController;
32import android.media.session.MediaSession;
33import android.media.session.MediaSessionManager;
34import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
35import android.media.session.PlaybackState;
36import android.os.UserHandle;
37
38import com.android.systemui.Dependency;
39import com.android.systemui.R;
40import com.android.systemui.statusbar.policy.UserInfoController;
41
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.List;
45
46/**
47 * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only
48 * if there are no actions from the PiP activity itself). The active media controller is only set
49 * when there is a media session from the top PiP activity.
50 */
51public class PipMediaController {
52
53    private static final String ACTION_PLAY = "com.android.systemui.pip.phone.PLAY";
54    private static final String ACTION_PAUSE = "com.android.systemui.pip.phone.PAUSE";
55    private static final String ACTION_NEXT = "com.android.systemui.pip.phone.NEXT";
56    private static final String ACTION_PREV = "com.android.systemui.pip.phone.PREV";
57
58    /**
59     * A listener interface to receive notification on changes to the media actions.
60     */
61    public interface ActionListener {
62        /**
63         * Called when the media actions changes.
64         */
65        void onMediaActionsChanged(List<RemoteAction> actions);
66    }
67
68    private final Context mContext;
69    private final IActivityManager mActivityManager;
70
71    private final MediaSessionManager mMediaSessionManager;
72    private MediaController mMediaController;
73
74    private RemoteAction mPauseAction;
75    private RemoteAction mPlayAction;
76    private RemoteAction mNextAction;
77    private RemoteAction mPrevAction;
78
79    private BroadcastReceiver mPlayPauseActionReceiver = new BroadcastReceiver() {
80        @Override
81        public void onReceive(Context context, Intent intent) {
82            final String action = intent.getAction();
83            if (action.equals(ACTION_PLAY)) {
84                mMediaController.getTransportControls().play();
85            } else if (action.equals(ACTION_PAUSE)) {
86                mMediaController.getTransportControls().pause();
87            } else if (action.equals(ACTION_NEXT)) {
88                mMediaController.getTransportControls().skipToNext();
89            } else if (action.equals(ACTION_PREV)) {
90                mMediaController.getTransportControls().skipToPrevious();
91            }
92        }
93    };
94
95    private final MediaController.Callback mPlaybackChangedListener = new MediaController.Callback() {
96        @Override
97        public void onPlaybackStateChanged(PlaybackState state) {
98            notifyActionsChanged();
99        }
100    };
101
102    private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener =
103            new OnActiveSessionsChangedListener() {
104        @Override
105        public void onActiveSessionsChanged(List<MediaController> controllers) {
106            resolveActiveMediaController(controllers);
107        }
108    };
109
110    private ArrayList<ActionListener> mListeners = new ArrayList<>();
111
112    public PipMediaController(Context context, IActivityManager activityManager) {
113        mContext = context;
114        mActivityManager = activityManager;
115        IntentFilter mediaControlFilter = new IntentFilter();
116        mediaControlFilter.addAction(ACTION_PLAY);
117        mediaControlFilter.addAction(ACTION_PAUSE);
118        mediaControlFilter.addAction(ACTION_NEXT);
119        mediaControlFilter.addAction(ACTION_PREV);
120        mContext.registerReceiver(mPlayPauseActionReceiver, mediaControlFilter);
121
122        createMediaActions();
123        mMediaSessionManager =
124                (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
125
126        // The media session listener needs to be re-registered when switching users
127        UserInfoController userInfoController = Dependency.get(UserInfoController.class);
128        userInfoController.addCallback((String name, Drawable picture, String userAccount) ->
129                registerSessionListenerForCurrentUser());
130    }
131
132    /**
133     * Handles when an activity is pinned.
134     */
135    public void onActivityPinned() {
136        // Once we enter PiP, try to find the active media controller for the top most activity
137        resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null,
138                UserHandle.USER_CURRENT));
139    }
140
141    /**
142     * Adds a new media action listener.
143     */
144    public void addListener(ActionListener listener) {
145        if (!mListeners.contains(listener)) {
146            mListeners.add(listener);
147            listener.onMediaActionsChanged(getMediaActions());
148        }
149    }
150
151    /**
152     * Removes a media action listener.
153     */
154    public void removeListener(ActionListener listener) {
155        listener.onMediaActionsChanged(Collections.EMPTY_LIST);
156        mListeners.remove(listener);
157    }
158
159    /**
160     * Gets the set of media actions currently available.
161     */
162    private List<RemoteAction> getMediaActions() {
163        if (mMediaController == null || mMediaController.getPlaybackState() == null) {
164            return Collections.EMPTY_LIST;
165        }
166
167        ArrayList<RemoteAction> mediaActions = new ArrayList<>();
168        int state = mMediaController.getPlaybackState().getState();
169        boolean isPlaying = MediaSession.isActiveState(state);
170        long actions = mMediaController.getPlaybackState().getActions();
171
172        // Prev action
173        mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
174        mediaActions.add(mPrevAction);
175
176        // Play/pause action
177        if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
178            mediaActions.add(mPlayAction);
179        } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
180            mediaActions.add(mPauseAction);
181        }
182
183        // Next action
184        mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
185        mediaActions.add(mNextAction);
186        return mediaActions;
187    }
188
189    /**
190     * Creates the standard media buttons that we may show.
191     */
192    private void createMediaActions() {
193        String pauseDescription = mContext.getString(R.string.pip_pause);
194        mPauseAction = new RemoteAction(Icon.createWithResource(mContext,
195                R.drawable.ic_pause_white), pauseDescription, pauseDescription,
196                        PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PAUSE),
197                                FLAG_UPDATE_CURRENT));
198
199        String playDescription = mContext.getString(R.string.pip_play);
200        mPlayAction = new RemoteAction(Icon.createWithResource(mContext,
201                R.drawable.ic_play_arrow_white), playDescription, playDescription,
202                        PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PLAY),
203                                FLAG_UPDATE_CURRENT));
204
205        String nextDescription = mContext.getString(R.string.pip_skip_to_next);
206        mNextAction = new RemoteAction(Icon.createWithResource(mContext,
207                R.drawable.ic_skip_next_white), nextDescription, nextDescription,
208                        PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NEXT),
209                                FLAG_UPDATE_CURRENT));
210
211        String prevDescription = mContext.getString(R.string.pip_skip_to_prev);
212        mPrevAction = new RemoteAction(Icon.createWithResource(mContext,
213                R.drawable.ic_skip_previous_white), prevDescription, prevDescription,
214                        PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PREV),
215                                FLAG_UPDATE_CURRENT));
216    }
217
218    /**
219     * Re-registers the session listener for the current user.
220     */
221    private void registerSessionListenerForCurrentUser() {
222        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
223        mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsChangedListener, null,
224                UserHandle.USER_CURRENT, null);
225    }
226
227    /**
228     * Tries to find and set the active media controller for the top PiP activity.
229     */
230    private void resolveActiveMediaController(List<MediaController> controllers) {
231        if (controllers != null) {
232            final ComponentName topActivity = PipUtils.getTopPinnedActivity(mContext,
233                    mActivityManager).first;
234            if (topActivity != null) {
235                for (int i = 0; i < controllers.size(); i++) {
236                    final MediaController controller = controllers.get(i);
237                    if (controller.getPackageName().equals(topActivity.getPackageName())) {
238                        setActiveMediaController(controller);
239                        return;
240                    }
241                }
242            }
243        }
244        setActiveMediaController(null);
245    }
246
247    /**
248     * Sets the active media controller for the top PiP activity.
249     */
250    private void setActiveMediaController(MediaController controller) {
251        if (controller != mMediaController) {
252            if (mMediaController != null) {
253                mMediaController.unregisterCallback(mPlaybackChangedListener);
254            }
255            mMediaController = controller;
256            if (controller != null) {
257                controller.registerCallback(mPlaybackChangedListener);
258            }
259            notifyActionsChanged();
260
261            // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
262        }
263    }
264
265    /**
266     * Notifies all listeners that the actions have changed.
267     */
268    private void notifyActionsChanged() {
269        if (!mListeners.isEmpty()) {
270            List<RemoteAction> actions = getMediaActions();
271            mListeners.forEach(l -> l.onMediaActionsChanged(actions));
272        }
273    }
274}
275