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 
17 package com.android.systemui.pip.phone;
18 
19 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
20 
21 import android.app.IActivityManager;
22 import android.app.PendingIntent;
23 import android.app.RemoteAction;
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.graphics.drawable.Drawable;
30 import android.graphics.drawable.Icon;
31 import android.media.session.MediaController;
32 import android.media.session.MediaSession;
33 import android.media.session.MediaSessionManager;
34 import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
35 import android.media.session.PlaybackState;
36 import android.os.UserHandle;
37 
38 import com.android.systemui.Dependency;
39 import com.android.systemui.R;
40 import com.android.systemui.statusbar.policy.UserInfoController;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import 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  */
51 public 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          */
onMediaActionsChanged(List<RemoteAction> actions)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 
PipMediaController(Context context, IActivityManager activityManager)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      */
onActivityPinned()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      */
addListener(ActionListener listener)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      */
removeListener(ActionListener listener)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      */
getMediaActions()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      */
createMediaActions()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      */
registerSessionListenerForCurrentUser()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      */
resolveActiveMediaController(List<MediaController> controllers)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      */
setActiveMediaController(MediaController controller)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      */
notifyActionsChanged()268     private void notifyActionsChanged() {
269         if (!mListeners.isEmpty()) {
270             List<RemoteAction> actions = getMediaActions();
271             mListeners.forEach(l -> l.onMediaActionsChanged(actions));
272         }
273     }
274 }
275