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