MediaSessionService.java revision 4646d288821d62fdfe481be67d8b7fed7d7eabd8
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 com.android.server.media;
18
19import android.Manifest;
20import android.app.ActivityManager;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.pm.PackageManager;
24import android.media.routeprovider.RouteRequest;
25import android.media.session.ISession;
26import android.media.session.ISessionCallback;
27import android.media.session.ISessionManager;
28import android.media.session.RouteInfo;
29import android.media.session.RouteOptions;
30import android.media.session.Session;
31import android.os.Binder;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.RemoteException;
35import android.os.UserHandle;
36import android.provider.Settings;
37import android.text.TextUtils;
38import android.util.Log;
39import android.util.SparseArray;
40
41import com.android.server.SystemService;
42import com.android.server.Watchdog;
43import com.android.server.Watchdog.Monitor;
44
45import java.io.FileDescriptor;
46import java.io.PrintWriter;
47import java.util.ArrayList;
48import java.util.List;
49
50/**
51 * System implementation of MediaSessionManager
52 */
53public class MediaSessionService extends SystemService implements Monitor {
54    private static final String TAG = "MediaSessionService";
55    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
56
57    private final SessionManagerImpl mSessionManagerImpl;
58    // private final MediaRouteProviderWatcher mRouteProviderWatcher;
59    private final MediaSessionStack mPriorityStack;
60
61    private final ArrayList<MediaSessionRecord> mAllSessions = new ArrayList<MediaSessionRecord>();
62    private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>();
63    // private final ArrayList<MediaRouteProviderProxy> mProviders
64    // = new ArrayList<MediaRouteProviderProxy>();
65    private final Object mLock = new Object();
66    private final Handler mHandler = new Handler();
67
68    private MediaSessionRecord mPrioritySession;
69    private int mCurrentUserId = -1;
70
71    // Used to keep track of the current request to show routes for a specific
72    // session so we drop late callbacks properly.
73    private int mShowRoutesRequestId = 0;
74
75    // TODO refactor to have per user state for providers. See
76    // MediaRouterService for an example
77
78    public MediaSessionService(Context context) {
79        super(context);
80        mSessionManagerImpl = new SessionManagerImpl();
81        mPriorityStack = new MediaSessionStack();
82    }
83
84    @Override
85    public void onStart() {
86        publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
87        Watchdog.getInstance().addMonitor(this);
88        updateUser();
89    }
90
91    /**
92     * Should trigger showing the Media route picker dialog. Right now it just
93     * kicks off a query to all the providers to get routes.
94     *
95     * @param record The session to show the picker for.
96     */
97    public void showRoutePickerForSession(MediaSessionRecord record) {
98        // TODO for now just toggle the route to test (we will only have one
99        // match for now)
100        synchronized (mLock) {
101            if (!mAllSessions.contains(record)) {
102                Log.d(TAG, "Unknown session tried to show route picker. Ignoring.");
103                return;
104            }
105            RouteInfo current = record.getRoute();
106            UserRecord user = mUserRecords.get(record.getUserId());
107            if (current != null) {
108                // For now send null to mean the local route
109                MediaRouteProviderProxy proxy = user.getProviderLocked(current.getProvider());
110                if (proxy != null) {
111                    proxy.removeSession(record);
112                }
113                record.selectRoute(null);
114                return;
115            }
116            ArrayList<MediaRouteProviderProxy> providers = user.getProvidersLocked();
117            mShowRoutesRequestId++;
118            for (int i = providers.size() - 1; i >= 0; i--) {
119                MediaRouteProviderProxy provider = providers.get(i);
120                provider.getRoutes(record, mShowRoutesRequestId);
121            }
122        }
123    }
124
125    /**
126     * Connect a session to the given route.
127     *
128     * @param session The session to connect.
129     * @param route The route to connect to.
130     * @param options The options to use for the connection.
131     */
132    public void connectToRoute(MediaSessionRecord session, RouteInfo route,
133            RouteOptions options) {
134        synchronized (mLock) {
135            if (!mAllSessions.contains(session)) {
136                Log.d(TAG, "Unknown session attempting to connect to route. Ignoring");
137                return;
138            }
139            UserRecord user = mUserRecords.get(session.getUserId());
140            if (user == null) {
141                Log.wtf(TAG, "connectToRoute: User " + session.getUserId() + " does not exist.");
142                return;
143            }
144            MediaRouteProviderProxy proxy = user.getProviderLocked(route.getProvider());
145            if (proxy == null) {
146                Log.w(TAG, "Provider for route " + route.getName() + " does not exist.");
147                return;
148            }
149            RouteRequest request = new RouteRequest(session.getSessionInfo(), options, true);
150            proxy.connectToRoute(session, route, request);
151        }
152    }
153
154    public void updateSession(MediaSessionRecord record) {
155        synchronized (mLock) {
156            if (!mAllSessions.contains(record)) {
157                Log.d(TAG, "Unknown session updated. Ignoring.");
158                return;
159            }
160            mPriorityStack.onSessionStateChange(record);
161            if (record.isSystemPriority()) {
162                if (record.isActive()) {
163                    if (mPrioritySession != null) {
164                        Log.w(TAG, "Replacing existing priority session with a new session");
165                    }
166                    mPrioritySession = record;
167                } else {
168                    if (mPrioritySession == record) {
169                        mPrioritySession = null;
170                    }
171                }
172            }
173        }
174    }
175
176    public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
177        synchronized (mLock) {
178            if (!mAllSessions.contains(record)) {
179                Log.d(TAG, "Unknown session changed playback state. Ignoring.");
180                return;
181            }
182            mPriorityStack.onPlaystateChange(record, oldState, newState);
183        }
184    }
185
186    @Override
187    public void onStartUser(int userHandle) {
188        updateUser();
189    }
190
191    @Override
192    public void onSwitchUser(int userHandle) {
193        updateUser();
194    }
195
196    @Override
197    public void onStopUser(int userHandle) {
198        synchronized (mLock) {
199            UserRecord user = mUserRecords.get(userHandle);
200            if (user != null) {
201                destroyUserLocked(user);
202            }
203        }
204    }
205
206    @Override
207    public void monitor() {
208        synchronized (mLock) {
209            // Check for deadlock
210        }
211    }
212
213    protected void enforcePhoneStatePermission(int pid, int uid) {
214        if (getContext().checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
215                != PackageManager.PERMISSION_GRANTED) {
216            throw new SecurityException("Must hold the MODIFY_PHONE_STATE permission.");
217        }
218    }
219
220    void sessionDied(MediaSessionRecord session) {
221        synchronized (mLock) {
222            destroySessionLocked(session);
223        }
224    }
225
226    void destroySession(MediaSessionRecord session) {
227        synchronized (mLock) {
228            destroySessionLocked(session);
229        }
230    }
231
232    private void updateUser() {
233        synchronized (mLock) {
234            int userId = ActivityManager.getCurrentUser();
235            if (mCurrentUserId != userId) {
236                final int oldUserId = mCurrentUserId;
237                mCurrentUserId = userId; // do this first
238
239                UserRecord oldUser = mUserRecords.get(oldUserId);
240                if (oldUser != null) {
241                    oldUser.stopLocked();
242                }
243
244                UserRecord newUser = getOrCreateUser(userId);
245                newUser.startLocked();
246            }
247        }
248    }
249
250    /**
251     * Stop the user and unbind from everything.
252     *
253     * @param user The user to dispose of
254     */
255    private void destroyUserLocked(UserRecord user) {
256        user.stopLocked();
257        user.destroyLocked();
258        mUserRecords.remove(user.mUserId);
259    }
260
261    /*
262     * When a session is removed several things need to happen.
263     * 1. We need to remove it from the relevant user.
264     * 2. We need to remove it from the priority stack.
265     * 3. We need to remove it from all sessions.
266     * 4. If this is the system priority session we need to clear it.
267     * 5. We need to unlink to death from the cb binder
268     * 6. We need to tell the session to do any final cleanup (onDestroy)
269     */
270    private void destroySessionLocked(MediaSessionRecord session) {
271        int userId = session.getUserId();
272        UserRecord user = mUserRecords.get(userId);
273        if (user != null) {
274            user.removeSessionLocked(session);
275        }
276
277        mPriorityStack.removeSession(session);
278        mAllSessions.remove(session);
279        if (session == mPrioritySession) {
280            mPrioritySession = null;
281        }
282
283        try {
284            session.getCallback().asBinder().unlinkToDeath(session, 0);
285        } catch (Exception e) {
286            // ignore exceptions while destroying a session.
287        }
288        session.onDestroy();
289    }
290
291    private void enforcePackageName(String packageName, int uid) {
292        if (TextUtils.isEmpty(packageName)) {
293            throw new IllegalArgumentException("packageName may not be empty");
294        }
295        String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
296        final int packageCount = packages.length;
297        for (int i = 0; i < packageCount; i++) {
298            if (packageName.equals(packages[i])) {
299                return;
300            }
301        }
302        throw new IllegalArgumentException("packageName is not owned by the calling process");
303    }
304
305    /**
306     * Checks a caller's authorization to register an IRemoteControlDisplay.
307     * Authorization is granted if one of the following is true:
308     * <ul>
309     * <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL
310     * permission</li>
311     * <li>the caller's listener is one of the enabled notification listeners
312     * for the caller's user</li>
313     * </ul>
314     */
315    private void enforceMediaPermissions(ComponentName compName, int pid, int uid,
316            int resolvedUserId) {
317        if (getContext()
318                .checkPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
319                    != PackageManager.PERMISSION_GRANTED
320                && !isEnabledNotificationListener(compName, UserHandle.getUserId(uid),
321                        resolvedUserId)) {
322            throw new SecurityException("Missing permission to control media.");
323        }
324    }
325
326    /**
327     * This checks if the component is an enabled notification listener for the
328     * specified user. Enabled components may only operate on behalf of the user
329     * they're running as.
330     *
331     * @param compName The component that is enabled.
332     * @param userId The user id of the caller.
333     * @param forUserId The user id they're making the request on behalf of.
334     * @return True if the component is enabled, false otherwise
335     */
336    private boolean isEnabledNotificationListener(ComponentName compName, int userId,
337            int forUserId) {
338        if (userId != forUserId) {
339            // You may not access another user's content as an enabled listener.
340            return false;
341        }
342        if (compName != null) {
343            final String enabledNotifListeners = Settings.Secure.getStringForUser(
344                    getContext().getContentResolver(),
345                    Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
346                    userId);
347            if (enabledNotifListeners != null) {
348                final String[] components = enabledNotifListeners.split(":");
349                for (int i = 0; i < components.length; i++) {
350                    final ComponentName component =
351                            ComponentName.unflattenFromString(components[i]);
352                    if (component != null) {
353                        if (compName.equals(component)) {
354                            if (DEBUG) {
355                                Log.d(TAG, "ok to get sessions: " + component +
356                                        " is authorized notification listener");
357                            }
358                            return true;
359                        }
360                    }
361                }
362            }
363            if (DEBUG) {
364                Log.d(TAG, "not ok to get sessions, " + compName +
365                        " is not in list of ENABLED_NOTIFICATION_LISTENERS for user " + userId);
366            }
367        }
368        return false;
369    }
370
371    private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
372            String callerPackageName, ISessionCallback cb, String tag) throws RemoteException {
373        synchronized (mLock) {
374            return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb, tag);
375        }
376    }
377
378    /*
379     * When a session is created the following things need to happen.
380     * 1. It's callback binder needs a link to death
381     * 2. It needs to be added to all sessions.
382     * 3. It needs to be added to the priority stack.
383     * 4. It needs to be added to the relevant user record.
384     */
385    private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
386            String callerPackageName, ISessionCallback cb, String tag) {
387
388        final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId,
389                callerPackageName, cb, tag, this, mHandler);
390        try {
391            cb.asBinder().linkToDeath(session, 0);
392        } catch (RemoteException e) {
393            throw new RuntimeException("Media Session owner died prematurely.", e);
394        }
395
396        mAllSessions.add(session);
397        mPriorityStack.addSession(session);
398
399        UserRecord user = getOrCreateUser(userId);
400        user.addSessionLocked(session);
401
402        if (DEBUG) {
403            Log.d(TAG, "Created session for package " + callerPackageName + " with tag " + tag);
404        }
405        return session;
406    }
407
408    private UserRecord getOrCreateUser(int userId) {
409        UserRecord user = mUserRecords.get(userId);
410        if (user == null) {
411            user = new UserRecord(getContext(), userId);
412            mUserRecords.put(userId, user);
413        }
414        return user;
415    }
416
417    private int findIndexOfSessionForIdLocked(String sessionId) {
418        for (int i = mAllSessions.size() - 1; i >= 0; i--) {
419            MediaSessionRecord session = mAllSessions.get(i);
420            if (TextUtils.equals(session.getSessionInfo().getId(), sessionId)) {
421                return i;
422            }
423        }
424        return -1;
425    }
426
427    private boolean isSessionDiscoverable(MediaSessionRecord record) {
428        // TODO probably want to check more than if it's active.
429        return record.isActive();
430    }
431
432    private MediaRouteProviderProxy.RoutesListener mRoutesCallback
433            = new MediaRouteProviderProxy.RoutesListener() {
434        @Override
435        public void onRoutesUpdated(String sessionId, ArrayList<RouteInfo> routes,
436                int reqId) {
437            // TODO for now select the first route to test, eventually add the
438            // new routes to the dialog if it is still open
439            synchronized (mLock) {
440                int index = findIndexOfSessionForIdLocked(sessionId);
441                if (index != -1 && routes != null && routes.size() > 0) {
442                    MediaSessionRecord record = mAllSessions.get(index);
443                    RouteInfo route = routes.get(0);
444                    record.selectRoute(route);
445                    UserRecord user = mUserRecords.get(record.getUserId());
446                    MediaRouteProviderProxy provider = user.getProviderLocked(route.getProvider());
447                    provider.addSession(record);
448                }
449            }
450        }
451
452        @Override
453        public void onRouteConnected(String sessionId, RouteInfo route,
454                RouteRequest options, RouteConnectionRecord connection) {
455            synchronized (mLock) {
456                int index = findIndexOfSessionForIdLocked(sessionId);
457                if (index != -1) {
458                    MediaSessionRecord session = mAllSessions.get(index);
459                    session.setRouteConnected(route, options.getConnectionOptions(), connection);
460                }
461            }
462        }
463    };
464
465    /**
466     * Information about a particular user. The contents of this object is
467     * guarded by mLock.
468     */
469    final class UserRecord {
470        private final int mUserId;
471        private final MediaRouteProviderWatcher mRouteProviderWatcher;
472        private final ArrayList<MediaRouteProviderProxy> mProviders
473                = new ArrayList<MediaRouteProviderProxy>();
474        private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
475
476        public UserRecord(Context context, int userId) {
477            mUserId = userId;
478            mRouteProviderWatcher = new MediaRouteProviderWatcher(context,
479                    mProviderWatcherCallback, mHandler, userId);
480        }
481
482        public void startLocked() {
483            mRouteProviderWatcher.start();
484        }
485
486        public void stopLocked() {
487            mRouteProviderWatcher.stop();
488            updateInterestLocked();
489        }
490
491        public void destroyLocked() {
492            for (int i = mSessions.size() - 1; i >= 0; i--) {
493                MediaSessionRecord session = mSessions.get(i);
494                MediaSessionService.this.destroySessionLocked(session);
495                if (session.isConnected()) {
496                    session.disconnect(Session.DISCONNECT_REASON_USER_STOPPING);
497                }
498            }
499        }
500
501        public ArrayList<MediaRouteProviderProxy> getProvidersLocked() {
502            return mProviders;
503        }
504
505        public ArrayList<MediaSessionRecord> getSessionsLocked() {
506            return mSessions;
507        }
508
509        public void addSessionLocked(MediaSessionRecord session) {
510            mSessions.add(session);
511            updateInterestLocked();
512        }
513
514        public void removeSessionLocked(MediaSessionRecord session) {
515            mSessions.remove(session);
516            RouteInfo route = session.getRoute();
517            if (route != null) {
518                MediaRouteProviderProxy provider = getProviderLocked(route.getProvider());
519                if (provider != null) {
520                    provider.removeSession(session);
521                }
522            }
523            updateInterestLocked();
524        }
525
526        public void dumpLocked(PrintWriter pw, String prefix) {
527            pw.println(prefix + "Record for user " + mUserId);
528            String indent = prefix + "  ";
529            int size = mProviders.size();
530            pw.println(indent + size + " Providers:");
531            for (int i = 0; i < size; i++) {
532                mProviders.get(i).dump(pw, indent);
533            }
534            pw.println();
535            size = mSessions.size();
536            pw.println(indent + size + " Sessions:");
537            for (int i = 0; i < size; i++) {
538                // Just print the session info, the full session dump will
539                // already be in the list of all sessions.
540                pw.println(indent + mSessions.get(i).getSessionInfo());
541            }
542        }
543
544        public void updateInterestLocked() {
545            // TODO go through the sessions and build up the set of interfaces
546            // we're interested in. Update the provider watcher.
547            // For now, just express interest in all providers for the current
548            // user
549            boolean interested = mUserId == mCurrentUserId;
550            for (int i = mProviders.size() - 1; i >= 0; i--) {
551                mProviders.get(i).setInterested(interested);
552            }
553        }
554
555        private MediaRouteProviderProxy getProviderLocked(String providerId) {
556            for (int i = mProviders.size() - 1; i >= 0; i--) {
557                MediaRouteProviderProxy provider = mProviders.get(i);
558                if (TextUtils.equals(providerId, provider.getId())) {
559                    return provider;
560                }
561            }
562            return null;
563        }
564
565        private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
566                = new MediaRouteProviderWatcher.Callback() {
567            @Override
568            public void removeProvider(MediaRouteProviderProxy provider) {
569                synchronized (mLock) {
570                    mProviders.remove(provider);
571                    provider.setRoutesListener(null);
572                    provider.setInterested(false);
573                }
574            }
575
576            @Override
577            public void addProvider(MediaRouteProviderProxy provider) {
578                synchronized (mLock) {
579                    mProviders.add(provider);
580                    provider.setRoutesListener(mRoutesCallback);
581                    provider.setInterested(true);
582                }
583            }
584        };
585    }
586
587    class SessionManagerImpl extends ISessionManager.Stub {
588        // TODO add createSessionAsUser, pass user-id to
589        // ActivityManagerNative.handleIncomingUser and stash result for use
590        // when starting services on that session's behalf.
591        @Override
592        public ISession createSession(String packageName, ISessionCallback cb, String tag,
593                int userId) throws RemoteException {
594            final int pid = Binder.getCallingPid();
595            final int uid = Binder.getCallingUid();
596            final long token = Binder.clearCallingIdentity();
597            try {
598                enforcePackageName(packageName, uid);
599                int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
600                        false /* allowAll */, true /* requireFull */, "createSession", packageName);
601                if (cb == null) {
602                    throw new IllegalArgumentException("Controller callback cannot be null");
603                }
604                return createSessionInternal(pid, uid, resolvedUserId, packageName, cb, tag)
605                        .getSessionBinder();
606            } finally {
607                Binder.restoreCallingIdentity(token);
608            }
609        }
610
611        @Override
612        public List<IBinder> getSessions(ComponentName componentName, int userId) {
613            final int pid = Binder.getCallingPid();
614            final int uid = Binder.getCallingUid();
615            final long token = Binder.clearCallingIdentity();
616
617            try {
618                String packageName = null;
619                if (componentName != null) {
620                    // If they gave us a component name verify they own the
621                    // package
622                    packageName = componentName.getPackageName();
623                    enforcePackageName(packageName, uid);
624                }
625                // Check that they can make calls on behalf of the user and
626                // get the final user id
627                int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
628                        true /* allowAll */, true /* requireFull */, "getSessions", packageName);
629                // Check if they have the permissions or their component is
630                // enabled for the user they're calling from.
631                enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
632                ArrayList<IBinder> binders = new ArrayList<IBinder>();
633                synchronized (mLock) {
634                    ArrayList<MediaSessionRecord> records = mPriorityStack
635                            .getActiveSessions(resolvedUserId);
636                    int size = records.size();
637                    for (int i = 0; i < size; i++) {
638                        binders.add(records.get(i).getControllerBinder().asBinder());
639                    }
640                }
641                return binders;
642            } finally {
643                Binder.restoreCallingIdentity(token);
644            }
645        }
646
647        @Override
648        public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
649            if (getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
650                    != PackageManager.PERMISSION_GRANTED) {
651                pw.println("Permission Denial: can't dump MediaSessionService from from pid="
652                        + Binder.getCallingPid()
653                        + ", uid=" + Binder.getCallingUid());
654                return;
655            }
656
657            pw.println("MEDIA SESSION SERVICE (dumpsys media_session)");
658            pw.println();
659
660            synchronized (mLock) {
661                pw.println("Session for calls:" + mPrioritySession);
662                if (mPrioritySession != null) {
663                    mPrioritySession.dump(pw, "");
664                }
665                int count = mAllSessions.size();
666                pw.println(count + " Sessions:");
667                for (int i = 0; i < count; i++) {
668                    mAllSessions.get(i).dump(pw, "");
669                    pw.println();
670                }
671                mPriorityStack.dump(pw, "");
672
673                pw.println("User Records:");
674                count = mUserRecords.size();
675                for (int i = 0; i < count; i++) {
676                    UserRecord user = mUserRecords.get(i);
677                    user.dumpLocked(pw, "");
678                }
679            }
680        }
681    }
682
683}
684