blob: bc9137049726bf7c5a239a865f9c74ab65fa454d [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
19import android.content.Context;
RoboErik07c70772014-03-20 13:33:52 -070020import android.media.routeprovider.RouteRequest;
21import android.media.session.ISession;
22import android.media.session.ISessionCallback;
23import android.media.session.ISessionManager;
24import android.media.session.RouteInfo;
25import android.media.session.RouteOptions;
RoboErik01fe6612014-02-13 14:19:04 -080026import android.os.Binder;
RoboErik8ae0f342014-02-24 18:02:08 -080027import android.os.Handler;
RoboErik01fe6612014-02-13 14:19:04 -080028import android.os.RemoteException;
29import android.text.TextUtils;
30import android.util.Log;
31
32import com.android.server.SystemService;
33
34import java.util.ArrayList;
35
36/**
37 * System implementation of MediaSessionManager
38 */
39public class MediaSessionService extends SystemService {
40 private static final String TAG = "MediaSessionService";
41 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
42
43 private final SessionManagerImpl mSessionManagerImpl;
RoboErik07c70772014-03-20 13:33:52 -070044 private final MediaRouteProviderWatcher mRouteProviderWatcher;
RoboErik01fe6612014-02-13 14:19:04 -080045
46 private final ArrayList<MediaSessionRecord> mSessions
47 = new ArrayList<MediaSessionRecord>();
RoboErik07c70772014-03-20 13:33:52 -070048 private final ArrayList<MediaRouteProviderProxy> mProviders
49 = new ArrayList<MediaRouteProviderProxy>();
RoboErik01fe6612014-02-13 14:19:04 -080050 private final Object mLock = new Object();
RoboErik8ae0f342014-02-24 18:02:08 -080051 // TODO do we want a separate thread for handling mediasession messages?
52 private final Handler mHandler = new Handler();
RoboErik01fe6612014-02-13 14:19:04 -080053
RoboErik07c70772014-03-20 13:33:52 -070054 // Used to keep track of the current request to show routes for a specific
55 // session so we drop late callbacks properly.
56 private int mShowRoutesRequestId = 0;
57
58 // TODO refactor to have per user state. See MediaRouterService for an
59 // example
60
RoboErik01fe6612014-02-13 14:19:04 -080061 public MediaSessionService(Context context) {
62 super(context);
63 mSessionManagerImpl = new SessionManagerImpl();
RoboErik07c70772014-03-20 13:33:52 -070064 mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
65 mHandler, context.getUserId());
RoboErik01fe6612014-02-13 14:19:04 -080066 }
67
68 @Override
69 public void onStart() {
70 publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
RoboErik07c70772014-03-20 13:33:52 -070071 mRouteProviderWatcher.start();
72 }
73
74 /**
75 * Should trigger showing the Media route picker dialog. Right now it just
76 * kicks off a query to all the providers to get routes.
77 *
78 * @param record The session to show the picker for.
79 */
80 public void showRoutePickerForSession(MediaSessionRecord record) {
81 // TODO for now just toggle the route to test (we will only have one
82 // match for now)
83 if (record.getRoute() != null) {
84 // For now send null to mean the local route
85 record.selectRoute(null);
86 return;
87 }
88 mShowRoutesRequestId++;
89 ArrayList<MediaRouteProviderProxy> providers = mRouteProviderWatcher.getProviders();
90 for (int i = providers.size() - 1; i >= 0; i--) {
91 MediaRouteProviderProxy provider = providers.get(i);
92 provider.getRoutes(record, mShowRoutesRequestId);
93 }
94 }
95
96 /**
97 * Connect a session to the given route.
98 *
99 * @param session The session to connect.
100 * @param route The route to connect to.
101 * @param options The options to use for the connection.
102 */
103 public void connectToRoute(MediaSessionRecord session, RouteInfo route,
104 RouteOptions options) {
105 synchronized (mLock) {
106 MediaRouteProviderProxy proxy = getProviderLocked(route.getProvider());
107 if (proxy == null) {
108 Log.w(TAG, "Provider for route " + route.getName() + " does not exist.");
109 return;
110 }
111 RouteRequest request = new RouteRequest(session.getSessionInfo(), options, true);
112 // TODO make connect an async call to a ThreadPoolExecutor
113 proxy.connectToRoute(session, route, request);
114 }
RoboErik01fe6612014-02-13 14:19:04 -0800115 }
116
117 void sessionDied(MediaSessionRecord session) {
118 synchronized (mSessions) {
119 destroySessionLocked(session);
120 }
121 }
122
123 void destroySession(MediaSessionRecord session) {
124 synchronized (mSessions) {
125 destroySessionLocked(session);
126 }
127 }
128
129 private void destroySessionLocked(MediaSessionRecord session) {
130 mSessions.remove(session);
131 }
132
133 private void enforcePackageName(String packageName, int uid) {
134 if (TextUtils.isEmpty(packageName)) {
135 throw new IllegalArgumentException("packageName may not be empty");
136 }
137 String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
138 final int packageCount = packages.length;
139 for (int i = 0; i < packageCount; i++) {
140 if (packageName.equals(packages[i])) {
141 return;
142 }
143 }
144 throw new IllegalArgumentException("packageName is not owned by the calling process");
145 }
146
147 private MediaSessionRecord createSessionInternal(int pid, String packageName,
RoboErik07c70772014-03-20 13:33:52 -0700148 ISessionCallback cb, String tag) {
RoboErik01fe6612014-02-13 14:19:04 -0800149 synchronized (mLock) {
150 return createSessionLocked(pid, packageName, cb, tag);
151 }
152 }
153
154 private MediaSessionRecord createSessionLocked(int pid, String packageName,
RoboErik07c70772014-03-20 13:33:52 -0700155 ISessionCallback cb, String tag) {
RoboErik8ae0f342014-02-24 18:02:08 -0800156 final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
157 mHandler);
RoboErik01fe6612014-02-13 14:19:04 -0800158 try {
159 cb.asBinder().linkToDeath(session, 0);
160 } catch (RemoteException e) {
161 throw new RuntimeException("Media Session owner died prematurely.", e);
162 }
163 synchronized (mSessions) {
164 mSessions.add(session);
165 }
166 if (DEBUG) {
167 Log.d(TAG, "Created session for package " + packageName + " with tag " + tag);
168 }
169 return session;
170 }
171
RoboErik07c70772014-03-20 13:33:52 -0700172 private MediaRouteProviderProxy getProviderLocked(String providerId) {
173 for (int i = mProviders.size() - 1; i >= 0; i--) {
174 MediaRouteProviderProxy provider = mProviders.get(i);
175 if (TextUtils.equals(providerId, provider.getId())) {
176 return provider;
177 }
178 }
179 return null;
180 }
181
182 private int findIndexOfSessionForIdLocked(String sessionId) {
183 for (int i = mSessions.size() - 1; i >= 0; i--) {
184 MediaSessionRecord session = mSessions.get(i);
185 if (TextUtils.equals(session.getSessionInfo().getId(), sessionId)) {
186 return i;
187 }
188 }
189 return -1;
190 }
191
192 private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
193 = new MediaRouteProviderWatcher.Callback() {
RoboErik01fe6612014-02-13 14:19:04 -0800194 @Override
RoboErik07c70772014-03-20 13:33:52 -0700195 public void removeProvider(MediaRouteProviderProxy provider) {
196 synchronized (mLock) {
197 mProviders.remove(provider);
198 provider.setRoutesListener(null);
199 provider.setInterested(false);
200 }
201 }
202
203 @Override
204 public void addProvider(MediaRouteProviderProxy provider) {
205 synchronized (mLock) {
206 mProviders.add(provider);
207 provider.setRoutesListener(mRoutesCallback);
208 provider.setInterested(true);
209 }
210 }
211 };
212
213 private MediaRouteProviderProxy.RoutesListener mRoutesCallback
214 = new MediaRouteProviderProxy.RoutesListener() {
215 @Override
216 public void onRoutesUpdated(String sessionId, ArrayList<RouteInfo> routes,
217 int reqId) {
218 // TODO for now select the first route to test, eventually add the
219 // new routes to the dialog if it is still open
220 synchronized (mLock) {
221 int index = findIndexOfSessionForIdLocked(sessionId);
222 if (index != -1 && routes != null && routes.size() > 0) {
223 MediaSessionRecord record = mSessions.get(index);
224 record.selectRoute(routes.get(0));
225 }
226 }
227 }
228
229 @Override
230 public void onRouteConnected(String sessionId, RouteInfo route,
231 RouteRequest options, RouteConnectionRecord connection) {
232 synchronized (mLock) {
233 int index = findIndexOfSessionForIdLocked(sessionId);
234 if (index != -1) {
235 MediaSessionRecord session = mSessions.get(index);
236 session.setRouteConnected(route, options.getConnectionOptions(), connection);
237 }
238 }
239 }
240 };
241
242 class SessionManagerImpl extends ISessionManager.Stub {
243 // TODO add createSessionAsUser, pass user-id to
244 // ActivityManagerNative.handleIncomingUser and stash result for use
245 // when starting services on that session's behalf.
246 @Override
247 public ISession createSession(String packageName, ISessionCallback cb, String tag)
RoboErik01fe6612014-02-13 14:19:04 -0800248 throws RemoteException {
249 final int pid = Binder.getCallingPid();
250 final int uid = Binder.getCallingUid();
251 final long token = Binder.clearCallingIdentity();
252 try {
253 enforcePackageName(packageName, uid);
254 if (cb == null) {
255 throw new IllegalArgumentException("Controller callback cannot be null");
256 }
257 return createSessionInternal(pid, packageName, cb, tag).getSessionBinder();
258 } finally {
259 Binder.restoreCallingIdentity(token);
260 }
261 }
262 }
263
264}