MediaSessionService.java revision a278ea7cecb59a73586e5dd74ec05e85caa370c5
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.content.Context;
21import android.content.pm.PackageManager;
22import android.media.routeprovider.RouteRequest;
23import android.media.session.ISession;
24import android.media.session.ISessionCallback;
25import android.media.session.ISessionManager;
26import android.media.session.RouteInfo;
27import android.media.session.RouteOptions;
28import android.os.Binder;
29import android.os.Handler;
30import android.os.RemoteException;
31import android.text.TextUtils;
32import android.util.Log;
33
34import com.android.server.SystemService;
35import com.android.server.Watchdog;
36import com.android.server.Watchdog.Monitor;
37
38import java.io.FileDescriptor;
39import java.io.PrintWriter;
40import java.util.ArrayList;
41
42/**
43 * System implementation of MediaSessionManager
44 */
45public class MediaSessionService extends SystemService implements Monitor {
46    private static final String TAG = "MediaSessionService";
47    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
48
49    private final SessionManagerImpl mSessionManagerImpl;
50    private final MediaRouteProviderWatcher mRouteProviderWatcher;
51
52    private final ArrayList<MediaSessionRecord> mSessions
53            = new ArrayList<MediaSessionRecord>();
54    private final ArrayList<MediaRouteProviderProxy> mProviders
55            = new ArrayList<MediaRouteProviderProxy>();
56    private final Object mLock = new Object();
57    // TODO do we want a separate thread for handling mediasession messages?
58    private final Handler mHandler = new Handler();
59
60    // Used to keep track of the current request to show routes for a specific
61    // session so we drop late callbacks properly.
62    private int mShowRoutesRequestId = 0;
63
64    // TODO refactor to have per user state. See MediaRouterService for an
65    // example
66
67    public MediaSessionService(Context context) {
68        super(context);
69        mSessionManagerImpl = new SessionManagerImpl();
70        mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
71                mHandler, context.getUserId());
72    }
73
74    @Override
75    public void onStart() {
76        publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
77        mRouteProviderWatcher.start();
78        Watchdog.getInstance().addMonitor(this);
79    }
80
81    /**
82     * Should trigger showing the Media route picker dialog. Right now it just
83     * kicks off a query to all the providers to get routes.
84     *
85     * @param record The session to show the picker for.
86     */
87    public void showRoutePickerForSession(MediaSessionRecord record) {
88        // TODO for now just toggle the route to test (we will only have one
89        // match for now)
90        if (record.getRoute() != null) {
91            // For now send null to mean the local route
92            record.selectRoute(null);
93            return;
94        }
95        mShowRoutesRequestId++;
96        ArrayList<MediaRouteProviderProxy> providers = mRouteProviderWatcher.getProviders();
97        for (int i = providers.size() - 1; i >= 0; i--) {
98            MediaRouteProviderProxy provider = providers.get(i);
99            provider.getRoutes(record, mShowRoutesRequestId);
100        }
101    }
102
103    /**
104     * Connect a session to the given route.
105     *
106     * @param session The session to connect.
107     * @param route The route to connect to.
108     * @param options The options to use for the connection.
109     */
110    public void connectToRoute(MediaSessionRecord session, RouteInfo route,
111            RouteOptions options) {
112        synchronized (mLock) {
113            MediaRouteProviderProxy proxy = getProviderLocked(route.getProvider());
114            if (proxy == null) {
115                Log.w(TAG, "Provider for route " + route.getName() + " does not exist.");
116                return;
117            }
118            RouteRequest request = new RouteRequest(session.getSessionInfo(), options, true);
119            // TODO make connect an async call to a ThreadPoolExecutor
120            proxy.connectToRoute(session, route, request);
121        }
122    }
123
124    @Override
125    public void monitor() {
126        synchronized (mLock) {
127            // Check for deadlock
128        }
129    }
130
131    void sessionDied(MediaSessionRecord session) {
132        synchronized (mLock) {
133            destroySessionLocked(session);
134        }
135    }
136
137    void destroySession(MediaSessionRecord session) {
138        synchronized (mLock) {
139            destroySessionLocked(session);
140        }
141    }
142
143    private void destroySessionLocked(MediaSessionRecord session) {
144        mSessions.remove(session);
145    }
146
147    private void enforcePackageName(String packageName, int uid) {
148        if (TextUtils.isEmpty(packageName)) {
149            throw new IllegalArgumentException("packageName may not be empty");
150        }
151        String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
152        final int packageCount = packages.length;
153        for (int i = 0; i < packageCount; i++) {
154            if (packageName.equals(packages[i])) {
155                return;
156            }
157        }
158        throw new IllegalArgumentException("packageName is not owned by the calling process");
159    }
160
161    private MediaSessionRecord createSessionInternal(int pid, String packageName,
162            ISessionCallback cb, String tag) {
163        synchronized (mLock) {
164            return createSessionLocked(pid, packageName, cb, tag);
165        }
166    }
167
168    private MediaSessionRecord createSessionLocked(int pid, String packageName,
169            ISessionCallback cb, String tag) {
170        final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
171                mHandler);
172        try {
173            cb.asBinder().linkToDeath(session, 0);
174        } catch (RemoteException e) {
175            throw new RuntimeException("Media Session owner died prematurely.", e);
176        }
177        mSessions.add(session);
178        if (DEBUG) {
179            Log.d(TAG, "Created session for package " + packageName + " with tag " + tag);
180        }
181        return session;
182    }
183
184    private MediaRouteProviderProxy getProviderLocked(String providerId) {
185        for (int i = mProviders.size() - 1; i >= 0; i--) {
186            MediaRouteProviderProxy provider = mProviders.get(i);
187            if (TextUtils.equals(providerId, provider.getId())) {
188                return provider;
189            }
190        }
191        return null;
192    }
193
194    private int findIndexOfSessionForIdLocked(String sessionId) {
195        for (int i = mSessions.size() - 1; i >= 0; i--) {
196            MediaSessionRecord session = mSessions.get(i);
197            if (TextUtils.equals(session.getSessionInfo().getId(), sessionId)) {
198                return i;
199            }
200        }
201        return -1;
202    }
203
204    private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
205            = new MediaRouteProviderWatcher.Callback() {
206        @Override
207        public void removeProvider(MediaRouteProviderProxy provider) {
208            synchronized (mLock) {
209                mProviders.remove(provider);
210                provider.setRoutesListener(null);
211                provider.setInterested(false);
212            }
213        }
214
215        @Override
216        public void addProvider(MediaRouteProviderProxy provider) {
217            synchronized (mLock) {
218                mProviders.add(provider);
219                provider.setRoutesListener(mRoutesCallback);
220                provider.setInterested(true);
221            }
222        }
223    };
224
225    private MediaRouteProviderProxy.RoutesListener mRoutesCallback
226            = new MediaRouteProviderProxy.RoutesListener() {
227        @Override
228        public void onRoutesUpdated(String sessionId, ArrayList<RouteInfo> routes,
229                int reqId) {
230            // TODO for now select the first route to test, eventually add the
231            // new routes to the dialog if it is still open
232            synchronized (mLock) {
233                int index = findIndexOfSessionForIdLocked(sessionId);
234                if (index != -1 && routes != null && routes.size() > 0) {
235                    MediaSessionRecord record = mSessions.get(index);
236                    record.selectRoute(routes.get(0));
237                }
238            }
239        }
240
241        @Override
242        public void onRouteConnected(String sessionId, RouteInfo route,
243                RouteRequest options, RouteConnectionRecord connection) {
244            synchronized (mLock) {
245                int index = findIndexOfSessionForIdLocked(sessionId);
246                if (index != -1) {
247                    MediaSessionRecord session = mSessions.get(index);
248                    session.setRouteConnected(route, options.getConnectionOptions(), connection);
249                }
250            }
251        }
252    };
253
254    class SessionManagerImpl extends ISessionManager.Stub {
255        // TODO add createSessionAsUser, pass user-id to
256        // ActivityManagerNative.handleIncomingUser and stash result for use
257        // when starting services on that session's behalf.
258        @Override
259        public ISession createSession(String packageName, ISessionCallback cb, String tag)
260                throws RemoteException {
261            final int pid = Binder.getCallingPid();
262            final int uid = Binder.getCallingUid();
263            final long token = Binder.clearCallingIdentity();
264            try {
265                enforcePackageName(packageName, uid);
266                if (cb == null) {
267                    throw new IllegalArgumentException("Controller callback cannot be null");
268                }
269                return createSessionInternal(pid, packageName, cb, tag).getSessionBinder();
270            } finally {
271                Binder.restoreCallingIdentity(token);
272            }
273        }
274
275        @Override
276        public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
277            if (getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
278                    != PackageManager.PERMISSION_GRANTED) {
279                pw.println("Permission Denial: can't dump MediaSessionService from from pid="
280                        + Binder.getCallingPid()
281                        + ", uid=" + Binder.getCallingUid());
282                return;
283            }
284
285            pw.println("MEDIA SESSION SERVICE (dumpsys media_session)");
286            pw.println();
287
288            synchronized (mLock) {
289                int count = mSessions.size();
290                pw.println("Sessions - have " + count + " states:");
291                for (int i = 0; i < count; i++) {
292                    MediaSessionRecord record = mSessions.get(i);
293                    pw.println();
294                    record.dump(pw, "");
295                }
296                pw.println("Providers:");
297                count = mProviders.size();
298                for (int i = 0; i < count; i++) {
299                    MediaRouteProviderProxy provider = mProviders.get(i);
300                    provider.dump(pw, "");
301                }
302            }
303        }
304    }
305
306}
307