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
17package android.media.session;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.ComponentName;
22import android.content.Context;
23import android.media.AudioManager;
24import android.media.IRemoteVolumeController;
25import android.media.session.ISessionManager;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.RemoteException;
29import android.os.ServiceManager;
30import android.os.UserHandle;
31import android.service.notification.NotificationListenerService;
32import android.util.ArrayMap;
33import android.util.Log;
34import android.view.KeyEvent;
35
36import java.util.ArrayList;
37import 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 */
50public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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 {
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
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
357        private void release() {
358            mContext = null;
359            mListener = null;
360            mHandler = null;
361        }
362    }
363}
364