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