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