MediaSessionService.java revision a5b02329209be355eafadbdf9ee685ffa58d3148
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.ISessionController;
28import android.media.session.ISessionManager;
29import android.media.session.PlaybackState;
30import android.media.session.RouteInfo;
31import android.media.session.RouteOptions;
32import android.os.Binder;
33import android.os.Handler;
34import android.os.IBinder;
35import android.os.Process;
36import android.os.RemoteException;
37import android.os.UserHandle;
38import android.provider.Settings;
39import android.text.TextUtils;
40import android.util.Log;
41
42import com.android.server.SystemService;
43import com.android.server.Watchdog;
44import com.android.server.Watchdog.Monitor;
45
46import java.io.FileDescriptor;
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import java.util.List;
50
51/**
52 * System implementation of MediaSessionManager
53 */
54public class MediaSessionService extends SystemService implements Monitor {
55    private static final String TAG = "MediaSessionService";
56    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
57
58    private final SessionManagerImpl mSessionManagerImpl;
59    private final MediaRouteProviderWatcher mRouteProviderWatcher;
60    private final MediaSessionStack mPriorityStack;
61
62    private final ArrayList<MediaSessionRecord> mRecords = new ArrayList<MediaSessionRecord>();
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
70    // Used to keep track of the current request to show routes for a specific
71    // session so we drop late callbacks properly.
72    private int mShowRoutesRequestId = 0;
73
74    // TODO refactor to have per user state for providers. See
75    // MediaRouterService for an example
76
77    public MediaSessionService(Context context) {
78        super(context);
79        mSessionManagerImpl = new SessionManagerImpl();
80        mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
81                mHandler, context.getUserId());
82        mPriorityStack = new MediaSessionStack();
83    }
84
85    @Override
86    public void onStart() {
87        publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
88        mRouteProviderWatcher.start();
89        Watchdog.getInstance().addMonitor(this);
90    }
91
92    /**
93     * Should trigger showing the Media route picker dialog. Right now it just
94     * kicks off a query to all the providers to get routes.
95     *
96     * @param record The session to show the picker for.
97     */
98    public void showRoutePickerForSession(MediaSessionRecord record) {
99        // TODO for now just toggle the route to test (we will only have one
100        // match for now)
101        if (record.getRoute() != null) {
102            // For now send null to mean the local route
103            record.selectRoute(null);
104            return;
105        }
106        mShowRoutesRequestId++;
107        ArrayList<MediaRouteProviderProxy> providers = mRouteProviderWatcher.getProviders();
108        for (int i = providers.size() - 1; i >= 0; i--) {
109            MediaRouteProviderProxy provider = providers.get(i);
110            provider.getRoutes(record, mShowRoutesRequestId);
111        }
112    }
113
114    /**
115     * Connect a session to the given route.
116     *
117     * @param session The session to connect.
118     * @param route The route to connect to.
119     * @param options The options to use for the connection.
120     */
121    public void connectToRoute(MediaSessionRecord session, RouteInfo route,
122            RouteOptions options) {
123        synchronized (mLock) {
124            MediaRouteProviderProxy proxy = getProviderLocked(route.getProvider());
125            if (proxy == null) {
126                Log.w(TAG, "Provider for route " + route.getName() + " does not exist.");
127                return;
128            }
129            RouteRequest request = new RouteRequest(session.getSessionInfo(), options, true);
130            // TODO make connect an async call to a ThreadPoolExecutor
131            proxy.connectToRoute(session, route, request);
132        }
133    }
134
135    public void updateSession(MediaSessionRecord record) {
136        synchronized (mLock) {
137            mPriorityStack.onSessionStateChange(record);
138            if (record.isSystemPriority()) {
139                if (record.isActive()) {
140                    if (mPrioritySession != null) {
141                        Log.w(TAG, "Replacing existing priority session with a new session");
142                    }
143                    mPrioritySession = record;
144                } else {
145                    if (mPrioritySession == record) {
146                        mPrioritySession = null;
147                    }
148                }
149            }
150        }
151    }
152
153    public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
154        synchronized (mLock) {
155            mPriorityStack.onPlaystateChange(record, oldState, newState);
156        }
157    }
158
159    @Override
160    public void monitor() {
161        synchronized (mLock) {
162            // Check for deadlock
163        }
164    }
165
166    void sessionDied(MediaSessionRecord session) {
167        synchronized (mLock) {
168            destroySessionLocked(session);
169        }
170    }
171
172    void destroySession(MediaSessionRecord session) {
173        synchronized (mLock) {
174            destroySessionLocked(session);
175        }
176    }
177
178    private void destroySessionLocked(MediaSessionRecord session) {
179        mRecords.remove(session);
180        mPriorityStack.removeSession(session);
181        if (session == mPrioritySession) {
182            mPrioritySession = null;
183        }
184    }
185
186    private void enforcePackageName(String packageName, int uid) {
187        if (TextUtils.isEmpty(packageName)) {
188            throw new IllegalArgumentException("packageName may not be empty");
189        }
190        String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
191        final int packageCount = packages.length;
192        for (int i = 0; i < packageCount; i++) {
193            if (packageName.equals(packages[i])) {
194                return;
195            }
196        }
197        throw new IllegalArgumentException("packageName is not owned by the calling process");
198    }
199
200    protected void enforcePhoneStatePermission(int pid, int uid) {
201        if (getContext().checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
202                != PackageManager.PERMISSION_GRANTED) {
203            throw new SecurityException("Must hold the MODIFY_PHONE_STATE permission.");
204        }
205    }
206
207    /**
208     * Checks a caller's authorization to register an IRemoteControlDisplay.
209     * Authorization is granted if one of the following is true:
210     * <ul>
211     * <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL
212     * permission</li>
213     * <li>the caller's listener is one of the enabled notification listeners
214     * for the caller's user</li>
215     * </ul>
216     */
217    private void enforceMediaPermissions(ComponentName compName, int pid, int uid,
218            int resolvedUserId) {
219        if (getContext()
220                .checkPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
221                    != PackageManager.PERMISSION_GRANTED
222                && !isEnabledNotificationListener(compName, UserHandle.getUserId(uid),
223                        resolvedUserId)) {
224            throw new SecurityException("Missing permission to control media.");
225        }
226    }
227
228    /**
229     * This checks if the component is an enabled notification listener for the
230     * specified user. Enabled components may only operate on behalf of the user
231     * they're running as.
232     *
233     * @param compName The component that is enabled.
234     * @param userId The user id of the caller.
235     * @param forUserId The user id they're making the request on behalf of.
236     * @return True if the component is enabled, false otherwise
237     */
238    private boolean isEnabledNotificationListener(ComponentName compName, int userId,
239            int forUserId) {
240        if (userId != forUserId) {
241            // You may not access another user's content as an enabled listener.
242            return false;
243        }
244        if (compName != null) {
245            final String enabledNotifListeners = Settings.Secure.getStringForUser(
246                    getContext().getContentResolver(),
247                    Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
248                    userId);
249            if (enabledNotifListeners != null) {
250                final String[] components = enabledNotifListeners.split(":");
251                for (int i = 0; i < components.length; i++) {
252                    final ComponentName component =
253                            ComponentName.unflattenFromString(components[i]);
254                    if (component != null) {
255                        if (compName.equals(component)) {
256                            if (DEBUG) {
257                                Log.d(TAG, "ok to get sessions: " + component +
258                                        " is authorized notification listener");
259                            }
260                            return true;
261                        }
262                    }
263                }
264            }
265            if (DEBUG) {
266                Log.d(TAG, "not ok to get sessions, " + compName +
267                        " is not in list of ENABLED_NOTIFICATION_LISTENERS for user " + userId);
268            }
269        }
270        return false;
271    }
272
273    private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
274            String callerPackageName, ISessionCallback cb, String tag) {
275        synchronized (mLock) {
276            return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb, tag);
277        }
278    }
279
280    private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
281            String callerPackageName, ISessionCallback cb, String tag) {
282        final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId,
283                callerPackageName, cb, tag, this, mHandler);
284        try {
285            cb.asBinder().linkToDeath(session, 0);
286        } catch (RemoteException e) {
287            throw new RuntimeException("Media Session owner died prematurely.", e);
288        }
289        mRecords.add(session);
290        mPriorityStack.addSession(session);
291        if (DEBUG) {
292            Log.d(TAG, "Created session for package " + callerPackageName + " with tag " + tag);
293        }
294        return session;
295    }
296
297    private int findIndexOfSessionForIdLocked(String sessionId) {
298        for (int i = mRecords.size() - 1; i >= 0; i--) {
299            MediaSessionRecord session = mRecords.get(i);
300            if (TextUtils.equals(session.getSessionInfo().getId(), sessionId)) {
301                return i;
302            }
303        }
304        return -1;
305    }
306
307    private MediaRouteProviderProxy getProviderLocked(String providerId) {
308        for (int i = mProviders.size() - 1; i >= 0; i--) {
309            MediaRouteProviderProxy provider = mProviders.get(i);
310            if (TextUtils.equals(providerId, provider.getId())) {
311                return provider;
312            }
313        }
314        return null;
315    }
316
317    private boolean isSessionDiscoverable(MediaSessionRecord record) {
318        // TODO probably want to check more than if it's published.
319        return record.isActive();
320    }
321
322    private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
323            = new MediaRouteProviderWatcher.Callback() {
324        @Override
325        public void removeProvider(MediaRouteProviderProxy provider) {
326            synchronized (mLock) {
327                mProviders.remove(provider);
328                provider.setRoutesListener(null);
329                provider.setInterested(false);
330            }
331        }
332
333        @Override
334        public void addProvider(MediaRouteProviderProxy provider) {
335            synchronized (mLock) {
336                mProviders.add(provider);
337                provider.setRoutesListener(mRoutesCallback);
338                provider.setInterested(true);
339            }
340        }
341    };
342
343    private MediaRouteProviderProxy.RoutesListener mRoutesCallback
344            = new MediaRouteProviderProxy.RoutesListener() {
345        @Override
346        public void onRoutesUpdated(String sessionId, ArrayList<RouteInfo> routes,
347                int reqId) {
348            // TODO for now select the first route to test, eventually add the
349            // new routes to the dialog if it is still open
350            synchronized (mLock) {
351                int index = findIndexOfSessionForIdLocked(sessionId);
352                if (index != -1 && routes != null && routes.size() > 0) {
353                    MediaSessionRecord record = mRecords.get(index);
354                    record.selectRoute(routes.get(0));
355                }
356            }
357        }
358
359        @Override
360        public void onRouteConnected(String sessionId, RouteInfo route,
361                RouteRequest options, RouteConnectionRecord connection) {
362            synchronized (mLock) {
363                int index = findIndexOfSessionForIdLocked(sessionId);
364                if (index != -1) {
365                    MediaSessionRecord session = mRecords.get(index);
366                    session.setRouteConnected(route, options.getConnectionOptions(), connection);
367                }
368            }
369        }
370    };
371
372    class SessionManagerImpl extends ISessionManager.Stub {
373        // TODO add createSessionAsUser, pass user-id to
374        // ActivityManagerNative.handleIncomingUser and stash result for use
375        // when starting services on that session's behalf.
376        @Override
377        public ISession createSession(String packageName, ISessionCallback cb, String tag,
378                int userId) throws RemoteException {
379            final int pid = Binder.getCallingPid();
380            final int uid = Binder.getCallingUid();
381            final long token = Binder.clearCallingIdentity();
382            try {
383                enforcePackageName(packageName, uid);
384                int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
385                        false /* allowAll */, true /* requireFull */, "createSession", packageName);
386                if (cb == null) {
387                    throw new IllegalArgumentException("Controller callback cannot be null");
388                }
389                return createSessionInternal(pid, uid, resolvedUserId, packageName, cb, tag)
390                        .getSessionBinder();
391            } finally {
392                Binder.restoreCallingIdentity(token);
393            }
394        }
395
396        @Override
397        public List<IBinder> getSessions(ComponentName componentName, int userId) {
398            final int pid = Binder.getCallingPid();
399            final int uid = Binder.getCallingUid();
400            final long token = Binder.clearCallingIdentity();
401
402            try {
403                String packageName = null;
404                if (componentName != null) {
405                    // If they gave us a component name verify they own the
406                    // package
407                    packageName = componentName.getPackageName();
408                    enforcePackageName(packageName, uid);
409                }
410                // Check that they can make calls on behalf of the user and
411                // get the final user id
412                int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
413                        true /* allowAll */, true /* requireFull */, "getSessions", packageName);
414                // Check if they have the permissions or their component is
415                // enabled for the user they're calling from.
416                enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
417                ArrayList<IBinder> binders = new ArrayList<IBinder>();
418                synchronized (mLock) {
419                    ArrayList<MediaSessionRecord> records = mPriorityStack
420                            .getActiveSessions(resolvedUserId);
421                    int size = records.size();
422                    for (int i = 0; i < size; i++) {
423                        binders.add(records.get(i).getControllerBinder().asBinder());
424                    }
425                }
426                return binders;
427            } finally {
428                Binder.restoreCallingIdentity(token);
429            }
430        }
431
432        @Override
433        public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
434            if (getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
435                    != PackageManager.PERMISSION_GRANTED) {
436                pw.println("Permission Denial: can't dump MediaSessionService from from pid="
437                        + Binder.getCallingPid()
438                        + ", uid=" + Binder.getCallingUid());
439                return;
440            }
441
442            pw.println("MEDIA SESSION SERVICE (dumpsys media_session)");
443            pw.println();
444
445            synchronized (mLock) {
446                pw.println("Session for calls:" + mPrioritySession);
447                if (mPrioritySession != null) {
448                    mPrioritySession.dump(pw, "");
449                }
450                int count = mRecords.size();
451                pw.println(count + " Sessions:");
452                for (int i = 0; i < count; i++) {
453                    mRecords.get(i).dump(pw, "");
454                    pw.println();
455                }
456                mPriorityStack.dump(pw, "");
457
458                pw.println("Providers:");
459                count = mProviders.size();
460                for (int i = 0; i < count; i++) {
461                    MediaRouteProviderProxy provider = mProviders.get(i);
462                    provider.dump(pw, "");
463                }
464            }
465        }
466    }
467
468}
469