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.compat.annotation.UnsupportedAppUsage;
26 import android.content.Intent;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ParceledListSlice;
29 import android.media.browse.MediaBrowser;
30 import android.media.browse.MediaBrowserUtils;
31 import android.media.session.MediaSession;
32 import android.media.session.MediaSessionManager;
33 import android.media.session.MediaSessionManager.RemoteUserInfo;
34 import android.os.Binder;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.IBinder;
38 import android.os.RemoteException;
39 import android.os.ResultReceiver;
40 import android.util.ArrayMap;
41 import android.util.Log;
42 import android.util.Pair;
43 
44 import java.io.FileDescriptor;
45 import java.io.PrintWriter;
46 import java.lang.annotation.Retention;
47 import java.lang.annotation.RetentionPolicy;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.Iterator;
52 import java.util.List;
53 
54 /**
55  * Base class for media browser services.
56  * <p>
57  * Media browser services enable applications to browse media content provided by an application
58  * and ask the application to start playing it. They may also be used to control content that
59  * is already playing by way of a {@link MediaSession}.
60  * </p>
61  *
62  * To extend this class, you must declare the service in your manifest file with
63  * an intent filter with the {@link #SERVICE_INTERFACE} action.
64  *
65  * For example:
66  * </p><pre>
67  * &lt;service android:name=".MyMediaBrowserService"
68  *          android:label="&#64;string/service_name" >
69  *     &lt;intent-filter>
70  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
71  *     &lt;/intent-filter>
72  * &lt;/service>
73  * </pre>
74  *
75  */
76 public abstract class MediaBrowserService extends Service {
77     private static final String TAG = "MediaBrowserService";
78     private static final boolean DBG = false;
79 
80     /**
81      * The {@link Intent} that must be declared as handled by the service.
82      */
83     @SdkConstant(SdkConstantType.SERVICE_ACTION)
84     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
85 
86     /**
87      * A key for passing the MediaItem to the ResultReceiver in getItem.
88      * @hide
89      */
90     @UnsupportedAppUsage
91     public static final String KEY_MEDIA_ITEM = "media_item";
92 
93     private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
94     private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
95 
96     private static final int RESULT_ERROR = -1;
97     private static final int RESULT_OK = 0;
98 
99     /** @hide */
100     @Retention(RetentionPolicy.SOURCE)
101     @IntDef(flag = true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
102             RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
103     private @interface ResultFlags { }
104 
105     private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
106     private ConnectionRecord mCurConnection;
107     private final Handler mHandler = new Handler();
108     private ServiceBinder mBinder;
109     MediaSession.Token mSession;
110 
111     /**
112      * All the info about a connection.
113      */
114     private class ConnectionRecord implements IBinder.DeathRecipient {
115         String pkg;
116         int uid;
117         int pid;
118         Bundle rootHints;
119         IMediaBrowserServiceCallbacks callbacks;
120         BrowserRoot root;
121         HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
122 
123         @Override
binderDied()124         public void binderDied() {
125             mHandler.post(new Runnable() {
126                 @Override
127                 public void run() {
128                     mConnections.remove(callbacks.asBinder());
129                 }
130             });
131         }
132     }
133 
134     /**
135      * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
136      * <p>
137      * Each of the methods that takes one of these to send the result must call
138      * {@link #sendResult} to respond to the caller with the given results. If those
139      * functions return without calling {@link #sendResult}, they must instead call
140      * {@link #detach} before returning, and then may call {@link #sendResult} when
141      * they are done. If more than one of those methods is called, an exception will
142      * be thrown.
143      *
144      * @see #onLoadChildren
145      * @see #onLoadItem
146      */
147     public class Result<T> {
148         private Object mDebug;
149         private boolean mDetachCalled;
150         private boolean mSendResultCalled;
151         @UnsupportedAppUsage
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(
305                 String id, IMediaBrowserServiceCallbacks callbacks) {
306             // do-nothing
307         }
308 
309         @Override
removeSubscription(final String id, final IBinder token, final IMediaBrowserServiceCallbacks callbacks)310         public void removeSubscription(final String id, final IBinder token,
311                 final IMediaBrowserServiceCallbacks callbacks) {
312             mHandler.post(new Runnable() {
313                 @Override
314                 public void run() {
315                     final IBinder b = callbacks.asBinder();
316 
317                     ConnectionRecord connection = mConnections.get(b);
318                     if (connection == null) {
319                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
320                                 + id);
321                         return;
322                     }
323                     if (!MediaBrowserService.this.removeSubscription(id, connection, token)) {
324                         Log.w(TAG, "removeSubscription called for " + id
325                                 + " which is not subscribed");
326                     }
327                 }
328             });
329         }
330 
331         @Override
getMediaItem(final String mediaId, final ResultReceiver receiver, final IMediaBrowserServiceCallbacks callbacks)332         public void getMediaItem(final String mediaId, final ResultReceiver receiver,
333                 final IMediaBrowserServiceCallbacks callbacks) {
334             mHandler.post(new Runnable() {
335                 @Override
336                 public void run() {
337                     final IBinder b = callbacks.asBinder();
338                     ConnectionRecord connection = mConnections.get(b);
339                     if (connection == null) {
340                         Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
341                         return;
342                     }
343                     performLoadItem(mediaId, connection, receiver);
344                 }
345             });
346         }
347     }
348 
349     @Override
onCreate()350     public void onCreate() {
351         super.onCreate();
352         mBinder = new ServiceBinder();
353     }
354 
355     @Override
onBind(Intent intent)356     public IBinder onBind(Intent intent) {
357         if (SERVICE_INTERFACE.equals(intent.getAction())) {
358             return mBinder;
359         }
360         return null;
361     }
362 
363     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)364     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
365     }
366 
367     /**
368      * Called to get the root information for browsing by a particular client.
369      * <p>
370      * The implementation should verify that the client package has permission
371      * to access browse media information before returning the root id; it
372      * should return null if the client is not allowed to access this
373      * information.
374      * </p>
375      *
376      * @param clientPackageName The package name of the application which is
377      *            requesting access to browse media.
378      * @param clientUid The uid of the application which is requesting access to
379      *            browse media.
380      * @param rootHints An optional bundle of service-specific arguments to send
381      *            to the media browser service when connecting and retrieving the
382      *            root id for browsing, or null if none. The contents of this
383      *            bundle may affect the information returned when browsing.
384      * @return The {@link BrowserRoot} for accessing this app's content or null.
385      * @see BrowserRoot#EXTRA_RECENT
386      * @see BrowserRoot#EXTRA_OFFLINE
387      * @see BrowserRoot#EXTRA_SUGGESTED
388      */
onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)389     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
390             int clientUid, @Nullable Bundle rootHints);
391 
392     /**
393      * Called to get information about the children of a media item.
394      * <p>
395      * Implementations must call {@link Result#sendResult result.sendResult}
396      * with the list of children. If loading the children will be an expensive
397      * operation that should be performed on another thread,
398      * {@link Result#detach result.detach} may be called before returning from
399      * this function, and then {@link Result#sendResult result.sendResult}
400      * called when the loading is complete.
401      * </p><p>
402      * In case the media item does not have any children, call {@link Result#sendResult}
403      * with an empty list. When the given {@code parentId} is invalid, implementations must
404      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
405      * {@link MediaBrowser.SubscriptionCallback#onError}.
406      * </p>
407      *
408      * @param parentId The id of the parent media item whose children are to be
409      *            queried.
410      * @param result The Result to send the list of children to.
411      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result)412     public abstract void onLoadChildren(@NonNull String parentId,
413             @NonNull Result<List<MediaBrowser.MediaItem>> result);
414 
415     /**
416      * Called to get information about the children of a media item.
417      * <p>
418      * Implementations must call {@link Result#sendResult result.sendResult}
419      * with the list of children. If loading the children will be an expensive
420      * operation that should be performed on another thread,
421      * {@link Result#detach result.detach} may be called before returning from
422      * this function, and then {@link Result#sendResult result.sendResult}
423      * called when the loading is complete.
424      * </p><p>
425      * In case the media item does not have any children, call {@link Result#sendResult}
426      * with an empty list. When the given {@code parentId} is invalid, implementations must
427      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
428      * {@link MediaBrowser.SubscriptionCallback#onError}.
429      * </p>
430      *
431      * @param parentId The id of the parent media item whose children are to be
432      *            queried.
433      * @param result The Result to send the list of children to.
434      * @param options The bundle of service-specific arguments sent from the media
435      *            browser. The information returned through the result should be
436      *            affected by the contents of this bundle.
437      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options)438     public void onLoadChildren(@NonNull String parentId,
439             @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) {
440         // To support backward compatibility, when the implementation of MediaBrowserService doesn't
441         // override onLoadChildren() with options, onLoadChildren() without options will be used
442         // instead, and the options will be applied in the implementation of result.onResultSent().
443         result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
444         onLoadChildren(parentId, result);
445     }
446 
447     /**
448      * Called to get information about a specific media item.
449      * <p>
450      * Implementations must call {@link Result#sendResult result.sendResult}. If
451      * loading the item will be an expensive operation {@link Result#detach
452      * result.detach} may be called before returning from this function, and
453      * then {@link Result#sendResult result.sendResult} called when the item has
454      * been loaded.
455      * </p><p>
456      * When the given {@code itemId} is invalid, implementations must call
457      * {@link Result#sendResult result.sendResult} with {@code null}.
458      * </p><p>
459      * The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}.
460      * </p>
461      *
462      * @param itemId The id for the specific
463      *            {@link android.media.browse.MediaBrowser.MediaItem}.
464      * @param result The Result to send the item to.
465      */
onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)466     public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
467         result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
468         result.sendResult(null);
469     }
470 
471     /**
472      * Call to set the media session.
473      * <p>
474      * This should be called as soon as possible during the service's startup.
475      * It may only be called once.
476      *
477      * @param token The token for the service's {@link MediaSession}.
478      */
setSessionToken(final MediaSession.Token token)479     public void setSessionToken(final MediaSession.Token token) {
480         if (token == null) {
481             throw new IllegalArgumentException("Session token may not be null.");
482         }
483         if (mSession != null) {
484             throw new IllegalStateException("The session token has already been set.");
485         }
486         mSession = token;
487         mHandler.post(new Runnable() {
488             @Override
489             public void run() {
490                 Iterator<ConnectionRecord> iter = mConnections.values().iterator();
491                 while (iter.hasNext()) {
492                     ConnectionRecord connection = iter.next();
493                     try {
494                         connection.callbacks.onConnect(connection.root.getRootId(), token,
495                                 connection.root.getExtras());
496                     } catch (RemoteException e) {
497                         Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
498                         iter.remove();
499                     }
500                 }
501             }
502         });
503     }
504 
505     /**
506      * Gets the session token, or null if it has not yet been created
507      * or if it has been destroyed.
508      */
getSessionToken()509     public @Nullable MediaSession.Token getSessionToken() {
510         return mSession;
511     }
512 
513     /**
514      * Gets the root hints sent from the currently connected {@link MediaBrowser}.
515      * The root hints are service-specific arguments included in an optional bundle sent to the
516      * media browser service when connecting and retrieving the root id for browsing, or null if
517      * none. The contents of this bundle may affect the information returned when browsing.
518      *
519      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
520      *             {@link #onLoadChildren} or {@link #onLoadItem}.
521      * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT
522      * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
523      * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
524      */
getBrowserRootHints()525     public final Bundle getBrowserRootHints() {
526         if (mCurConnection == null) {
527             throw new IllegalStateException("This should be called inside of onGetRoot or"
528                     + " onLoadChildren or onLoadItem methods");
529         }
530         return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
531     }
532 
533     /**
534      * Gets the browser information who sent the current request.
535      *
536      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
537      *             {@link #onLoadChildren} or {@link #onLoadItem}.
538      * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
539      */
getCurrentBrowserInfo()540     public final RemoteUserInfo getCurrentBrowserInfo() {
541         if (mCurConnection == null) {
542             throw new IllegalStateException("This should be called inside of onGetRoot or"
543                     + " onLoadChildren or onLoadItem methods");
544         }
545         return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid);
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         private final String mRootId;
824         private final 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