• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.service.media;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SdkConstant;
22 import android.annotation.SdkConstant.SdkConstantType;
23 import android.app.Service;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ParceledListSlice;
27 import android.media.browse.MediaBrowser;
28 import android.media.session.MediaSession;
29 import android.os.Binder;
30 import android.os.Bundle;
31 import android.os.IBinder;
32 import android.os.Handler;
33 import android.os.RemoteException;
34 import android.os.ResultReceiver;
35 import android.service.media.IMediaBrowserService;
36 import android.service.media.IMediaBrowserServiceCallbacks;
37 import android.text.TextUtils;
38 import android.util.ArrayMap;
39 import android.util.Log;
40 
41 import java.io.FileDescriptor;
42 import java.io.PrintWriter;
43 import java.util.HashSet;
44 import java.util.List;
45 
46 /**
47  * Base class for media browse services.
48  * <p>
49  * Media browse services enable applications to browse media content provided by an application
50  * and ask the application to start playing it.  They may also be used to control content that
51  * is already playing by way of a {@link MediaSession}.
52  * </p>
53  *
54  * To extend this class, you must declare the service in your manifest file with
55  * an intent filter with the {@link #SERVICE_INTERFACE} action.
56  *
57  * For example:
58  * </p><pre>
59  * &lt;service android:name=".MyMediaBrowserService"
60  *          android:label="&#64;string/service_name" >
61  *     &lt;intent-filter>
62  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
63  *     &lt;/intent-filter>
64  * &lt;/service>
65  * </pre>
66  *
67  */
68 public abstract class MediaBrowserService extends Service {
69     private static final String TAG = "MediaBrowserService";
70     private static final boolean DBG = false;
71 
72     /**
73      * The {@link Intent} that must be declared as handled by the service.
74      */
75     @SdkConstant(SdkConstantType.SERVICE_ACTION)
76     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
77 
78     /**
79      * A key for passing the MediaItem to the ResultReceiver in getItem.
80      *
81      * @hide
82      */
83     public static final String KEY_MEDIA_ITEM = "media_item";
84 
85     private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
86     private final Handler mHandler = new Handler();
87     private ServiceBinder mBinder;
88     MediaSession.Token mSession;
89 
90     /**
91      * All the info about a connection.
92      */
93     private class ConnectionRecord {
94         String pkg;
95         Bundle rootHints;
96         IMediaBrowserServiceCallbacks callbacks;
97         BrowserRoot root;
98         HashSet<String> subscriptions = new HashSet();
99     }
100 
101     /**
102      * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
103      * <p>
104      * Each of the methods that takes one of these to send the result must call
105      * {@link #sendResult} to respond to the caller with the given results.  If those
106      * functions return without calling {@link #sendResult}, they must instead call
107      * {@link #detach} before returning, and then may call {@link #sendResult} when
108      * they are done.  If more than one of those methods is called, an exception will
109      * be thrown.
110      *
111      * @see MediaBrowserService#onLoadChildren
112      * @see MediaBrowserService#onGetMediaItem
113      */
114     public class Result<T> {
115         private Object mDebug;
116         private boolean mDetachCalled;
117         private boolean mSendResultCalled;
118 
Result(Object debug)119         Result(Object debug) {
120             mDebug = debug;
121         }
122 
123         /**
124          * Send the result back to the caller.
125          */
sendResult(T result)126         public void sendResult(T result) {
127             if (mSendResultCalled) {
128                 throw new IllegalStateException("sendResult() called twice for: " + mDebug);
129             }
130             mSendResultCalled = true;
131             onResultSent(result);
132         }
133 
134         /**
135          * Detach this message from the current thread and allow the {@link #sendResult}
136          * call to happen later.
137          */
detach()138         public void detach() {
139             if (mDetachCalled) {
140                 throw new IllegalStateException("detach() called when detach() had already"
141                         + " been called for: " + mDebug);
142             }
143             if (mSendResultCalled) {
144                 throw new IllegalStateException("detach() called when sendResult() had already"
145                         + " been called for: " + mDebug);
146             }
147             mDetachCalled = true;
148         }
149 
isDone()150         boolean isDone() {
151             return mDetachCalled || mSendResultCalled;
152         }
153 
154         /**
155          * Called when the result is sent, after assertions about not being called twice
156          * have happened.
157          */
onResultSent(T result)158         void onResultSent(T result) {
159         }
160     }
161 
162     private class ServiceBinder extends IMediaBrowserService.Stub {
163         @Override
connect(final String pkg, final Bundle rootHints, final IMediaBrowserServiceCallbacks callbacks)164         public void connect(final String pkg, final Bundle rootHints,
165                 final IMediaBrowserServiceCallbacks callbacks) {
166 
167             final int uid = Binder.getCallingUid();
168             if (!isValidPackage(pkg, uid)) {
169                 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
170                         + " package=" + pkg);
171             }
172 
173             mHandler.post(new Runnable() {
174                     @Override
175                     public void run() {
176                         final IBinder b = callbacks.asBinder();
177 
178                         // Clear out the old subscriptions.  We are getting new ones.
179                         mConnections.remove(b);
180 
181                         final ConnectionRecord connection = new ConnectionRecord();
182                         connection.pkg = pkg;
183                         connection.rootHints = rootHints;
184                         connection.callbacks = callbacks;
185 
186                         connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
187 
188                         // If they didn't return something, don't allow this client.
189                         if (connection.root == null) {
190                             Log.i(TAG, "No root for client " + pkg + " from service "
191                                     + getClass().getName());
192                             try {
193                                 callbacks.onConnectFailed();
194                             } catch (RemoteException ex) {
195                                 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
196                                         + "pkg=" + pkg);
197                             }
198                         } else {
199                             try {
200                                 mConnections.put(b, connection);
201                                 if (mSession != null) {
202                                     callbacks.onConnect(connection.root.getRootId(),
203                                             mSession, connection.root.getExtras());
204                                 }
205                             } catch (RemoteException ex) {
206                                 Log.w(TAG, "Calling onConnect() failed. Dropping client. "
207                                         + "pkg=" + pkg);
208                                 mConnections.remove(b);
209                             }
210                         }
211                     }
212                 });
213         }
214 
215         @Override
disconnect(final IMediaBrowserServiceCallbacks callbacks)216         public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
217             mHandler.post(new Runnable() {
218                     @Override
219                     public void run() {
220                         final IBinder b = callbacks.asBinder();
221 
222                         // Clear out the old subscriptions.  We are getting new ones.
223                         final ConnectionRecord old = mConnections.remove(b);
224                         if (old != null) {
225                             // TODO
226                         }
227                     }
228                 });
229         }
230 
231 
232         @Override
addSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks)233         public void addSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks) {
234             mHandler.post(new Runnable() {
235                     @Override
236                     public void run() {
237                         final IBinder b = callbacks.asBinder();
238 
239                         // Get the record for the connection
240                         final ConnectionRecord connection = mConnections.get(b);
241                         if (connection == null) {
242                             Log.w(TAG, "addSubscription for callback that isn't registered id="
243                                 + id);
244                             return;
245                         }
246 
247                         MediaBrowserService.this.addSubscription(id, connection);
248                     }
249                 });
250         }
251 
252         @Override
removeSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks)253         public void removeSubscription(final String id,
254                 final IMediaBrowserServiceCallbacks callbacks) {
255             mHandler.post(new Runnable() {
256                 @Override
257                 public void run() {
258                     final IBinder b = callbacks.asBinder();
259 
260                     ConnectionRecord connection = mConnections.get(b);
261                     if (connection == null) {
262                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
263                                 + id);
264                         return;
265                     }
266                     if (!connection.subscriptions.remove(id)) {
267                         Log.w(TAG, "removeSubscription called for " + id
268                                 + " which is not subscribed");
269                     }
270                 }
271             });
272         }
273 
274         @Override
getMediaItem(final String mediaId, final ResultReceiver receiver)275         public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
276             if (TextUtils.isEmpty(mediaId) || receiver == null) {
277                 return;
278             }
279 
280             mHandler.post(new Runnable() {
281                 @Override
282                 public void run() {
283                     performLoadItem(mediaId, receiver);
284                 }
285             });
286         }
287     }
288 
289     @Override
onCreate()290     public void onCreate() {
291         super.onCreate();
292         mBinder = new ServiceBinder();
293     }
294 
295     @Override
onBind(Intent intent)296     public IBinder onBind(Intent intent) {
297         if (SERVICE_INTERFACE.equals(intent.getAction())) {
298             return mBinder;
299         }
300         return null;
301     }
302 
303     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)304     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
305     }
306 
307     /**
308      * Called to get the root information for browsing by a particular client.
309      * <p>
310      * The implementation should verify that the client package has permission
311      * to access browse media information before returning the root id; it
312      * should return null if the client is not allowed to access this
313      * information.
314      * </p>
315      *
316      * @param clientPackageName The package name of the application which is
317      *            requesting access to browse media.
318      * @param clientUid The uid of the application which is requesting access to
319      *            browse media.
320      * @param rootHints An optional bundle of service-specific arguments to send
321      *            to the media browse service when connecting and retrieving the
322      *            root id for browsing, or null if none. The contents of this
323      *            bundle may affect the information returned when browsing.
324      * @return The {@link BrowserRoot} for accessing this app's content or null.
325      */
onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)326     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
327             int clientUid, @Nullable Bundle rootHints);
328 
329     /**
330      * Called to get information about the children of a media item.
331      * <p>
332      * Implementations must call {@link Result#sendResult result.sendResult}
333      * with the list of children. If loading the children will be an expensive
334      * operation that should be performed on another thread,
335      * {@link Result#detach result.detach} may be called before returning from
336      * this function, and then {@link Result#sendResult result.sendResult}
337      * called when the loading is complete.
338      *
339      * @param parentId The id of the parent media item whose children are to be
340      *            queried.
341      * @param result The Result to send the list of children to, or null if the
342      *            id is invalid.
343      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result)344     public abstract void onLoadChildren(@NonNull String parentId,
345             @NonNull Result<List<MediaBrowser.MediaItem>> result);
346 
347     /**
348      * Called to get information about a specific media item.
349      * <p>
350      * Implementations must call {@link Result#sendResult result.sendResult}. If
351      * loading the item will be an expensive operation {@link Result#detach
352      * result.detach} may be called before returning from this function, and
353      * then {@link Result#sendResult result.sendResult} called when the item has
354      * been loaded.
355      * <p>
356      * The default implementation sends a null result.
357      *
358      * @param itemId The id for the specific
359      *            {@link android.media.browse.MediaBrowser.MediaItem}.
360      * @param result The Result to send the item to, or null if the id is
361      *            invalid.
362      */
onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)363     public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
364         result.sendResult(null);
365     }
366 
367     /**
368      * Call to set the media session.
369      * <p>
370      * This should be called as soon as possible during the service's startup.
371      * It may only be called once.
372      *
373      * @param token The token for the service's {@link MediaSession}.
374      */
setSessionToken(final MediaSession.Token token)375     public void setSessionToken(final MediaSession.Token token) {
376         if (token == null) {
377             throw new IllegalArgumentException("Session token may not be null.");
378         }
379         if (mSession != null) {
380             throw new IllegalStateException("The session token has already been set.");
381         }
382         mSession = token;
383         mHandler.post(new Runnable() {
384             @Override
385             public void run() {
386                 for (IBinder key : mConnections.keySet()) {
387                     ConnectionRecord connection = mConnections.get(key);
388                     try {
389                         connection.callbacks.onConnect(connection.root.getRootId(), token,
390                                 connection.root.getExtras());
391                     } catch (RemoteException e) {
392                         Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
393                         mConnections.remove(key);
394                     }
395                 }
396             }
397         });
398     }
399 
400     /**
401      * Gets the session token, or null if it has not yet been created
402      * or if it has been destroyed.
403      */
getSessionToken()404     public @Nullable MediaSession.Token getSessionToken() {
405         return mSession;
406     }
407 
408     /**
409      * Notifies all connected media browsers that the children of
410      * the specified parent id have changed in some way.
411      * This will cause browsers to fetch subscribed content again.
412      *
413      * @param parentId The id of the parent media item whose
414      * children changed.
415      */
notifyChildrenChanged(@onNull final String parentId)416     public void notifyChildrenChanged(@NonNull final String parentId) {
417         if (parentId == null) {
418             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
419         }
420         mHandler.post(new Runnable() {
421             @Override
422             public void run() {
423                 for (IBinder binder : mConnections.keySet()) {
424                     ConnectionRecord connection = mConnections.get(binder);
425                     if (connection.subscriptions.contains(parentId)) {
426                         performLoadChildren(parentId, connection);
427                     }
428                 }
429             }
430         });
431     }
432 
433     /**
434      * Return whether the given package is one of the ones that is owned by the uid.
435      */
isValidPackage(String pkg, int uid)436     private boolean isValidPackage(String pkg, int uid) {
437         if (pkg == null) {
438             return false;
439         }
440         final PackageManager pm = getPackageManager();
441         final String[] packages = pm.getPackagesForUid(uid);
442         final int N = packages.length;
443         for (int i=0; i<N; i++) {
444             if (packages[i].equals(pkg)) {
445                 return true;
446             }
447         }
448         return false;
449     }
450 
451     /**
452      * Save the subscription and if it is a new subscription send the results.
453      */
addSubscription(String id, ConnectionRecord connection)454     private void addSubscription(String id, ConnectionRecord connection) {
455         // Save the subscription
456         connection.subscriptions.add(id);
457 
458         // send the results
459         performLoadChildren(id, connection);
460     }
461 
462     /**
463      * Call onLoadChildren and then send the results back to the connection.
464      * <p>
465      * Callers must make sure that this connection is still connected.
466      */
performLoadChildren(final String parentId, final ConnectionRecord connection)467     private void performLoadChildren(final String parentId, final ConnectionRecord connection) {
468         final Result<List<MediaBrowser.MediaItem>> result
469                 = new Result<List<MediaBrowser.MediaItem>>(parentId) {
470             @Override
471             void onResultSent(List<MediaBrowser.MediaItem> list) {
472                 if (list == null) {
473                     throw new IllegalStateException("onLoadChildren sent null list for id "
474                             + parentId);
475                 }
476                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
477                     if (DBG) {
478                         Log.d(TAG, "Not sending onLoadChildren result for connection that has"
479                                 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
480                     }
481                     return;
482                 }
483 
484                 final ParceledListSlice<MediaBrowser.MediaItem> pls = new ParceledListSlice(list);
485                 try {
486                     connection.callbacks.onLoadChildren(parentId, pls);
487                 } catch (RemoteException ex) {
488                     // The other side is in the process of crashing.
489                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
490                             + " package=" + connection.pkg);
491                 }
492             }
493         };
494 
495         onLoadChildren(parentId, result);
496 
497         if (!result.isDone()) {
498             throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
499                     + " before returning for package=" + connection.pkg + " id=" + parentId);
500         }
501     }
502 
performLoadItem(String itemId, final ResultReceiver receiver)503     private void performLoadItem(String itemId, final ResultReceiver receiver) {
504         final Result<MediaBrowser.MediaItem> result =
505                 new Result<MediaBrowser.MediaItem>(itemId) {
506             @Override
507             void onResultSent(MediaBrowser.MediaItem item) {
508                 Bundle bundle = new Bundle();
509                 bundle.putParcelable(KEY_MEDIA_ITEM, item);
510                 receiver.send(0, bundle);
511             }
512         };
513 
514         MediaBrowserService.this.onLoadItem(itemId, result);
515 
516         if (!result.isDone()) {
517             throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
518                     + " before returning for id=" + itemId);
519         }
520     }
521 
522     /**
523      * Contains information that the browser service needs to send to the client
524      * when first connected.
525      */
526     public static final class BrowserRoot {
527         final private String mRootId;
528         final private Bundle mExtras;
529 
530         /**
531          * Constructs a browser root.
532          * @param rootId The root id for browsing.
533          * @param extras Any extras about the browser service.
534          */
BrowserRoot(@onNull String rootId, @Nullable Bundle extras)535         public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
536             if (rootId == null) {
537                 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
538                         "Use null for BrowserRoot instead.");
539             }
540             mRootId = rootId;
541             mExtras = extras;
542         }
543 
544         /**
545          * Gets the root id for browsing.
546          */
getRootId()547         public String getRootId() {
548             return mRootId;
549         }
550 
551         /**
552          * Gets any extras about the brwoser service.
553          */
getExtras()554         public Bundle getExtras() {
555             return mExtras;
556         }
557     }
558 }
559