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