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