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.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SdkConstant;
23 import android.annotation.SdkConstant.SdkConstantType;
24 import android.app.Service;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ParceledListSlice;
28 import android.media.browse.MediaBrowser;
29 import android.media.browse.MediaBrowserUtils;
30 import android.media.session.MediaSession;
31 import android.os.Binder;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.media.session.MediaSessionManager;
35 import android.media.session.MediaSessionManager.RemoteUserInfo;
36 import android.os.IBinder;
37 import android.os.RemoteException;
38 import android.os.ResultReceiver;
39 import android.service.media.IMediaBrowserService;
40 import android.service.media.IMediaBrowserServiceCallbacks;
41 import android.text.TextUtils;
42 import android.util.ArrayMap;
43 import android.util.Log;
44 import android.util.Pair;
45 
46 import java.io.FileDescriptor;
47 import java.io.PrintWriter;
48 import java.lang.annotation.Retention;
49 import java.lang.annotation.RetentionPolicy;
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.Iterator;
54 import java.util.List;
55 
56 /**
57  * Base class for media browser services.
58  * <p>
59  * Media browser services enable applications to browse media content provided by an application
60  * and ask the application to start playing it. They may also be used to control content that
61  * is already playing by way of a {@link MediaSession}.
62  * </p>
63  *
64  * To extend this class, you must declare the service in your manifest file with
65  * an intent filter with the {@link #SERVICE_INTERFACE} action.
66  *
67  * For example:
68  * </p><pre>
69  * &lt;service android:name=".MyMediaBrowserService"
70  *          android:label="&#64;string/service_name" >
71  *     &lt;intent-filter>
72  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
73  *     &lt;/intent-filter>
74  * &lt;/service>
75  * </pre>
76  *
77  */
78 public abstract class MediaBrowserService extends Service {
79     private static final String TAG = "MediaBrowserService";
80     private static final boolean DBG = false;
81 
82     /**
83      * The {@link Intent} that must be declared as handled by the service.
84      */
85     @SdkConstant(SdkConstantType.SERVICE_ACTION)
86     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
87 
88     /**
89      * A key for passing the MediaItem to the ResultReceiver in getItem.
90      * @hide
91      */
92     public static final String KEY_MEDIA_ITEM = "media_item";
93 
94     private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
95     private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
96 
97     private static final int RESULT_ERROR = -1;
98     private static final int RESULT_OK = 0;
99 
100     /** @hide */
101     @Retention(RetentionPolicy.SOURCE)
102     @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
103             RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
104     private @interface ResultFlags { }
105 
106     private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
107     private ConnectionRecord mCurConnection;
108     private final Handler mHandler = new Handler();
109     private ServiceBinder mBinder;
110     MediaSession.Token mSession;
111 
112     /**
113      * All the info about a connection.
114      */
115     private class ConnectionRecord implements IBinder.DeathRecipient {
116         String pkg;
117         int uid;
118         int pid;
119         Bundle rootHints;
120         IMediaBrowserServiceCallbacks callbacks;
121         BrowserRoot root;
122         HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
123 
124         @Override
binderDied()125         public void binderDied() {
126             mHandler.post(new Runnable() {
127                 @Override
128                 public void run() {
129                     mConnections.remove(callbacks.asBinder());
130                 }
131             });
132         }
133     }
134 
135     /**
136      * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
137      * <p>
138      * Each of the methods that takes one of these to send the result must call
139      * {@link #sendResult} to respond to the caller with the given results. If those
140      * functions return without calling {@link #sendResult}, they must instead call
141      * {@link #detach} before returning, and then may call {@link #sendResult} when
142      * they are done. If more than one of those methods is called, an exception will
143      * be thrown.
144      *
145      * @see #onLoadChildren
146      * @see #onLoadItem
147      */
148     public class Result<T> {
149         private Object mDebug;
150         private boolean mDetachCalled;
151         private boolean mSendResultCalled;
152         private int mFlags;
153 
Result(Object debug)154         Result(Object debug) {
155             mDebug = debug;
156         }
157 
158         /**
159          * Send the result back to the caller.
160          */
sendResult(T result)161         public void sendResult(T result) {
162             if (mSendResultCalled) {
163                 throw new IllegalStateException("sendResult() called twice for: " + mDebug);
164             }
165             mSendResultCalled = true;
166             onResultSent(result, mFlags);
167         }
168 
169         /**
170          * Detach this message from the current thread and allow the {@link #sendResult}
171          * call to happen later.
172          */
detach()173         public void detach() {
174             if (mDetachCalled) {
175                 throw new IllegalStateException("detach() called when detach() had already"
176                         + " been called for: " + mDebug);
177             }
178             if (mSendResultCalled) {
179                 throw new IllegalStateException("detach() called when sendResult() had already"
180                         + " been called for: " + mDebug);
181             }
182             mDetachCalled = true;
183         }
184 
isDone()185         boolean isDone() {
186             return mDetachCalled || mSendResultCalled;
187         }
188 
setFlags(@esultFlags int flags)189         void setFlags(@ResultFlags int flags) {
190             mFlags = flags;
191         }
192 
193         /**
194          * Called when the result is sent, after assertions about not being called twice
195          * have happened.
196          */
onResultSent(T result, @ResultFlags int flags)197         void onResultSent(T result, @ResultFlags int flags) {
198         }
199     }
200 
201     private class ServiceBinder extends IMediaBrowserService.Stub {
202         @Override
connect(final String pkg, final Bundle rootHints, final IMediaBrowserServiceCallbacks callbacks)203         public void connect(final String pkg, final Bundle rootHints,
204                 final IMediaBrowserServiceCallbacks callbacks) {
205 
206             final int pid = Binder.getCallingPid();
207             final int uid = Binder.getCallingUid();
208             if (!isValidPackage(pkg, uid)) {
209                 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
210                         + " package=" + pkg);
211             }
212 
213             mHandler.post(new Runnable() {
214                     @Override
215                     public void run() {
216                         final IBinder b = callbacks.asBinder();
217 
218                         // Clear out the old subscriptions. We are getting new ones.
219                         mConnections.remove(b);
220 
221                         final ConnectionRecord connection = new ConnectionRecord();
222                         connection.pkg = pkg;
223                         connection.pid = pid;
224                         connection.uid = uid;
225                         connection.rootHints = rootHints;
226                         connection.callbacks = callbacks;
227 
228                         mCurConnection = connection;
229                         connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
230                         mCurConnection = null;
231 
232                         // If they didn't return something, don't allow this client.
233                         if (connection.root == null) {
234                             Log.i(TAG, "No root for client " + pkg + " from service "
235                                     + getClass().getName());
236                             try {
237                                 callbacks.onConnectFailed();
238                             } catch (RemoteException ex) {
239                                 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
240                                         + "pkg=" + pkg);
241                             }
242                         } else {
243                             try {
244                                 mConnections.put(b, connection);
245                                 b.linkToDeath(connection, 0);
246                                 if (mSession != null) {
247                                     callbacks.onConnect(connection.root.getRootId(),
248                                             mSession, connection.root.getExtras());
249                                 }
250                             } catch (RemoteException ex) {
251                                 Log.w(TAG, "Calling onConnect() failed. Dropping client. "
252                                         + "pkg=" + pkg);
253                                 mConnections.remove(b);
254                             }
255                         }
256                     }
257                 });
258         }
259 
260         @Override
disconnect(final IMediaBrowserServiceCallbacks callbacks)261         public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
262             mHandler.post(new Runnable() {
263                     @Override
264                     public void run() {
265                         final IBinder b = callbacks.asBinder();
266 
267                         // Clear out the old subscriptions. We are getting new ones.
268                         final ConnectionRecord old = mConnections.remove(b);
269                         if (old != null) {
270                             // TODO
271                             old.callbacks.asBinder().unlinkToDeath(old, 0);
272                         }
273                     }
274                 });
275         }
276 
277         @Override
addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks)278         public void addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) {
279             // do-nothing
280         }
281 
282         @Override
addSubscription(final String id, final IBinder token, final Bundle options, final IMediaBrowserServiceCallbacks callbacks)283         public void addSubscription(final String id, final IBinder token, final Bundle options,
284                 final IMediaBrowserServiceCallbacks callbacks) {
285             mHandler.post(new Runnable() {
286                     @Override
287                     public void run() {
288                         final IBinder b = callbacks.asBinder();
289 
290                         // Get the record for the connection
291                         final ConnectionRecord connection = mConnections.get(b);
292                         if (connection == null) {
293                             Log.w(TAG, "addSubscription for callback that isn't registered id="
294                                 + id);
295                             return;
296                         }
297 
298                         MediaBrowserService.this.addSubscription(id, connection, token, options);
299                     }
300                 });
301         }
302 
303         @Override
removeSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks)304         public void removeSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) {
305             // do-nothing
306         }
307 
308         @Override
removeSubscription(final String id, final IBinder token, final IMediaBrowserServiceCallbacks callbacks)309         public void removeSubscription(final String id, final IBinder token,
310                 final IMediaBrowserServiceCallbacks callbacks) {
311             mHandler.post(new Runnable() {
312                 @Override
313                 public void run() {
314                     final IBinder b = callbacks.asBinder();
315 
316                     ConnectionRecord connection = mConnections.get(b);
317                     if (connection == null) {
318                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
319                                 + id);
320                         return;
321                     }
322                     if (!MediaBrowserService.this.removeSubscription(id, connection, token)) {
323                         Log.w(TAG, "removeSubscription called for " + id
324                                 + " which is not subscribed");
325                     }
326                 }
327             });
328         }
329 
330         @Override
getMediaItem(final String mediaId, final ResultReceiver receiver, final IMediaBrowserServiceCallbacks callbacks)331         public void getMediaItem(final String mediaId, final ResultReceiver receiver,
332                 final IMediaBrowserServiceCallbacks callbacks) {
333             mHandler.post(new Runnable() {
334                 @Override
335                 public void run() {
336                     final IBinder b = callbacks.asBinder();
337                     ConnectionRecord connection = mConnections.get(b);
338                     if (connection == null) {
339                         Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
340                         return;
341                     }
342                     performLoadItem(mediaId, connection, receiver);
343                 }
344             });
345         }
346     }
347 
348     @Override
onCreate()349     public void onCreate() {
350         super.onCreate();
351         mBinder = new ServiceBinder();
352     }
353 
354     @Override
onBind(Intent intent)355     public IBinder onBind(Intent intent) {
356         if (SERVICE_INTERFACE.equals(intent.getAction())) {
357             return mBinder;
358         }
359         return null;
360     }
361 
362     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)363     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
364     }
365 
366     /**
367      * Called to get the root information for browsing by a particular client.
368      * <p>
369      * The implementation should verify that the client package has permission
370      * to access browse media information before returning the root id; it
371      * should return null if the client is not allowed to access this
372      * information.
373      * </p>
374      *
375      * @param clientPackageName The package name of the application which is
376      *            requesting access to browse media.
377      * @param clientUid The uid of the application which is requesting access to
378      *            browse media.
379      * @param rootHints An optional bundle of service-specific arguments to send
380      *            to the media browser service when connecting and retrieving the
381      *            root id for browsing, or null if none. The contents of this
382      *            bundle may affect the information returned when browsing.
383      * @return The {@link BrowserRoot} for accessing this app's content or null.
384      * @see BrowserRoot#EXTRA_RECENT
385      * @see BrowserRoot#EXTRA_OFFLINE
386      * @see BrowserRoot#EXTRA_SUGGESTED
387      */
onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)388     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
389             int clientUid, @Nullable Bundle rootHints);
390 
391     /**
392      * Called to get information about the children of a media item.
393      * <p>
394      * Implementations must call {@link Result#sendResult result.sendResult}
395      * with the list of children. If loading the children will be an expensive
396      * operation that should be performed on another thread,
397      * {@link Result#detach result.detach} may be called before returning from
398      * this function, and then {@link Result#sendResult result.sendResult}
399      * called when the loading is complete.
400      * </p><p>
401      * In case the media item does not have any children, call {@link Result#sendResult}
402      * with an empty list. When the given {@code parentId} is invalid, implementations must
403      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
404      * {@link MediaBrowser.SubscriptionCallback#onError}.
405      * </p>
406      *
407      * @param parentId The id of the parent media item whose children are to be
408      *            queried.
409      * @param result The Result to send the list of children to.
410      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result)411     public abstract void onLoadChildren(@NonNull String parentId,
412             @NonNull Result<List<MediaBrowser.MediaItem>> result);
413 
414     /**
415      * Called to get information about the children of a media item.
416      * <p>
417      * Implementations must call {@link Result#sendResult result.sendResult}
418      * with the list of children. If loading the children will be an expensive
419      * operation that should be performed on another thread,
420      * {@link Result#detach result.detach} may be called before returning from
421      * this function, and then {@link Result#sendResult result.sendResult}
422      * called when the loading is complete.
423      * </p><p>
424      * In case the media item does not have any children, call {@link Result#sendResult}
425      * with an empty list. When the given {@code parentId} is invalid, implementations must
426      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
427      * {@link MediaBrowser.SubscriptionCallback#onError}.
428      * </p>
429      *
430      * @param parentId The id of the parent media item whose children are to be
431      *            queried.
432      * @param result The Result to send the list of children to.
433      * @param options The bundle of service-specific arguments sent from the media
434      *            browser. The information returned through the result should be
435      *            affected by the contents of this bundle.
436      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options)437     public void onLoadChildren(@NonNull String parentId,
438             @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) {
439         // To support backward compatibility, when the implementation of MediaBrowserService doesn't
440         // override onLoadChildren() with options, onLoadChildren() without options will be used
441         // instead, and the options will be applied in the implementation of result.onResultSent().
442         result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
443         onLoadChildren(parentId, result);
444     }
445 
446     /**
447      * Called to get information about a specific media item.
448      * <p>
449      * Implementations must call {@link Result#sendResult result.sendResult}. If
450      * loading the item will be an expensive operation {@link Result#detach
451      * result.detach} may be called before returning from this function, and
452      * then {@link Result#sendResult result.sendResult} called when the item has
453      * been loaded.
454      * </p><p>
455      * When the given {@code itemId} is invalid, implementations must call
456      * {@link Result#sendResult result.sendResult} with {@code null}.
457      * </p><p>
458      * The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}.
459      * </p>
460      *
461      * @param itemId The id for the specific
462      *            {@link android.media.browse.MediaBrowser.MediaItem}.
463      * @param result The Result to send the item to.
464      */
onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)465     public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
466         result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
467         result.sendResult(null);
468     }
469 
470     /**
471      * Call to set the media session.
472      * <p>
473      * This should be called as soon as possible during the service's startup.
474      * It may only be called once.
475      *
476      * @param token The token for the service's {@link MediaSession}.
477      */
setSessionToken(final MediaSession.Token token)478     public void setSessionToken(final MediaSession.Token token) {
479         if (token == null) {
480             throw new IllegalArgumentException("Session token may not be null.");
481         }
482         if (mSession != null) {
483             throw new IllegalStateException("The session token has already been set.");
484         }
485         mSession = token;
486         mHandler.post(new Runnable() {
487             @Override
488             public void run() {
489                 Iterator<ConnectionRecord> iter = mConnections.values().iterator();
490                 while (iter.hasNext()){
491                     ConnectionRecord connection = iter.next();
492                     try {
493                         connection.callbacks.onConnect(connection.root.getRootId(), token,
494                                 connection.root.getExtras());
495                     } catch (RemoteException e) {
496                         Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
497                         iter.remove();
498                     }
499                 }
500             }
501         });
502     }
503 
504     /**
505      * Gets the session token, or null if it has not yet been created
506      * or if it has been destroyed.
507      */
getSessionToken()508     public @Nullable MediaSession.Token getSessionToken() {
509         return mSession;
510     }
511 
512     /**
513      * Gets the root hints sent from the currently connected {@link MediaBrowser}.
514      * The root hints are service-specific arguments included in an optional bundle sent to the
515      * media browser service when connecting and retrieving the root id for browsing, or null if
516      * none. The contents of this bundle may affect the information returned when browsing.
517      *
518      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
519      *             {@link #onLoadChildren} or {@link #onLoadItem}.
520      * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT
521      * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
522      * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
523      */
getBrowserRootHints()524     public final Bundle getBrowserRootHints() {
525         if (mCurConnection == null) {
526             throw new IllegalStateException("This should be called inside of onGetRoot or"
527                     + " onLoadChildren or onLoadItem methods");
528         }
529         return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
530     }
531 
532     /**
533      * Gets the browser information who sent the current request.
534      *
535      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
536      *             {@link #onLoadChildren} or {@link #onLoadItem}.
537      * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
538      */
getCurrentBrowserInfo()539     public final RemoteUserInfo getCurrentBrowserInfo() {
540         if (mCurConnection == null) {
541             throw new IllegalStateException("This should be called inside of onGetRoot or"
542                     + " onLoadChildren or onLoadItem methods");
543         }
544         return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid,
545                 mCurConnection.callbacks.asBinder());
546     }
547 
548     /**
549      * Notifies all connected media browsers that the children of
550      * the specified parent id have changed in some way.
551      * This will cause browsers to fetch subscribed content again.
552      *
553      * @param parentId The id of the parent media item whose
554      * children changed.
555      */
notifyChildrenChanged(@onNull String parentId)556     public void notifyChildrenChanged(@NonNull String parentId) {
557         notifyChildrenChangedInternal(parentId, null);
558     }
559 
560     /**
561      * Notifies all connected media browsers that the children of
562      * the specified parent id have changed in some way.
563      * This will cause browsers to fetch subscribed content again.
564      *
565      * @param parentId The id of the parent media item whose
566      *            children changed.
567      * @param options The bundle of service-specific arguments to send
568      *            to the media browser. The contents of this bundle may
569      *            contain the information about the change.
570      */
notifyChildrenChanged(@onNull String parentId, @NonNull Bundle options)571     public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
572         if (options == null) {
573             throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
574         }
575         notifyChildrenChangedInternal(parentId, options);
576     }
577 
notifyChildrenChangedInternal(final String parentId, final Bundle options)578     private void notifyChildrenChangedInternal(final String parentId, final Bundle options) {
579         if (parentId == null) {
580             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
581         }
582         mHandler.post(new Runnable() {
583             @Override
584             public void run() {
585                 for (IBinder binder : mConnections.keySet()) {
586                     ConnectionRecord connection = mConnections.get(binder);
587                     List<Pair<IBinder, Bundle>> callbackList =
588                             connection.subscriptions.get(parentId);
589                     if (callbackList != null) {
590                         for (Pair<IBinder, Bundle> callback : callbackList) {
591                             if (MediaBrowserUtils.hasDuplicatedItems(options, callback.second)) {
592                                 performLoadChildren(parentId, connection, callback.second);
593                             }
594                         }
595                     }
596                 }
597             }
598         });
599     }
600 
601     /**
602      * Return whether the given package is one of the ones that is owned by the uid.
603      */
isValidPackage(String pkg, int uid)604     private boolean isValidPackage(String pkg, int uid) {
605         if (pkg == null) {
606             return false;
607         }
608         final PackageManager pm = getPackageManager();
609         final String[] packages = pm.getPackagesForUid(uid);
610         final int N = packages.length;
611         for (int i=0; i<N; i++) {
612             if (packages[i].equals(pkg)) {
613                 return true;
614             }
615         }
616         return false;
617     }
618 
619     /**
620      * Save the subscription and if it is a new subscription send the results.
621      */
addSubscription(String id, ConnectionRecord connection, IBinder token, Bundle options)622     private void addSubscription(String id, ConnectionRecord connection, IBinder token,
623             Bundle options) {
624         // Save the subscription
625         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
626         if (callbackList == null) {
627             callbackList = new ArrayList<>();
628         }
629         for (Pair<IBinder, Bundle> callback : callbackList) {
630             if (token == callback.first
631                     && MediaBrowserUtils.areSameOptions(options, callback.second)) {
632                 return;
633             }
634         }
635         callbackList.add(new Pair<>(token, options));
636         connection.subscriptions.put(id, callbackList);
637         // send the results
638         performLoadChildren(id, connection, options);
639     }
640 
641     /**
642      * Remove the subscription.
643      */
removeSubscription(String id, ConnectionRecord connection, IBinder token)644     private boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
645         if (token == null) {
646             return connection.subscriptions.remove(id) != null;
647         }
648         boolean removed = false;
649         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
650         if (callbackList != null) {
651             Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
652             while (iter.hasNext()){
653                 if (token == iter.next().first) {
654                     removed = true;
655                     iter.remove();
656                 }
657             }
658             if (callbackList.size() == 0) {
659                 connection.subscriptions.remove(id);
660             }
661         }
662         return removed;
663     }
664 
665     /**
666      * Call onLoadChildren and then send the results back to the connection.
667      * <p>
668      * Callers must make sure that this connection is still connected.
669      */
performLoadChildren(final String parentId, final ConnectionRecord connection, final Bundle options)670     private void performLoadChildren(final String parentId, final ConnectionRecord connection,
671             final Bundle options) {
672         final Result<List<MediaBrowser.MediaItem>> result
673                 = new Result<List<MediaBrowser.MediaItem>>(parentId) {
674             @Override
675             void onResultSent(List<MediaBrowser.MediaItem> list, @ResultFlags int flag) {
676                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
677                     if (DBG) {
678                         Log.d(TAG, "Not sending onLoadChildren result for connection that has"
679                                 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
680                     }
681                     return;
682                 }
683 
684                 List<MediaBrowser.MediaItem> filteredList =
685                         (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
686                         ? applyOptions(list, options) : list;
687                 final ParceledListSlice<MediaBrowser.MediaItem> pls =
688                         filteredList == null ? null : new ParceledListSlice<>(filteredList);
689                 try {
690                     connection.callbacks.onLoadChildrenWithOptions(parentId, pls, options);
691                 } catch (RemoteException ex) {
692                     // The other side is in the process of crashing.
693                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
694                             + " package=" + connection.pkg);
695                 }
696             }
697         };
698 
699         mCurConnection = connection;
700         if (options == null) {
701             onLoadChildren(parentId, result);
702         } else {
703             onLoadChildren(parentId, result, options);
704         }
705         mCurConnection = null;
706 
707         if (!result.isDone()) {
708             throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
709                     + " before returning for package=" + connection.pkg + " id=" + parentId);
710         }
711     }
712 
applyOptions(List<MediaBrowser.MediaItem> list, final Bundle options)713     private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list,
714             final Bundle options) {
715         if (list == null) {
716             return null;
717         }
718         int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
719         int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
720         if (page == -1 && pageSize == -1) {
721             return list;
722         }
723         int fromIndex = pageSize * page;
724         int toIndex = fromIndex + pageSize;
725         if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
726             return Collections.EMPTY_LIST;
727         }
728         if (toIndex > list.size()) {
729             toIndex = list.size();
730         }
731         return list.subList(fromIndex, toIndex);
732     }
733 
performLoadItem(String itemId, final ConnectionRecord connection, final ResultReceiver receiver)734     private void performLoadItem(String itemId, final ConnectionRecord connection,
735             final ResultReceiver receiver) {
736         final Result<MediaBrowser.MediaItem> result =
737                 new Result<MediaBrowser.MediaItem>(itemId) {
738             @Override
739             void onResultSent(MediaBrowser.MediaItem item, @ResultFlags int flag) {
740                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
741                     if (DBG) {
742                         Log.d(TAG, "Not sending onLoadItem result for connection that has"
743                                 + " been disconnected. pkg=" + connection.pkg + " id=" + itemId);
744                     }
745                     return;
746                 }
747                 if ((flag & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
748                     receiver.send(RESULT_ERROR, null);
749                     return;
750                 }
751                 Bundle bundle = new Bundle();
752                 bundle.putParcelable(KEY_MEDIA_ITEM, item);
753                 receiver.send(RESULT_OK, bundle);
754             }
755         };
756 
757         mCurConnection = connection;
758         onLoadItem(itemId, result);
759         mCurConnection = null;
760 
761         if (!result.isDone()) {
762             throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
763                     + " before returning for id=" + itemId);
764         }
765     }
766 
767     /**
768      * Contains information that the browser service needs to send to the client
769      * when first connected.
770      */
771     public static final class BrowserRoot {
772         /**
773          * The lookup key for a boolean that indicates whether the browser service should return a
774          * browser root for recently played media items.
775          *
776          * <p>When creating a media browser for a given media browser service, this key can be
777          * supplied as a root hint for retrieving media items that are recently played.
778          * If the media browser service can provide such media items, the implementation must return
779          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
780          *
781          * <p>The root hint may contain multiple keys.
782          *
783          * @see #EXTRA_OFFLINE
784          * @see #EXTRA_SUGGESTED
785          */
786         public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
787 
788         /**
789          * The lookup key for a boolean that indicates whether the browser service should return a
790          * browser root for offline media items.
791          *
792          * <p>When creating a media browser for a given media browser service, this key can be
793          * supplied as a root hint for retrieving media items that are can be played without an
794          * internet connection.
795          * If the media browser service can provide such media items, the implementation must return
796          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
797          *
798          * <p>The root hint may contain multiple keys.
799          *
800          * @see #EXTRA_RECENT
801          * @see #EXTRA_SUGGESTED
802          */
803         public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
804 
805         /**
806          * The lookup key for a boolean that indicates whether the browser service should return a
807          * browser root for suggested media items.
808          *
809          * <p>When creating a media browser for a given media browser service, this key can be
810          * supplied as a root hint for retrieving the media items suggested by the media browser
811          * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
812          * is considered ordered by relevance, first being the top suggestion.
813          * If the media browser service can provide such media items, the implementation must return
814          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
815          *
816          * <p>The root hint may contain multiple keys.
817          *
818          * @see #EXTRA_RECENT
819          * @see #EXTRA_OFFLINE
820          */
821         public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
822 
823         final private String mRootId;
824         final private Bundle mExtras;
825 
826         /**
827          * Constructs a browser root.
828          * @param rootId The root id for browsing.
829          * @param extras Any extras about the browser service.
830          */
BrowserRoot(@onNull String rootId, @Nullable Bundle extras)831         public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
832             if (rootId == null) {
833                 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
834                         "Use null for BrowserRoot instead.");
835             }
836             mRootId = rootId;
837             mExtras = extras;
838         }
839 
840         /**
841          * Gets the root id for browsing.
842          */
getRootId()843         public String getRootId() {
844             return mRootId;
845         }
846 
847         /**
848          * Gets any extras about the browser service.
849          */
getExtras()850         public Bundle getExtras() {
851             return mExtras;
852         }
853     }
854 }
855