blob: fb858fcde05a8a5862388b9db62b6d027672ba6f [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;
RoboErika8f95142014-05-05 14:23:49 -070029import android.media.session.PlaybackState;
RoboErik07c70772014-03-20 13:33:52 -070030import android.media.session.RouteInfo;
31import android.media.session.RouteOptions;
RoboErik01fe6612014-02-13 14:19:04 -080032import android.os.Binder;
RoboErik8ae0f342014-02-24 18:02:08 -080033import android.os.Handler;
RoboErike7880d82014-04-30 12:48:25 -070034import android.os.IBinder;
35import android.os.Process;
RoboErik01fe6612014-02-13 14:19:04 -080036import android.os.RemoteException;
RoboErike7880d82014-04-30 12:48:25 -070037import android.os.UserHandle;
38import android.provider.Settings;
RoboErik01fe6612014-02-13 14:19:04 -080039import android.text.TextUtils;
40import android.util.Log;
41
42import com.android.server.SystemService;
RoboErika278ea72014-04-24 14:49:01 -070043import com.android.server.Watchdog;
44import com.android.server.Watchdog.Monitor;
RoboErik01fe6612014-02-13 14:19:04 -080045
RoboErika278ea72014-04-24 14:49:01 -070046import java.io.FileDescriptor;
47import java.io.PrintWriter;
RoboErik01fe6612014-02-13 14:19:04 -080048import java.util.ArrayList;
RoboErike7880d82014-04-30 12:48:25 -070049import java.util.List;
RoboErik01fe6612014-02-13 14:19:04 -080050
51/**
52 * System implementation of MediaSessionManager
53 */
RoboErika278ea72014-04-24 14:49:01 -070054public class MediaSessionService extends SystemService implements Monitor {
RoboErik01fe6612014-02-13 14:19:04 -080055 private static final String TAG = "MediaSessionService";
56 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
57
58 private final SessionManagerImpl mSessionManagerImpl;
RoboErik07c70772014-03-20 13:33:52 -070059 private final MediaRouteProviderWatcher mRouteProviderWatcher;
RoboErika8f95142014-05-05 14:23:49 -070060 private final MediaSessionStack mPriorityStack;
RoboErik01fe6612014-02-13 14:19:04 -080061
RoboErika8f95142014-05-05 14:23:49 -070062 private final ArrayList<MediaSessionRecord> mRecords = new ArrayList<MediaSessionRecord>();
RoboErik07c70772014-03-20 13:33:52 -070063 private final ArrayList<MediaRouteProviderProxy> mProviders
64 = new ArrayList<MediaRouteProviderProxy>();
RoboErik01fe6612014-02-13 14:19:04 -080065 private final Object mLock = new Object();
RoboErik8ae0f342014-02-24 18:02:08 -080066 // TODO do we want a separate thread for handling mediasession messages?
67 private final Handler mHandler = new Handler();
RoboErik01fe6612014-02-13 14:19:04 -080068
RoboErike7880d82014-04-30 12:48:25 -070069 private MediaSessionRecord mPrioritySession;
70
RoboErik07c70772014-03-20 13:33:52 -070071 // 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
RoboErik01fe6612014-02-13 14:19:04 -080078 public MediaSessionService(Context context) {
79 super(context);
80 mSessionManagerImpl = new SessionManagerImpl();
RoboErik07c70772014-03-20 13:33:52 -070081 mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
82 mHandler, context.getUserId());
RoboErika8f95142014-05-05 14:23:49 -070083 mPriorityStack = new MediaSessionStack();
RoboErik01fe6612014-02-13 14:19:04 -080084 }
85
86 @Override
87 public void onStart() {
88 publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
RoboErik07c70772014-03-20 13:33:52 -070089 mRouteProviderWatcher.start();
RoboErika278ea72014-04-24 14:49:01 -070090 Watchdog.getInstance().addMonitor(this);
RoboErik07c70772014-03-20 13:33:52 -070091 }
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 }
RoboErik01fe6612014-02-13 14:19:04 -0800134 }
135
RoboErika8f95142014-05-05 14:23:49 -0700136 public void updateSession(MediaSessionRecord record) {
RoboErike7880d82014-04-30 12:48:25 -0700137 synchronized (mLock) {
RoboErika8f95142014-05-05 14:23:49 -0700138 mPriorityStack.onSessionStateChange(record);
RoboErike7880d82014-04-30 12:48:25 -0700139 if (record.isSystemPriority()) {
RoboErika8f95142014-05-05 14:23:49 -0700140 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 }
RoboErike7880d82014-04-30 12:48:25 -0700149 }
RoboErike7880d82014-04-30 12:48:25 -0700150 }
151 }
152 }
153
RoboErika8f95142014-05-05 14:23:49 -0700154 public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
155 synchronized (mLock) {
156 mPriorityStack.onPlaystateChange(record, oldState, newState);
157 }
158 }
159
RoboErika278ea72014-04-24 14:49:01 -0700160 @Override
161 public void monitor() {
162 synchronized (mLock) {
163 // Check for deadlock
164 }
165 }
166
RoboErik01fe6612014-02-13 14:19:04 -0800167 void sessionDied(MediaSessionRecord session) {
RoboErika278ea72014-04-24 14:49:01 -0700168 synchronized (mLock) {
RoboErik01fe6612014-02-13 14:19:04 -0800169 destroySessionLocked(session);
170 }
171 }
172
173 void destroySession(MediaSessionRecord session) {
RoboErika278ea72014-04-24 14:49:01 -0700174 synchronized (mLock) {
RoboErik01fe6612014-02-13 14:19:04 -0800175 destroySessionLocked(session);
176 }
177 }
178
179 private void destroySessionLocked(MediaSessionRecord session) {
RoboErika8f95142014-05-05 14:23:49 -0700180 mRecords.remove(session);
181 mPriorityStack.removeSession(session);
RoboErike7880d82014-04-30 12:48:25 -0700182 if (session == mPrioritySession) {
183 mPrioritySession = null;
184 }
RoboErik01fe6612014-02-13 14:19:04 -0800185 }
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
RoboErike7880d82014-04-30 12:48:25 -0700201 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
RoboErik01fe6612014-02-13 14:19:04 -0800257 private MediaSessionRecord createSessionInternal(int pid, String packageName,
RoboErike7880d82014-04-30 12:48:25 -0700258 ISessionCallback cb, String tag, boolean forCalls) {
RoboErik01fe6612014-02-13 14:19:04 -0800259 synchronized (mLock) {
260 return createSessionLocked(pid, packageName, cb, tag);
261 }
262 }
263
264 private MediaSessionRecord createSessionLocked(int pid, String packageName,
RoboErik07c70772014-03-20 13:33:52 -0700265 ISessionCallback cb, String tag) {
RoboErik8ae0f342014-02-24 18:02:08 -0800266 final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
267 mHandler);
RoboErik01fe6612014-02-13 14:19:04 -0800268 try {
269 cb.asBinder().linkToDeath(session, 0);
270 } catch (RemoteException e) {
271 throw new RuntimeException("Media Session owner died prematurely.", e);
272 }
RoboErika8f95142014-05-05 14:23:49 -0700273 mRecords.add(session);
274 mPriorityStack.addSession(session);
RoboErik01fe6612014-02-13 14:19:04 -0800275 if (DEBUG) {
276 Log.d(TAG, "Created session for package " + packageName + " with tag " + tag);
277 }
278 return session;
279 }
280
RoboErika8f95142014-05-05 14:23:49 -0700281 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
RoboErik07c70772014-03-20 13:33:52 -0700291 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
RoboErike7880d82014-04-30 12:48:25 -0700301 private boolean isSessionDiscoverable(MediaSessionRecord record) {
302 // TODO probably want to check more than if it's published.
RoboErika8f95142014-05-05 14:23:49 -0700303 return record.isActive();
RoboErike7880d82014-04-30 12:48:25 -0700304 }
305
RoboErik07c70772014-03-20 13:33:52 -0700306 private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
307 = new MediaRouteProviderWatcher.Callback() {
RoboErik01fe6612014-02-13 14:19:04 -0800308 @Override
RoboErik07c70772014-03-20 13:33:52 -0700309 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) {
RoboErika8f95142014-05-05 14:23:49 -0700337 MediaSessionRecord record = mRecords.get(index);
RoboErik07c70772014-03-20 13:33:52 -0700338 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) {
RoboErika8f95142014-05-05 14:23:49 -0700349 MediaSessionRecord session = mRecords.get(index);
RoboErik07c70772014-03-20 13:33:52 -0700350 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)
RoboErik01fe6612014-02-13 14:19:04 -0800362 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 }
RoboErike7880d82014-04-30 12:48:25 -0700371 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) {
RoboErike7880d82014-04-30 12:48:25 -0700379 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) {
RoboErika8f95142014-05-05 14:23:49 -0700394 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());
RoboErike7880d82014-04-30 12:48:25 -0700399 }
400 }
401 return binders;
RoboErik01fe6612014-02-13 14:19:04 -0800402 } finally {
403 Binder.restoreCallingIdentity(token);
404 }
405 }
RoboErika278ea72014-04-24 14:49:01 -0700406
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) {
RoboErike7880d82014-04-30 12:48:25 -0700421 pw.println("Session for calls:" + mPrioritySession);
422 if (mPrioritySession != null) {
423 mPrioritySession.dump(pw, "");
424 }
RoboErika8f95142014-05-05 14:23:49 -0700425 int count = mRecords.size();
426 pw.println(count + " Sessions:");
RoboErika278ea72014-04-24 14:49:01 -0700427 for (int i = 0; i < count; i++) {
RoboErika8f95142014-05-05 14:23:49 -0700428 mRecords.get(i).dump(pw, "");
RoboErika278ea72014-04-24 14:49:01 -0700429 pw.println();
RoboErika278ea72014-04-24 14:49:01 -0700430 }
RoboErika8f95142014-05-05 14:23:49 -0700431 mPriorityStack.dumpLocked(pw, "");
432
RoboErika278ea72014-04-24 14:49:01 -0700433 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 }
RoboErik01fe6612014-02-13 14:19:04 -0800441 }
442
443}