MediaBrowser.java revision 51af2f017f704f4d2ca7b5f14fd65ac09d85921a
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.media.browse;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.ServiceConnection;
26import android.content.pm.ParceledListSlice;
27import android.media.MediaDescription;
28import android.media.session.MediaController;
29import android.media.session.MediaSession;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.Parcel;
34import android.os.Parcelable;
35import android.os.RemoteException;
36import android.os.ResultReceiver;
37import android.service.media.IMediaBrowserService;
38import android.service.media.IMediaBrowserServiceCallbacks;
39import android.service.media.MediaBrowserService;
40import android.text.TextUtils;
41import android.util.ArrayMap;
42import android.util.Log;
43
44import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
46import java.lang.ref.WeakReference;
47import java.util.ArrayList;
48import java.util.List;
49import java.util.Map.Entry;
50
51/**
52 * Browses media content offered by a link MediaBrowserService.
53 * <p>
54 * This object is not thread-safe. All calls should happen on the thread on which the browser
55 * was constructed.
56 * </p>
57 * <h3>Standard Extra Data</h3>
58 *
59 * <p>These are the current standard fields that can be used as extra data via
60 * {@link #subscribe(String, Bundle, SubscriptionCallback)}, {@link #unsubscribe(String, Bundle)},
61 * and {@link SubscriptionCallback#onChildrenLoaded(String, List, Bundle)}.
62 *
63 * <ul>
64 *     <li> {@link #EXTRA_PAGE}
65 *     <li> {@link #EXTRA_PAGE_SIZE}
66 * </ul>
67 */
68public final class MediaBrowser {
69    private static final String TAG = "MediaBrowser";
70    private static final boolean DBG = false;
71
72    /**
73     * Used as an int extra field to denote the page number to subscribe.
74     * The value of {@code EXTRA_PAGE} should be greater than or equal to 1.
75     *
76     * @see android.service.media.MediaBrowserService.BrowserRoot
77     * @see #EXTRA_PAGE_SIZE
78     */
79    public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE";
80
81    /**
82     * Used as an int extra field to denote the number of media items in a page.
83     * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1.
84     *
85     * @see android.service.media.MediaBrowserService.BrowserRoot
86     * @see #EXTRA_PAGE
87     */
88    public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
89
90    private static final int CONNECT_STATE_DISCONNECTED = 0;
91    private static final int CONNECT_STATE_CONNECTING = 1;
92    private static final int CONNECT_STATE_CONNECTED = 2;
93    private static final int CONNECT_STATE_SUSPENDED = 3;
94
95    private final Context mContext;
96    private final ComponentName mServiceComponent;
97    private final ConnectionCallback mCallback;
98    private final Bundle mRootHints;
99    private final Handler mHandler = new Handler();
100    private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
101
102    private volatile int mState = CONNECT_STATE_DISCONNECTED;
103    private volatile String mRootId;
104    private volatile MediaSession.Token mMediaSessionToken;
105    private volatile Bundle mExtras;
106
107    private MediaServiceConnection mServiceConnection;
108    private IMediaBrowserService mServiceBinder;
109    private IMediaBrowserServiceCallbacks mServiceCallbacks;
110
111    /**
112     * Creates a media browser for the specified media browse service.
113     *
114     * @param context The context.
115     * @param serviceComponent The component name of the media browse service.
116     * @param callback The connection callback.
117     * @param rootHints An optional bundle of service-specific arguments to send
118     * to the media browse service when connecting and retrieving the root id
119     * for browsing, or null if none. The contents of this bundle may affect
120     * the information returned when browsing.
121     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT
122     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
123     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
124     */
125    public MediaBrowser(Context context, ComponentName serviceComponent,
126            ConnectionCallback callback, Bundle rootHints) {
127        if (context == null) {
128            throw new IllegalArgumentException("context must not be null");
129        }
130        if (serviceComponent == null) {
131            throw new IllegalArgumentException("service component must not be null");
132        }
133        if (callback == null) {
134            throw new IllegalArgumentException("connection callback must not be null");
135        }
136        mContext = context;
137        mServiceComponent = serviceComponent;
138        mCallback = callback;
139        mRootHints = rootHints;
140    }
141
142    /**
143     * Connects to the media browse service.
144     * <p>
145     * The connection callback specified in the constructor will be invoked
146     * when the connection completes or fails.
147     * </p>
148     */
149    public void connect() {
150        if (mState != CONNECT_STATE_DISCONNECTED) {
151            throw new IllegalStateException("connect() called while not disconnected (state="
152                    + getStateLabel(mState) + ")");
153        }
154        // TODO: remove this extra check.
155        if (DBG) {
156            if (mServiceConnection != null) {
157                throw new RuntimeException("mServiceConnection should be null. Instead it is "
158                        + mServiceConnection);
159            }
160        }
161        if (mServiceBinder != null) {
162            throw new RuntimeException("mServiceBinder should be null. Instead it is "
163                    + mServiceBinder);
164        }
165        if (mServiceCallbacks != null) {
166            throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
167                    + mServiceCallbacks);
168        }
169
170        mState = CONNECT_STATE_CONNECTING;
171
172        final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
173        intent.setComponent(mServiceComponent);
174
175        final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
176
177        boolean bound = false;
178        try {
179            bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
180        } catch (Exception ex) {
181            Log.e(TAG, "Failed binding to service " + mServiceComponent);
182        }
183
184        if (!bound) {
185            // Tell them that it didn't work. We are already on the main thread,
186            // but we don't want to do callbacks inside of connect(). So post it,
187            // and then check that we are on the same ServiceConnection. We know
188            // we won't also get an onServiceConnected or onServiceDisconnected,
189            // so we won't be doing double callbacks.
190            mHandler.post(new Runnable() {
191                @Override
192                public void run() {
193                    // Ensure that nobody else came in or tried to connect again.
194                    if (thisConnection == mServiceConnection) {
195                        forceCloseConnection();
196                        mCallback.onConnectionFailed();
197                    }
198                }
199            });
200        }
201
202        if (DBG) {
203            Log.d(TAG, "connect...");
204            dump();
205        }
206    }
207
208    /**
209     * Disconnects from the media browse service.
210     * After this, no more callbacks will be received.
211     */
212    public void disconnect() {
213        // It's ok to call this any state, because allowing this lets apps not have
214        // to check isConnected() unnecessarily. They won't appreciate the extra
215        // assertions for this. We do everything we can here to go back to a sane state.
216        if (mServiceCallbacks != null) {
217            try {
218                mServiceBinder.disconnect(mServiceCallbacks);
219            } catch (RemoteException ex) {
220                // We are disconnecting anyway. Log, just for posterity but it's not
221                // a big problem.
222                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
223            }
224        }
225        forceCloseConnection();
226
227        if (DBG) {
228            Log.d(TAG, "disconnect...");
229            dump();
230        }
231    }
232
233    /**
234     * Null out the variables and unbind from the service. This doesn't include
235     * calling disconnect on the service, because we only try to do that in the
236     * clean shutdown cases.
237     * <p>
238     * Everywhere that calls this EXCEPT for disconnect() should follow it with
239     * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
240     * for a clean shutdown, but everywhere else is a dirty shutdown and should
241     * notify the app.
242     */
243    private void forceCloseConnection() {
244        if (mServiceConnection != null) {
245            mContext.unbindService(mServiceConnection);
246        }
247        mState = CONNECT_STATE_DISCONNECTED;
248        mServiceConnection = null;
249        mServiceBinder = null;
250        mServiceCallbacks = null;
251        mRootId = null;
252        mMediaSessionToken = null;
253    }
254
255    /**
256     * Returns whether the browser is connected to the service.
257     */
258    public boolean isConnected() {
259        return mState == CONNECT_STATE_CONNECTED;
260    }
261
262    /**
263     * Gets the service component that the media browser is connected to.
264     */
265    public @NonNull ComponentName getServiceComponent() {
266        if (!isConnected()) {
267            throw new IllegalStateException("getServiceComponent() called while not connected" +
268                    " (state=" + mState + ")");
269        }
270        return mServiceComponent;
271    }
272
273    /**
274     * Gets the root id.
275     * <p>
276     * Note that the root id may become invalid or change when the
277     * browser is disconnected.
278     * </p>
279     *
280     * @throws IllegalStateException if not connected.
281     */
282    public @NonNull String getRoot() {
283        if (!isConnected()) {
284            throw new IllegalStateException("getRoot() called while not connected (state="
285                    + getStateLabel(mState) + ")");
286        }
287        return mRootId;
288    }
289
290    /**
291     * Gets any extras for the media service.
292     *
293     * @throws IllegalStateException if not connected.
294     */
295    public @Nullable Bundle getExtras() {
296        if (!isConnected()) {
297            throw new IllegalStateException("getExtras() called while not connected (state="
298                    + getStateLabel(mState) + ")");
299        }
300        return mExtras;
301    }
302
303    /**
304     * Gets the media session token associated with the media browser.
305     * <p>
306     * Note that the session token may become invalid or change when the
307     * browser is disconnected.
308     * </p>
309     *
310     * @return The session token for the browser, never null.
311     *
312     * @throws IllegalStateException if not connected.
313     */
314     public @NonNull MediaSession.Token getSessionToken() {
315        if (!isConnected()) {
316            throw new IllegalStateException("getSessionToken() called while not connected (state="
317                    + mState + ")");
318        }
319        return mMediaSessionToken;
320    }
321
322    /**
323     * Queries for information about the media items that are contained within
324     * the specified id and subscribes to receive updates when they change.
325     * <p>
326     * The list of subscriptions is maintained even when not connected and is
327     * restored after the reconnection. It is ok to subscribe while not connected
328     * but the results will not be returned until the connection completes.
329     * </p>
330     * <p>
331     * If the id is already subscribed with a different callback then the new
332     * callback will replace the previous one and the child data will be
333     * reloaded.
334     * </p>
335     *
336     * @param parentId The id of the parent media item whose list of children
337     *            will be subscribed.
338     * @param callback The callback to receive the list of children.
339     */
340    public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
341        subscribeInternal(parentId, null, callback);
342    }
343
344    /**
345     * Queries with service-specific arguments for information about the media items
346     * that are contained within the specified id and subscribes to receive updates
347     * when they change.
348     * <p>
349     * The list of subscriptions is maintained even when not connected and is
350     * restored after the reconnection. It is ok to subscribe while not connected
351     * but the results will not be returned until the connection completes.
352     * </p>
353     * <p>
354     * If the id is already subscribed with a different callback then the new
355     * callback will replace the previous one and the child data will be
356     * reloaded.
357     * </p>
358     *
359     * @param parentId The id of the parent media item whose list of children
360     *            will be subscribed.
361     * @param options A bundle of service-specific arguments to send to the media
362     *            browse service. The contents of this bundle may affect the
363     *            information returned when browsing.
364     * @param callback The callback to receive the list of children.
365     */
366    public void subscribe(@NonNull String parentId, @NonNull Bundle options,
367            @NonNull SubscriptionCallback callback) {
368        if (options == null) {
369            throw new IllegalArgumentException("options are null");
370        }
371        subscribeInternal(parentId, options, callback);
372    }
373
374    /**
375     * Unsubscribes for changes to the children of the specified media id.
376     * <p>
377     * The query callback will no longer be invoked for results associated with
378     * this id once this method returns.
379     * </p>
380     *
381     * @param parentId The id of the parent media item whose list of children
382     *            will be unsubscribed.
383     */
384    public void unsubscribe(@NonNull String parentId) {
385        unsubscribeInternal(parentId, null);
386    }
387
388    /**
389     * Unsubscribes for changes to the children of the specified media id.
390     * <p>
391     * The query callback will no longer be invoked for results associated with
392     * this id once this method returns.
393     * </p>
394     *
395     * @param parentId The id of the parent media item whose list of children
396     *            will be unsubscribed.
397     * @param options A bundle sent to the media browse service to subscribe.
398     */
399    public void unsubscribe(@NonNull String parentId, @NonNull Bundle options) {
400        if (options == null) {
401            throw new IllegalArgumentException("options are null");
402        }
403        unsubscribeInternal(parentId, options);
404    }
405
406    /**
407     * Retrieves a specific {@link MediaItem} from the connected service. Not
408     * all services may support this, so falling back to subscribing to the
409     * parent's id should be used when unavailable.
410     *
411     * @param mediaId The id of the item to retrieve.
412     * @param cb The callback to receive the result on.
413     */
414    public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
415        if (TextUtils.isEmpty(mediaId)) {
416            throw new IllegalArgumentException("mediaId is empty.");
417        }
418        if (cb == null) {
419            throw new IllegalArgumentException("cb is null.");
420        }
421        if (mState != CONNECT_STATE_CONNECTED) {
422            Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
423            mHandler.post(new Runnable() {
424                @Override
425                public void run() {
426                    cb.onError(mediaId);
427                }
428            });
429            return;
430        }
431        ResultReceiver receiver = new ResultReceiver(mHandler) {
432            @Override
433            protected void onReceiveResult(int resultCode, Bundle resultData) {
434                if (resultCode != 0 || resultData == null
435                        || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) {
436                    cb.onError(mediaId);
437                    return;
438                }
439                Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM);
440                if (!(item instanceof MediaItem)) {
441                    cb.onError(mediaId);
442                    return;
443                }
444                cb.onItemLoaded((MediaItem)item);
445            }
446        };
447        try {
448            mServiceBinder.getMediaItem(mediaId, receiver);
449        } catch (RemoteException e) {
450            Log.i(TAG, "Remote error getting media item.");
451            mHandler.post(new Runnable() {
452                @Override
453                public void run() {
454                    cb.onError(mediaId);
455                }
456            });
457        }
458    }
459
460    private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) {
461        // Check arguments.
462        if (TextUtils.isEmpty(parentId)) {
463            throw new IllegalArgumentException("parentId is empty.");
464        }
465        if (callback == null) {
466            throw new IllegalArgumentException("callback is null");
467        }
468        // Update or create the subscription.
469        Subscription sub = mSubscriptions.get(parentId);
470        if (sub == null) {
471            sub = new Subscription();
472            mSubscriptions.put(parentId, sub);
473        }
474        sub.add(callback, options);
475
476        // If we are connected, tell the service that we are watching. If we aren't connected,
477        // the service will be told when we connect.
478        if (mState == CONNECT_STATE_CONNECTED) {
479            try {
480                // NOTE: Do not call addSubscriptionWithOptions when options are null. Otherwise,
481                // it will break the action of support library which expects addSubscription will
482                // be called when options are null.
483                if (options == null) {
484                    mServiceBinder.addSubscription(parentId, mServiceCallbacks);
485                } else {
486                    mServiceBinder.addSubscriptionWithOptions(parentId, options, mServiceCallbacks);
487                }
488            } catch (RemoteException ex) {
489                // Process is crashing. We will disconnect, and upon reconnect we will
490                // automatically reregister. So nothing to do here.
491                Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
492            }
493        }
494    }
495
496    private void unsubscribeInternal(String parentId, Bundle options) {
497        // Check arguments.
498        if (TextUtils.isEmpty(parentId)) {
499            throw new IllegalArgumentException("parentId is empty.");
500        }
501
502        // Remove from our list.
503        Subscription sub = mSubscriptions.get(parentId);
504
505        // Tell the service if necessary.
506        if (sub != null && sub.remove(options) && mState == CONNECT_STATE_CONNECTED) {
507            try {
508                // NOTE: Do not call removeSubscriptionWithOptions when options are null. Otherwise,
509                // it will break the action of support library which expects removeSubscription will
510                // be called when options are null.
511                if (options == null) {
512                    mServiceBinder.removeSubscription(parentId, mServiceCallbacks);
513                } else {
514                    mServiceBinder.removeSubscriptionWithOptions(
515                            parentId, options, mServiceCallbacks);
516                }
517            } catch (RemoteException ex) {
518                // Process is crashing. We will disconnect, and upon reconnect we will
519                // automatically reregister. So nothing to do here.
520                Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
521            }
522        }
523        if (sub != null && sub.isEmpty()) {
524            mSubscriptions.remove(parentId);
525        }
526    }
527
528    /**
529     * For debugging.
530     */
531    private static String getStateLabel(int state) {
532        switch (state) {
533            case CONNECT_STATE_DISCONNECTED:
534                return "CONNECT_STATE_DISCONNECTED";
535            case CONNECT_STATE_CONNECTING:
536                return "CONNECT_STATE_CONNECTING";
537            case CONNECT_STATE_CONNECTED:
538                return "CONNECT_STATE_CONNECTED";
539            case CONNECT_STATE_SUSPENDED:
540                return "CONNECT_STATE_SUSPENDED";
541            default:
542                return "UNKNOWN/" + state;
543        }
544    }
545
546    private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
547            final String root, final MediaSession.Token session, final Bundle extra) {
548        mHandler.post(new Runnable() {
549            @Override
550            public void run() {
551                // Check to make sure there hasn't been a disconnect or a different
552                // ServiceConnection.
553                if (!isCurrent(callback, "onConnect")) {
554                    return;
555                }
556                // Don't allow them to call us twice.
557                if (mState != CONNECT_STATE_CONNECTING) {
558                    Log.w(TAG, "onConnect from service while mState="
559                            + getStateLabel(mState) + "... ignoring");
560                    return;
561                }
562                mRootId = root;
563                mMediaSessionToken = session;
564                mExtras = extra;
565                mState = CONNECT_STATE_CONNECTED;
566
567                if (DBG) {
568                    Log.d(TAG, "ServiceCallbacks.onConnect...");
569                    dump();
570                }
571                mCallback.onConnected();
572
573                // we may receive some subscriptions before we are connected, so re-subscribe
574                // everything now
575                for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) {
576                    String id = subscriptionEntry.getKey();
577                    Subscription sub = subscriptionEntry.getValue();
578                    for (Bundle options : sub.getOptionsList()) {
579                        try {
580                            // NOTE: Do not call addSubscriptionWithOptions when options are null.
581                            // Otherwise, it will break the action of support library which expects
582                            // addSubscription will be called when options are null.
583                            if (options == null) {
584                                mServiceBinder.addSubscription(id, mServiceCallbacks);
585                            } else {
586                                mServiceBinder.addSubscriptionWithOptions(
587                                        id, options, mServiceCallbacks);
588                            }
589                        } catch (RemoteException ex) {
590                            // Process is crashing. We will disconnect, and upon reconnect we will
591                            // automatically reregister. So nothing to do here.
592                            Log.d(TAG, "addSubscription failed with RemoteException parentId="
593                                    + id);
594                        }
595                    }
596                }
597            }
598        });
599    }
600
601    private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
602        mHandler.post(new Runnable() {
603            @Override
604            public void run() {
605                Log.e(TAG, "onConnectFailed for " + mServiceComponent);
606
607                // Check to make sure there hasn't been a disconnect or a different
608                // ServiceConnection.
609                if (!isCurrent(callback, "onConnectFailed")) {
610                    return;
611                }
612                // Don't allow them to call us twice.
613                if (mState != CONNECT_STATE_CONNECTING) {
614                    Log.w(TAG, "onConnect from service while mState="
615                            + getStateLabel(mState) + "... ignoring");
616                    return;
617                }
618
619                // Clean up
620                forceCloseConnection();
621
622                // Tell the app.
623                mCallback.onConnectionFailed();
624            }
625        });
626    }
627
628    private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback,
629            final String parentId, final ParceledListSlice list, final Bundle options) {
630        mHandler.post(new Runnable() {
631            @Override
632            public void run() {
633                // Check that there hasn't been a disconnect or a different
634                // ServiceConnection.
635                if (!isCurrent(callback, "onLoadChildren")) {
636                    return;
637                }
638
639                List<MediaItem> data = list == null ? null : list.getList();
640                if (DBG) {
641                    Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
642                }
643
644                // Check that the subscription is still subscribed.
645                final Subscription subscription = mSubscriptions.get(parentId);
646                if (subscription != null) {
647                    // Tell the app.
648                    SubscriptionCallback subscriptionCallback = subscription.getCallback(options);
649                    if (subscriptionCallback != null) {
650                        if (options == null) {
651                            subscriptionCallback.onChildrenLoaded(parentId, data);
652                        } else {
653                            subscriptionCallback.onChildrenLoaded(parentId, data, options);
654                        }
655                        return;
656                    }
657                }
658                if (DBG) {
659                    Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
660                }
661            }
662        });
663    }
664
665    /**
666     * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
667     */
668    private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
669        if (mServiceCallbacks != callback) {
670            if (mState != CONNECT_STATE_DISCONNECTED) {
671                Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
672                        + mServiceCallbacks + " this=" + this);
673            }
674            return false;
675        }
676        return true;
677    }
678
679    private ServiceCallbacks getNewServiceCallbacks() {
680        return new ServiceCallbacks(this);
681    }
682
683    /**
684     * Log internal state.
685     * @hide
686     */
687    void dump() {
688        Log.d(TAG, "MediaBrowser...");
689        Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
690        Log.d(TAG, "  mCallback=" + mCallback);
691        Log.d(TAG, "  mRootHints=" + mRootHints);
692        Log.d(TAG, "  mState=" + getStateLabel(mState));
693        Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
694        Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
695        Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
696        Log.d(TAG, "  mRootId=" + mRootId);
697        Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
698    }
699
700    /**
701     * A class with information on a single media item for use in browsing media.
702     */
703    public static class MediaItem implements Parcelable {
704        private final int mFlags;
705        private final MediaDescription mDescription;
706
707        /** @hide */
708        @Retention(RetentionPolicy.SOURCE)
709        @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
710        public @interface Flags { }
711
712        /**
713         * Flag: Indicates that the item has children of its own.
714         */
715        public static final int FLAG_BROWSABLE = 1 << 0;
716
717        /**
718         * Flag: Indicates that the item is playable.
719         * <p>
720         * The id of this item may be passed to
721         * {@link MediaController.TransportControls#playFromMediaId(String, Bundle)}
722         * to start playing it.
723         * </p>
724         */
725        public static final int FLAG_PLAYABLE = 1 << 1;
726
727        /**
728         * Create a new MediaItem for use in browsing media.
729         * @param description The description of the media, which must include a
730         *            media id.
731         * @param flags The flags for this item.
732         */
733        public MediaItem(@NonNull MediaDescription description, @Flags int flags) {
734            if (description == null) {
735                throw new IllegalArgumentException("description cannot be null");
736            }
737            if (TextUtils.isEmpty(description.getMediaId())) {
738                throw new IllegalArgumentException("description must have a non-empty media id");
739            }
740            mFlags = flags;
741            mDescription = description;
742        }
743
744        /**
745         * Private constructor.
746         */
747        private MediaItem(Parcel in) {
748            mFlags = in.readInt();
749            mDescription = MediaDescription.CREATOR.createFromParcel(in);
750        }
751
752        @Override
753        public int describeContents() {
754            return 0;
755        }
756
757        @Override
758        public void writeToParcel(Parcel out, int flags) {
759            out.writeInt(mFlags);
760            mDescription.writeToParcel(out, flags);
761        }
762
763        @Override
764        public String toString() {
765            final StringBuilder sb = new StringBuilder("MediaItem{");
766            sb.append("mFlags=").append(mFlags);
767            sb.append(", mDescription=").append(mDescription);
768            sb.append('}');
769            return sb.toString();
770        }
771
772        public static final Parcelable.Creator<MediaItem> CREATOR =
773                new Parcelable.Creator<MediaItem>() {
774                    @Override
775                    public MediaItem createFromParcel(Parcel in) {
776                        return new MediaItem(in);
777                    }
778
779                    @Override
780                    public MediaItem[] newArray(int size) {
781                        return new MediaItem[size];
782                    }
783                };
784
785        /**
786         * Gets the flags of the item.
787         */
788        public @Flags int getFlags() {
789            return mFlags;
790        }
791
792        /**
793         * Returns whether this item is browsable.
794         * @see #FLAG_BROWSABLE
795         */
796        public boolean isBrowsable() {
797            return (mFlags & FLAG_BROWSABLE) != 0;
798        }
799
800        /**
801         * Returns whether this item is playable.
802         * @see #FLAG_PLAYABLE
803         */
804        public boolean isPlayable() {
805            return (mFlags & FLAG_PLAYABLE) != 0;
806        }
807
808        /**
809         * Returns the description of the media.
810         */
811        public @NonNull MediaDescription getDescription() {
812            return mDescription;
813        }
814
815        /**
816         * Returns the media id for this item.
817         */
818        public @NonNull String getMediaId() {
819            return mDescription.getMediaId();
820        }
821    }
822
823    /**
824     * Callbacks for connection related events.
825     */
826    public static class ConnectionCallback {
827        /**
828         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
829         */
830        public void onConnected() {
831        }
832
833        /**
834         * Invoked when the client is disconnected from the media browser.
835         */
836        public void onConnectionSuspended() {
837        }
838
839        /**
840         * Invoked when the connection to the media browser failed.
841         */
842        public void onConnectionFailed() {
843        }
844    }
845
846    /**
847     * Callbacks for subscription related events.
848     */
849    public static abstract class SubscriptionCallback {
850        /**
851         * Called when the list of children is loaded or updated.
852         *
853         * @param parentId The media id of the parent media item.
854         * @param children The children which were loaded, or null if the id is invalid.
855         */
856        public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children) {
857        }
858
859        /**
860         * Called when the list of children is loaded or updated.
861         *
862         * @param parentId The media id of the parent media item.
863         * @param children The children which were loaded, or null if the id is invalid.
864         * @param options A bundle of service-specific arguments to send to the media
865         *            browse service. The contents of this bundle may affect the
866         *            information returned when browsing.
867         */
868        public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children,
869                @NonNull Bundle options) {
870        }
871
872        /**
873         * Called when the id doesn't exist or other errors in subscribing.
874         * <p>
875         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
876         * called, because some errors may heal themselves.
877         * </p>
878         *
879         * @param parentId The media id of the parent media item whose children could
880         *            not be loaded.
881         */
882        public void onError(@NonNull String parentId) {
883        }
884
885        /**
886         * Called when the id doesn't exist or other errors in subscribing.
887         * <p>
888         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
889         * called, because some errors may heal themselves.
890         * </p>
891         *
892         * @param parentId The media id of the parent media item whose children could
893         *            not be loaded.
894         * @param options A bundle of service-specific arguments sent to the media
895         *            browse service.
896         */
897        public void onError(@NonNull String parentId, @NonNull Bundle options) {
898        }
899    }
900
901    /**
902     * Callback for receiving the result of {@link #getItem}.
903     */
904    public static abstract class ItemCallback {
905        /**
906         * Called when the item has been returned by the browser service.
907         *
908         * @param item The item that was returned or null if it doesn't exist.
909         */
910        public void onItemLoaded(MediaItem item) {
911        }
912
913        /**
914         * Called when the item doesn't exist or there was an error retrieving it.
915         *
916         * @param itemId The media id of the media item which could not be loaded.
917         */
918        public void onError(@NonNull String itemId) {
919        }
920    }
921
922    /**
923     * ServiceConnection to the other app.
924     */
925    private class MediaServiceConnection implements ServiceConnection {
926        @Override
927        public void onServiceConnected(final ComponentName name, final IBinder binder) {
928            postOrRun(new Runnable() {
929                @Override
930                public void run() {
931                    if (DBG) {
932                        Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
933                                + " binder=" + binder);
934                        dump();
935                    }
936
937                    // Make sure we are still the current connection, and that they haven't called
938                    // disconnect().
939                    if (!isCurrent("onServiceConnected")) {
940                        return;
941                    }
942
943                    // Save their binder
944                    mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
945
946                    // We make a new mServiceCallbacks each time we connect so that we can drop
947                    // responses from previous connections.
948                    mServiceCallbacks = getNewServiceCallbacks();
949                    mState = CONNECT_STATE_CONNECTING;
950
951                    // Call connect, which is async. When we get a response from that we will
952                    // say that we're connected.
953                    try {
954                        if (DBG) {
955                            Log.d(TAG, "ServiceCallbacks.onConnect...");
956                            dump();
957                        }
958                        mServiceBinder.connect(mContext.getPackageName(), mRootHints,
959                                mServiceCallbacks);
960                    } catch (RemoteException ex) {
961                        // Connect failed, which isn't good. But the auto-reconnect on the service
962                        // will take over and we will come back. We will also get the
963                        // onServiceDisconnected, which has all the cleanup code. So let that do
964                        // it.
965                        Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
966                        if (DBG) {
967                            Log.d(TAG, "ServiceCallbacks.onConnect...");
968                            dump();
969                        }
970                    }
971                }
972            });
973        }
974
975        @Override
976        public void onServiceDisconnected(final ComponentName name) {
977            postOrRun(new Runnable() {
978                @Override
979                public void run() {
980                    if (DBG) {
981                        Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
982                                + " this=" + this + " mServiceConnection=" + mServiceConnection);
983                        dump();
984                    }
985
986                    // Make sure we are still the current connection, and that they haven't called
987                    // disconnect().
988                    if (!isCurrent("onServiceDisconnected")) {
989                        return;
990                    }
991
992                    // Clear out what we set in onServiceConnected
993                    mServiceBinder = null;
994                    mServiceCallbacks = null;
995
996                    // And tell the app that it's suspended.
997                    mState = CONNECT_STATE_SUSPENDED;
998                    mCallback.onConnectionSuspended();
999                }
1000            });
1001        }
1002
1003        private void postOrRun(Runnable r) {
1004            if (Thread.currentThread() == mHandler.getLooper().getThread()) {
1005                r.run();
1006            } else {
1007                mHandler.post(r);
1008            }
1009        }
1010
1011        /**
1012         * Return true if this is the current ServiceConnection. Also logs if it's not.
1013         */
1014        private boolean isCurrent(String funcName) {
1015            if (mServiceConnection != this) {
1016                if (mState != CONNECT_STATE_DISCONNECTED) {
1017                    // Check mState, because otherwise this log is noisy.
1018                    Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
1019                            + mServiceConnection + " this=" + this);
1020                }
1021                return false;
1022            }
1023            return true;
1024        }
1025    }
1026
1027    /**
1028     * Callbacks from the service.
1029     */
1030    private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
1031        private WeakReference<MediaBrowser> mMediaBrowser;
1032
1033        public ServiceCallbacks(MediaBrowser mediaBrowser) {
1034            mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
1035        }
1036
1037        /**
1038         * The other side has acknowledged our connection. The parameters to this function
1039         * are the initial data as requested.
1040         */
1041        @Override
1042        public void onConnect(String root, MediaSession.Token session,
1043                final Bundle extras) {
1044            MediaBrowser mediaBrowser = mMediaBrowser.get();
1045            if (mediaBrowser != null) {
1046                mediaBrowser.onServiceConnected(this, root, session, extras);
1047            }
1048        }
1049
1050        /**
1051         * The other side does not like us. Tell the app via onConnectionFailed.
1052         */
1053        @Override
1054        public void onConnectFailed() {
1055            MediaBrowser mediaBrowser = mMediaBrowser.get();
1056            if (mediaBrowser != null) {
1057                mediaBrowser.onConnectionFailed(this);
1058            }
1059        }
1060
1061        @Override
1062        public void onLoadChildren(String parentId, ParceledListSlice list) {
1063            onLoadChildrenWithOptions(parentId, list, null);
1064        }
1065
1066        @Override
1067        public void onLoadChildrenWithOptions(String parentId, ParceledListSlice list,
1068                final Bundle options) {
1069            MediaBrowser mediaBrowser = mMediaBrowser.get();
1070            if (mediaBrowser != null) {
1071                mediaBrowser.onLoadChildren(this, parentId, list, options);
1072            }
1073        }
1074    }
1075
1076    private static class Subscription {
1077        private final List<SubscriptionCallback> mCallbacks;
1078        private final List<Bundle> mOptionsList;
1079
1080        public Subscription() {
1081            mCallbacks = new ArrayList<>();
1082            mOptionsList = new ArrayList<>();
1083        }
1084
1085        public boolean isEmpty() {
1086            return mCallbacks.isEmpty();
1087        }
1088
1089        public List<Bundle> getOptionsList() {
1090            return mOptionsList;
1091        }
1092
1093        public List<SubscriptionCallback> getCallbacks() {
1094            return mCallbacks;
1095        }
1096
1097        public void add(SubscriptionCallback callback, Bundle options) {
1098            for (int i = 0; i < mOptionsList.size(); ++i) {
1099                if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1100                    mCallbacks.set(i, callback);
1101                    return;
1102                }
1103            }
1104            mCallbacks.add(callback);
1105            mOptionsList.add(options);
1106        }
1107
1108        public boolean remove(Bundle options) {
1109            for (int i = 0; i < mOptionsList.size(); ++i) {
1110                if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1111                    mCallbacks.remove(i);
1112                    mOptionsList.remove(i);
1113                    return true;
1114                }
1115            }
1116            return false;
1117        }
1118
1119        public SubscriptionCallback getCallback(Bundle options) {
1120            for (int i = 0; i < mOptionsList.size(); ++i) {
1121                if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1122                    return mCallbacks.get(i);
1123                }
1124            }
1125            return null;
1126        }
1127    }
1128}
1129