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