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