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