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