1 /*
2  * Copyright (C) 2014 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 android.media.session;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.media.AudioManager;
24 import android.media.IRemoteVolumeController;
25 import android.media.session.ISessionManager;
26 import android.os.Handler;
27 import android.os.IBinder;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.os.UserHandle;
31 import android.service.notification.NotificationListenerService;
32 import android.util.ArrayMap;
33 import android.util.Log;
34 import android.view.KeyEvent;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 
39 /**
40  * Provides support for interacting with {@link MediaSession media sessions}
41  * that applications have published to express their ongoing media playback
42  * state.
43  * <p>
44  * Use <code>Context.getSystemService(Context.MEDIA_SESSION_SERVICE)</code> to
45  * get an instance of this class.
46  *
47  * @see MediaSession
48  * @see MediaController
49  */
50 public final class MediaSessionManager {
51     private static final String TAG = "SessionManager";
52 
53     private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners
54             = new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>();
55     private final Object mLock = new Object();
56     private final ISessionManager mService;
57 
58     private Context mContext;
59 
60     /**
61      * @hide
62      */
MediaSessionManager(Context context)63     public MediaSessionManager(Context context) {
64         // Consider rewriting like DisplayManagerGlobal
65         // Decide if we need context
66         mContext = context;
67         IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE);
68         mService = ISessionManager.Stub.asInterface(b);
69     }
70 
71     /**
72      * Create a new session in the system and get the binder for it.
73      *
74      * @param tag A short name for debugging purposes.
75      * @return The binder object from the system
76      * @hide
77      */
createSession(@onNull MediaSession.CallbackStub cbStub, @NonNull String tag, int userId)78     public @NonNull ISession createSession(@NonNull MediaSession.CallbackStub cbStub,
79             @NonNull String tag, int userId) throws RemoteException {
80         return mService.createSession(mContext.getPackageName(), cbStub, tag, userId);
81     }
82 
83     /**
84      * Get a list of controllers for all ongoing sessions. The controllers will
85      * be provided in priority order with the most important controller at index
86      * 0.
87      * <p>
88      * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL
89      * permission be held by the calling app. You may also retrieve this list if
90      * your app is an enabled notification listener using the
91      * {@link NotificationListenerService} APIs, in which case you must pass the
92      * {@link ComponentName} of your enabled listener.
93      *
94      * @param notificationListener The enabled notification listener component.
95      *            May be null.
96      * @return A list of controllers for ongoing sessions.
97      */
getActiveSessions( @ullable ComponentName notificationListener)98     public @NonNull List<MediaController> getActiveSessions(
99             @Nullable ComponentName notificationListener) {
100         return getActiveSessionsForUser(notificationListener, UserHandle.myUserId());
101     }
102 
103     /**
104      * Get active sessions for a specific user. To retrieve actions for a user
105      * other than your own you must hold the
106      * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission
107      * in addition to any other requirements. If you are an enabled notification
108      * listener you may only get sessions for the users you are enabled for.
109      *
110      * @param notificationListener The enabled notification listener component.
111      *            May be null.
112      * @param userId The user id to fetch sessions for.
113      * @return A list of controllers for ongoing sessions.
114      * @hide
115      */
getActiveSessionsForUser( @ullable ComponentName notificationListener, int userId)116     public @NonNull List<MediaController> getActiveSessionsForUser(
117             @Nullable ComponentName notificationListener, int userId) {
118         ArrayList<MediaController> controllers = new ArrayList<MediaController>();
119         try {
120             List<IBinder> binders = mService.getSessions(notificationListener, userId);
121             int size = binders.size();
122             for (int i = 0; i < size; i++) {
123                 MediaController controller = new MediaController(mContext, ISessionController.Stub
124                         .asInterface(binders.get(i)));
125                 controllers.add(controller);
126             }
127         } catch (RemoteException e) {
128             Log.e(TAG, "Failed to get active sessions: ", e);
129         }
130         return controllers;
131     }
132 
133     /**
134      * Add a listener to be notified when the list of active sessions
135      * changes.This requires the
136      * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
137      * the calling app. You may also retrieve this list if your app is an
138      * enabled notification listener using the
139      * {@link NotificationListenerService} APIs, in which case you must pass the
140      * {@link ComponentName} of your enabled listener. Updates will be posted to
141      * the thread that registered the listener.
142      *
143      * @param sessionListener The listener to add.
144      * @param notificationListener The enabled notification listener component.
145      *            May be null.
146      */
addOnActiveSessionsChangedListener( @onNull OnActiveSessionsChangedListener sessionListener, @Nullable ComponentName notificationListener)147     public void addOnActiveSessionsChangedListener(
148             @NonNull OnActiveSessionsChangedListener sessionListener,
149             @Nullable ComponentName notificationListener) {
150         addOnActiveSessionsChangedListener(sessionListener, notificationListener, null);
151     }
152 
153     /**
154      * Add a listener to be notified when the list of active sessions
155      * changes.This requires the
156      * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
157      * the calling app. You may also retrieve this list if your app is an
158      * enabled notification listener using the
159      * {@link NotificationListenerService} APIs, in which case you must pass the
160      * {@link ComponentName} of your enabled listener. Updates will be posted to
161      * the handler specified or to the caller's thread if the handler is null.
162      *
163      * @param sessionListener The listener to add.
164      * @param notificationListener The enabled notification listener component.
165      *            May be null.
166      * @param handler The handler to post events to.
167      */
addOnActiveSessionsChangedListener( @onNull OnActiveSessionsChangedListener sessionListener, @Nullable ComponentName notificationListener, @Nullable Handler handler)168     public void addOnActiveSessionsChangedListener(
169             @NonNull OnActiveSessionsChangedListener sessionListener,
170             @Nullable ComponentName notificationListener, @Nullable Handler handler) {
171         addOnActiveSessionsChangedListener(sessionListener, notificationListener,
172                 UserHandle.myUserId(), handler);
173     }
174 
175     /**
176      * Add a listener to be notified when the list of active sessions
177      * changes.This requires the
178      * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
179      * the calling app. You may also retrieve this list if your app is an
180      * enabled notification listener using the
181      * {@link NotificationListenerService} APIs, in which case you must pass the
182      * {@link ComponentName} of your enabled listener.
183      *
184      * @param sessionListener The listener to add.
185      * @param notificationListener The enabled notification listener component.
186      *            May be null.
187      * @param userId The userId to listen for changes on.
188      * @param handler The handler to post updates on.
189      * @hide
190      */
addOnActiveSessionsChangedListener( @onNull OnActiveSessionsChangedListener sessionListener, @Nullable ComponentName notificationListener, int userId, @Nullable Handler handler)191     public void addOnActiveSessionsChangedListener(
192             @NonNull OnActiveSessionsChangedListener sessionListener,
193             @Nullable ComponentName notificationListener, int userId, @Nullable Handler handler) {
194         if (sessionListener == null) {
195             throw new IllegalArgumentException("listener may not be null");
196         }
197         if (handler == null) {
198             handler = new Handler();
199         }
200         synchronized (mLock) {
201             if (mListeners.get(sessionListener) != null) {
202                 Log.w(TAG, "Attempted to add session listener twice, ignoring.");
203                 return;
204             }
205             SessionsChangedWrapper wrapper = new SessionsChangedWrapper(mContext, sessionListener,
206                     handler);
207             try {
208                 mService.addSessionsListener(wrapper.mStub, notificationListener, userId);
209                 mListeners.put(sessionListener, wrapper);
210             } catch (RemoteException e) {
211                 Log.e(TAG, "Error in addOnActiveSessionsChangedListener.", e);
212             }
213         }
214     }
215 
216     /**
217      * Stop receiving active sessions updates on the specified listener.
218      *
219      * @param listener The listener to remove.
220      */
removeOnActiveSessionsChangedListener( @onNull OnActiveSessionsChangedListener listener)221     public void removeOnActiveSessionsChangedListener(
222             @NonNull OnActiveSessionsChangedListener listener) {
223         if (listener == null) {
224             throw new IllegalArgumentException("listener may not be null");
225         }
226         synchronized (mLock) {
227             SessionsChangedWrapper wrapper = mListeners.remove(listener);
228             if (wrapper != null) {
229                 try {
230                     mService.removeSessionsListener(wrapper.mStub);
231                 } catch (RemoteException e) {
232                     Log.e(TAG, "Error in removeOnActiveSessionsChangedListener.", e);
233                 } finally {
234                     wrapper.release();
235                 }
236             }
237         }
238     }
239 
240     /**
241      * Set the remote volume controller to receive volume updates on. Only for
242      * use by system UI.
243      *
244      * @param rvc The volume controller to receive updates on.
245      * @hide
246      */
setRemoteVolumeController(IRemoteVolumeController rvc)247     public void setRemoteVolumeController(IRemoteVolumeController rvc) {
248         try {
249             mService.setRemoteVolumeController(rvc);
250         } catch (RemoteException e) {
251             Log.e(TAG, "Error in setRemoteVolumeController.", e);
252         }
253     }
254 
255     /**
256      * Send a media key event. The receiver will be selected automatically.
257      *
258      * @param keyEvent The KeyEvent to send.
259      * @hide
260      */
dispatchMediaKeyEvent(@onNull KeyEvent keyEvent)261     public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent) {
262         dispatchMediaKeyEvent(keyEvent, false);
263     }
264 
265     /**
266      * Send a media key event. The receiver will be selected automatically.
267      *
268      * @param keyEvent The KeyEvent to send.
269      * @param needWakeLock True if a wake lock should be held while sending the key.
270      * @hide
271      */
dispatchMediaKeyEvent(@onNull KeyEvent keyEvent, boolean needWakeLock)272     public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent, boolean needWakeLock) {
273         try {
274             mService.dispatchMediaKeyEvent(keyEvent, needWakeLock);
275         } catch (RemoteException e) {
276             Log.e(TAG, "Failed to send key event.", e);
277         }
278     }
279 
280     /**
281      * Dispatch an adjust volume request to the system. It will be sent to the
282      * most relevant audio stream or media session. The direction must be one of
283      * {@link AudioManager#ADJUST_LOWER}, {@link AudioManager#ADJUST_RAISE},
284      * {@link AudioManager#ADJUST_SAME}.
285      *
286      * @param suggestedStream The stream to fall back to if there isn't a
287      *            relevant stream
288      * @param direction The direction to adjust volume in.
289      * @param flags Any flags to include with the volume change.
290      * @hide
291      */
dispatchAdjustVolume(int suggestedStream, int direction, int flags)292     public void dispatchAdjustVolume(int suggestedStream, int direction, int flags) {
293         try {
294             mService.dispatchAdjustVolume(suggestedStream, direction, flags);
295         } catch (RemoteException e) {
296             Log.e(TAG, "Failed to send adjust volume.", e);
297         }
298     }
299 
300     /**
301      * Check if the global priority session is currently active. This can be
302      * used to decide if media keys should be sent to the session or to the app.
303      *
304      * @hide
305      */
isGlobalPriorityActive()306     public boolean isGlobalPriorityActive() {
307         try {
308             return mService.isGlobalPriorityActive();
309         } catch (RemoteException e) {
310             Log.e(TAG, "Failed to check if the global priority is active.", e);
311         }
312         return false;
313     }
314 
315     /**
316      * Listens for changes to the list of active sessions. This can be added
317      * using {@link #addOnActiveSessionsChangedListener}.
318      */
319     public interface OnActiveSessionsChangedListener {
onActiveSessionsChanged(@ullable List<MediaController> controllers)320         public void onActiveSessionsChanged(@Nullable List<MediaController> controllers);
321     }
322 
323     private static final class SessionsChangedWrapper {
324         private Context mContext;
325         private OnActiveSessionsChangedListener mListener;
326         private Handler mHandler;
327 
SessionsChangedWrapper(Context context, OnActiveSessionsChangedListener listener, Handler handler)328         public SessionsChangedWrapper(Context context, OnActiveSessionsChangedListener listener,
329                 Handler handler) {
330             mContext = context;
331             mListener = listener;
332             mHandler = handler;
333         }
334 
335         private final IActiveSessionsListener.Stub mStub = new IActiveSessionsListener.Stub() {
336             @Override
337             public void onActiveSessionsChanged(final List<MediaSession.Token> tokens) {
338                 if (mHandler != null) {
339                     mHandler.post(new Runnable() {
340                         @Override
341                         public void run() {
342                             if (mListener != null) {
343                                 ArrayList<MediaController> controllers
344                                         = new ArrayList<MediaController>();
345                                 int size = tokens.size();
346                                 for (int i = 0; i < size; i++) {
347                                     controllers.add(new MediaController(mContext, tokens.get(i)));
348                                 }
349                                 mListener.onActiveSessionsChanged(controllers);
350                             }
351                         }
352                     });
353                 }
354             }
355         };
356 
release()357         private void release() {
358             mContext = null;
359             mListener = null;
360             mHandler = null;
361         }
362     }
363 }
364