blob: 3035521fc894d0db8dfa9bb74d56083b32195765 [file] [log] [blame]
RoboErik01fe6612014-02-13 14:19:04 -08001/*
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
RoboErika278ea72014-04-24 14:49:01 -070019import android.Manifest;
RoboErike7880d82014-04-30 12:48:25 -070020import android.app.ActivityManager;
21import android.content.ComponentName;
RoboErik01fe6612014-02-13 14:19:04 -080022import android.content.Context;
RoboErika278ea72014-04-24 14:49:01 -070023import android.content.pm.PackageManager;
RoboErik07c70772014-03-20 13:33:52 -070024import android.media.routeprovider.RouteRequest;
25import android.media.session.ISession;
26import android.media.session.ISessionCallback;
RoboErike7880d82014-04-30 12:48:25 -070027import android.media.session.ISessionController;
RoboErik07c70772014-03-20 13:33:52 -070028import android.media.session.ISessionManager;
29import android.media.session.RouteInfo;
30import android.media.session.RouteOptions;
RoboErik01fe6612014-02-13 14:19:04 -080031import android.os.Binder;
RoboErik8ae0f342014-02-24 18:02:08 -080032import android.os.Handler;
RoboErike7880d82014-04-30 12:48:25 -070033import android.os.IBinder;
34import android.os.Process;
RoboErik01fe6612014-02-13 14:19:04 -080035import android.os.RemoteException;
RoboErike7880d82014-04-30 12:48:25 -070036import android.os.UserHandle;
37import android.provider.Settings;
RoboErik01fe6612014-02-13 14:19:04 -080038import android.text.TextUtils;
39import android.util.Log;
40
41import com.android.server.SystemService;
RoboErika278ea72014-04-24 14:49:01 -070042import com.android.server.Watchdog;
43import com.android.server.Watchdog.Monitor;
RoboErik01fe6612014-02-13 14:19:04 -080044
RoboErika278ea72014-04-24 14:49:01 -070045import java.io.FileDescriptor;
46import java.io.PrintWriter;
RoboErik01fe6612014-02-13 14:19:04 -080047import java.util.ArrayList;
RoboErike7880d82014-04-30 12:48:25 -070048import java.util.List;
RoboErik01fe6612014-02-13 14:19:04 -080049
50/**
51 * System implementation of MediaSessionManager
52 */
RoboErika278ea72014-04-24 14:49:01 -070053public class MediaSessionService extends SystemService implements Monitor {
RoboErik01fe6612014-02-13 14:19:04 -080054 private static final String TAG = "MediaSessionService";
55 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
56
57 private final SessionManagerImpl mSessionManagerImpl;
RoboErik07c70772014-03-20 13:33:52 -070058 private final MediaRouteProviderWatcher mRouteProviderWatcher;
RoboErik01fe6612014-02-13 14:19:04 -080059
60 private final ArrayList<MediaSessionRecord> mSessions
61 = new ArrayList<MediaSessionRecord>();
RoboErik07c70772014-03-20 13:33:52 -070062 private final ArrayList<MediaRouteProviderProxy> mProviders
63 = new ArrayList<MediaRouteProviderProxy>();
RoboErik01fe6612014-02-13 14:19:04 -080064 private final Object mLock = new Object();
RoboErik8ae0f342014-02-24 18:02:08 -080065 // TODO do we want a separate thread for handling mediasession messages?
66 private final Handler mHandler = new Handler();
RoboErik01fe6612014-02-13 14:19:04 -080067
RoboErike7880d82014-04-30 12:48:25 -070068 private MediaSessionRecord mPrioritySession;
69
RoboErik07c70772014-03-20 13:33:52 -070070 // 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
RoboErik01fe6612014-02-13 14:19:04 -080077 public MediaSessionService(Context context) {
78 super(context);
79 mSessionManagerImpl = new SessionManagerImpl();
RoboErik07c70772014-03-20 13:33:52 -070080 mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
81 mHandler, context.getUserId());
RoboErik01fe6612014-02-13 14:19:04 -080082 }
83
84 @Override
85 public void onStart() {
86 publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
RoboErik07c70772014-03-20 13:33:52 -070087 mRouteProviderWatcher.start();
RoboErika278ea72014-04-24 14:49:01 -070088 Watchdog.getInstance().addMonitor(this);
RoboErik07c70772014-03-20 13:33:52 -070089 }
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 }
RoboErik01fe6612014-02-13 14:19:04 -0800132 }
133
RoboErike7880d82014-04-30 12:48:25 -0700134 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
RoboErika278ea72014-04-24 14:49:01 -0700145 @Override
146 public void monitor() {
147 synchronized (mLock) {
148 // Check for deadlock
149 }
150 }
151
RoboErik01fe6612014-02-13 14:19:04 -0800152 void sessionDied(MediaSessionRecord session) {
RoboErika278ea72014-04-24 14:49:01 -0700153 synchronized (mLock) {
RoboErik01fe6612014-02-13 14:19:04 -0800154 destroySessionLocked(session);
155 }
156 }
157
158 void destroySession(MediaSessionRecord session) {
RoboErika278ea72014-04-24 14:49:01 -0700159 synchronized (mLock) {
RoboErik01fe6612014-02-13 14:19:04 -0800160 destroySessionLocked(session);
161 }
162 }
163
164 private void destroySessionLocked(MediaSessionRecord session) {
165 mSessions.remove(session);
RoboErike7880d82014-04-30 12:48:25 -0700166 if (session == mPrioritySession) {
167 mPrioritySession = null;
168 }
RoboErik01fe6612014-02-13 14:19:04 -0800169 }
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
RoboErike7880d82014-04-30 12:48:25 -0700185 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
RoboErik01fe6612014-02-13 14:19:04 -0800241 private MediaSessionRecord createSessionInternal(int pid, String packageName,
RoboErike7880d82014-04-30 12:48:25 -0700242 ISessionCallback cb, String tag, boolean forCalls) {
RoboErik01fe6612014-02-13 14:19:04 -0800243 synchronized (mLock) {
244 return createSessionLocked(pid, packageName, cb, tag);
245 }
246 }
247
248 private MediaSessionRecord createSessionLocked(int pid, String packageName,
RoboErik07c70772014-03-20 13:33:52 -0700249 ISessionCallback cb, String tag) {
RoboErik8ae0f342014-02-24 18:02:08 -0800250 final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
251 mHandler);
RoboErik01fe6612014-02-13 14:19:04 -0800252 try {
253 cb.asBinder().linkToDeath(session, 0);
254 } catch (RemoteException e) {
255 throw new RuntimeException("Media Session owner died prematurely.", e);
256 }
RoboErika278ea72014-04-24 14:49:01 -0700257 mSessions.add(session);
RoboErik01fe6612014-02-13 14:19:04 -0800258 if (DEBUG) {
259 Log.d(TAG, "Created session for package " + packageName + " with tag " + tag);
260 }
261 return session;
262 }
263
RoboErik07c70772014-03-20 13:33:52 -0700264 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
RoboErike7880d82014-04-30 12:48:25 -0700284 private boolean isSessionDiscoverable(MediaSessionRecord record) {
285 // TODO probably want to check more than if it's published.
286 return record.isPublished();
287 }
288
RoboErik07c70772014-03-20 13:33:52 -0700289 private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
290 = new MediaRouteProviderWatcher.Callback() {
RoboErik01fe6612014-02-13 14:19:04 -0800291 @Override
RoboErik07c70772014-03-20 13:33:52 -0700292 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)
RoboErik01fe6612014-02-13 14:19:04 -0800345 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 }
RoboErike7880d82014-04-30 12:48:25 -0700354 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;
RoboErik01fe6612014-02-13 14:19:04 -0800386 } finally {
387 Binder.restoreCallingIdentity(token);
388 }
389 }
RoboErika278ea72014-04-24 14:49:01 -0700390
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) {
RoboErike7880d82014-04-30 12:48:25 -0700405 pw.println("Session for calls:" + mPrioritySession);
406 if (mPrioritySession != null) {
407 mPrioritySession.dump(pw, "");
408 }
RoboErika278ea72014-04-24 14:49:01 -0700409 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 }
RoboErik01fe6612014-02-13 14:19:04 -0800424 }
425
426}