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