MediaBrowser.java revision 37e6986d670fffa670cfe5a303b88c15ff2c9efc
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.Binder;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.Parcel;
35import android.os.Parcelable;
36import android.os.RemoteException;
37import android.os.ResultReceiver;
38import android.service.media.IMediaBrowserService;
39import android.service.media.IMediaBrowserServiceCallbacks;
40import android.service.media.MediaBrowserService;
41import android.text.TextUtils;
42import android.util.ArrayMap;
43import android.util.Log;
44
45import java.lang.annotation.Retention;
46import java.lang.annotation.RetentionPolicy;
47import java.lang.ref.WeakReference;
48import java.util.ArrayList;
49import java.util.List;
50import java.util.Map.Entry;
51
52/**
53 * Browses media content offered by a link MediaBrowserService.
54 * <p>
55 * This object is not thread-safe. All calls should happen on the thread on which the browser
56 * was constructed.
57 * </p>
58 * <h3>Standard Extra Data</h3>
59 *
60 * <p>These are the current standard fields that can be used as extra data via
61 * {@link #subscribe(String, Bundle, SubscriptionCallback)},
62 * {@link #unsubscribe(String, SubscriptionCallback)}, and
63 * {@link SubscriptionCallback#onChildrenLoaded(String, List, Bundle)}.
64 *
65 * <ul>
66 *     <li> {@link #EXTRA_PAGE}
67 *     <li> {@link #EXTRA_PAGE_SIZE}
68 * </ul>
69 */
70public final class MediaBrowser {
71    private static final String TAG = "MediaBrowser";
72    private static final boolean DBG = false;
73
74    /**
75     * Used as an int extra field to denote the page number to subscribe.
76     * The value of {@code EXTRA_PAGE} should be greater than or equal to 0.
77     *
78     * @see #EXTRA_PAGE_SIZE
79     */
80    public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE";
81
82    /**
83     * Used as an int extra field to denote the number of media items in a page.
84     * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1.
85     *
86     * @see #EXTRA_PAGE
87     */
88    public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
89
90    private static final int CONNECT_STATE_DISCONNECTED = 0;
91    private static final int CONNECT_STATE_CONNECTING = 1;
92    private static final int CONNECT_STATE_CONNECTED = 2;
93    private static final int CONNECT_STATE_SUSPENDED = 3;
94
95    private final Context mContext;
96    private final ComponentName mServiceComponent;
97    private final ConnectionCallback mCallback;
98    private final Bundle mRootHints;
99    private final Handler mHandler = new Handler();
100    private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
101
102    private volatile int mState = CONNECT_STATE_DISCONNECTED;
103    private volatile String mRootId;
104    private volatile MediaSession.Token mMediaSessionToken;
105    private volatile Bundle mExtras;
106
107    private MediaServiceConnection mServiceConnection;
108    private IMediaBrowserService mServiceBinder;
109    private IMediaBrowserServiceCallbacks mServiceCallbacks;
110
111    /**
112     * Creates a media browser for the specified media browse service.
113     *
114     * @param context The context.
115     * @param serviceComponent The component name of the media browse service.
116     * @param callback The connection callback.
117     * @param rootHints An optional bundle of service-specific arguments to send
118     * to the media browse service when connecting and retrieving the root id
119     * for browsing, or null if none. The contents of this bundle may affect
120     * the information returned when browsing.
121     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT
122     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
123     * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
124     */
125    public MediaBrowser(Context context, ComponentName serviceComponent,
126            ConnectionCallback callback, Bundle rootHints) {
127        if (context == null) {
128            throw new IllegalArgumentException("context must not be null");
129        }
130        if (serviceComponent == null) {
131            throw new IllegalArgumentException("service component must not be null");
132        }
133        if (callback == null) {
134            throw new IllegalArgumentException("connection callback must not be null");
135        }
136        mContext = context;
137        mServiceComponent = serviceComponent;
138        mCallback = callback;
139        mRootHints = rootHints;
140    }
141
142    /**
143     * Connects to the media browse service.
144     * <p>
145     * The connection callback specified in the constructor will be invoked
146     * when the connection completes or fails.
147     * </p>
148     */
149    public void connect() {
150        if (mState != CONNECT_STATE_DISCONNECTED) {
151            throw new IllegalStateException("connect() called while not disconnected (state="
152                    + getStateLabel(mState) + ")");
153        }
154        // TODO: remove this extra check.
155        if (DBG) {
156            if (mServiceConnection != null) {
157                throw new RuntimeException("mServiceConnection should be null. Instead it is "
158                        + mServiceConnection);
159            }
160        }
161        if (mServiceBinder != null) {
162            throw new RuntimeException("mServiceBinder should be null. Instead it is "
163                    + mServiceBinder);
164        }
165        if (mServiceCallbacks != null) {
166            throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
167                    + mServiceCallbacks);
168        }
169
170        mState = CONNECT_STATE_CONNECTING;
171
172        final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
173        intent.setComponent(mServiceComponent);
174
175        final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
176
177        boolean bound = false;
178        try {
179            bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
180        } catch (Exception ex) {
181            Log.e(TAG, "Failed binding to service " + mServiceComponent);
182        }
183
184        if (!bound) {
185            // Tell them that it didn't work. We are already on the main thread,
186            // but we don't want to do callbacks inside of connect(). So post it,
187            // and then check that we are on the same ServiceConnection. We know
188            // we won't also get an onServiceConnected or onServiceDisconnected,
189            // so we won't be doing double callbacks.
190            mHandler.post(new Runnable() {
191                @Override
192                public void run() {
193                    // Ensure that nobody else came in or tried to connect again.
194                    if (thisConnection == mServiceConnection) {
195                        forceCloseConnection();
196                        mCallback.onConnectionFailed();
197                    }
198                }
199            });
200        }
201
202        if (DBG) {
203            Log.d(TAG, "connect...");
204            dump();
205        }
206    }
207
208    /**
209     * Disconnects from the media browse service.
210     * After this, no more callbacks will be received.
211     */
212    public void disconnect() {
213        // It's ok to call this any state, because allowing this lets apps not have
214        // to check isConnected() unnecessarily. They won't appreciate the extra
215        // assertions for this. We do everything we can here to go back to a sane state.
216        mHandler.post(new Runnable() {
217            @Override
218            public void run() {
219                if (mServiceCallbacks != null) {
220                    try {
221                        mServiceBinder.disconnect(mServiceCallbacks);
222                    } catch (RemoteException ex) {
223                        // We are disconnecting anyway. Log, just for posterity but it's not
224                        // a big problem.
225                        Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
226                    }
227                }
228                forceCloseConnection();
229                if (DBG) {
230                    Log.d(TAG, "disconnect...");
231                    dump();
232                }
233            }
234        });
235    }
236
237    /**
238     * Null out the variables and unbind from the service. This doesn't include
239     * calling disconnect on the service, because we only try to do that in the
240     * clean shutdown cases.
241     * <p>
242     * Everywhere that calls this EXCEPT for disconnect() should follow it with
243     * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
244     * for a clean shutdown, but everywhere else is a dirty shutdown and should
245     * notify the app.
246     */
247    private void forceCloseConnection() {
248        if (mServiceConnection != null) {
249            mContext.unbindService(mServiceConnection);
250        }
251        mState = CONNECT_STATE_DISCONNECTED;
252        mServiceConnection = null;
253        mServiceBinder = null;
254        mServiceCallbacks = null;
255        mRootId = null;
256        mMediaSessionToken = null;
257    }
258
259    /**
260     * Returns whether the browser is connected to the service.
261     */
262    public boolean isConnected() {
263        return mState == CONNECT_STATE_CONNECTED;
264    }
265
266    /**
267     * Gets the service component that the media browser is connected to.
268     */
269    public @NonNull ComponentName getServiceComponent() {
270        if (!isConnected()) {
271            throw new IllegalStateException("getServiceComponent() called while not connected" +
272                    " (state=" + mState + ")");
273        }
274        return mServiceComponent;
275    }
276
277    /**
278     * Gets the root id.
279     * <p>
280     * Note that the root id may become invalid or change when the
281     * browser is disconnected.
282     * </p>
283     *
284     * @throws IllegalStateException if not connected.
285     */
286    public @NonNull String getRoot() {
287        if (!isConnected()) {
288            throw new IllegalStateException("getRoot() called while not connected (state="
289                    + getStateLabel(mState) + ")");
290        }
291        return mRootId;
292    }
293
294    /**
295     * Gets any extras for the media service.
296     *
297     * @throws IllegalStateException if not connected.
298     */
299    public @Nullable Bundle getExtras() {
300        if (!isConnected()) {
301            throw new IllegalStateException("getExtras() called while not connected (state="
302                    + getStateLabel(mState) + ")");
303        }
304        return mExtras;
305    }
306
307    /**
308     * Gets the media session token associated with the media browser.
309     * <p>
310     * Note that the session token may become invalid or change when the
311     * browser is disconnected.
312     * </p>
313     *
314     * @return The session token for the browser, never null.
315     *
316     * @throws IllegalStateException if not connected.
317     */
318     public @NonNull MediaSession.Token getSessionToken() {
319        if (!isConnected()) {
320            throw new IllegalStateException("getSessionToken() called while not connected (state="
321                    + mState + ")");
322        }
323        return mMediaSessionToken;
324    }
325
326    /**
327     * Queries for information about the media items that are contained within
328     * the specified id and subscribes to receive updates when they change.
329     * <p>
330     * The list of subscriptions is maintained even when not connected and is
331     * restored after the reconnection. It is ok to subscribe while not connected
332     * but the results will not be returned until the connection completes.
333     * </p>
334     * <p>
335     * If the id is already subscribed with a different callback then the new
336     * callback will replace the previous one and the child data will be
337     * reloaded.
338     * </p>
339     *
340     * @param parentId The id of the parent media item whose list of children
341     *            will be subscribed.
342     * @param callback The callback to receive the list of children.
343     */
344    public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
345        subscribeInternal(parentId, null, callback);
346    }
347
348    /**
349     * Queries with service-specific arguments for information about the media items
350     * that are contained within the specified id and subscribes to receive updates
351     * when they change.
352     * <p>
353     * The list of subscriptions is maintained even when not connected and is
354     * restored after the reconnection. It is ok to subscribe while not connected
355     * but the results will not be returned until the connection completes.
356     * </p>
357     * <p>
358     * If the id is already subscribed with a different callback then the new
359     * callback will replace the previous one and the child data will be
360     * reloaded.
361     * </p>
362     *
363     * @param parentId The id of the parent media item whose list of children
364     *            will be subscribed.
365     * @param options A bundle of service-specific arguments to send to the media
366     *            browse service. The contents of this bundle may affect the
367     *            information returned when browsing.
368     * @param callback The callback to receive the list of children.
369     */
370    public void subscribe(@NonNull String parentId, @NonNull Bundle options,
371            @NonNull SubscriptionCallback callback) {
372        if (options == null) {
373            throw new IllegalArgumentException("options are null");
374        }
375        subscribeInternal(parentId, new Bundle(options), callback);
376    }
377
378    /**
379     * Unsubscribes for changes to the children of the specified media id.
380     * <p>
381     * The query callback will no longer be invoked for results associated with
382     * this id once this method returns.
383     * </p>
384     *
385     * @param parentId The id of the parent media item whose list of children
386     *            will be unsubscribed.
387     */
388    public void unsubscribe(@NonNull String parentId) {
389        unsubscribeInternal(parentId, null);
390    }
391
392    /**
393     * Unsubscribes for changes to the children of the specified media id through a callback.
394     * <p>
395     * The query callback will no longer be invoked for results associated with
396     * this id once this method returns.
397     * </p>
398     *
399     * @param parentId The id of the parent media item whose list of children
400     *            will be unsubscribed.
401     * @param callback A callback sent to the media browse service to subscribe.
402     */
403    public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
404        if (callback == null) {
405            throw new IllegalArgumentException("callback is null");
406        }
407        unsubscribeInternal(parentId, callback);
408    }
409
410    /**
411     * Retrieves a specific {@link MediaItem} from the connected service. Not
412     * all services may support this, so falling back to subscribing to the
413     * parent's id should be used when unavailable.
414     *
415     * @param mediaId The id of the item to retrieve.
416     * @param cb The callback to receive the result on.
417     */
418    public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
419        if (TextUtils.isEmpty(mediaId)) {
420            throw new IllegalArgumentException("mediaId is empty.");
421        }
422        if (cb == null) {
423            throw new IllegalArgumentException("cb is null.");
424        }
425        if (mState != CONNECT_STATE_CONNECTED) {
426            Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
427            mHandler.post(new Runnable() {
428                @Override
429                public void run() {
430                    cb.onError(mediaId);
431                }
432            });
433            return;
434        }
435        ResultReceiver receiver = new ResultReceiver(mHandler) {
436            @Override
437            protected void onReceiveResult(int resultCode, Bundle resultData) {
438                if (resultCode != 0 || resultData == null
439                        || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) {
440                    cb.onError(mediaId);
441                    return;
442                }
443                Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM);
444                if (!(item instanceof MediaItem)) {
445                    cb.onError(mediaId);
446                    return;
447                }
448                cb.onItemLoaded((MediaItem)item);
449            }
450        };
451        try {
452            mServiceBinder.getMediaItem(mediaId, receiver);
453        } catch (RemoteException e) {
454            Log.i(TAG, "Remote error getting media item.");
455            mHandler.post(new Runnable() {
456                @Override
457                public void run() {
458                    cb.onError(mediaId);
459                }
460            });
461        }
462    }
463
464    private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) {
465        // Check arguments.
466        if (TextUtils.isEmpty(parentId)) {
467            throw new IllegalArgumentException("parentId is empty.");
468        }
469        if (callback == null) {
470            throw new IllegalArgumentException("callback is null");
471        }
472        // Update or create the subscription.
473        Subscription sub = mSubscriptions.get(parentId);
474        if (sub == null) {
475            sub = new Subscription();
476            mSubscriptions.put(parentId, sub);
477        }
478        sub.putCallback(options, callback);
479
480        // If we are connected, tell the service that we are watching. If we aren't connected,
481        // the service will be told when we connect.
482        if (mState == CONNECT_STATE_CONNECTED) {
483            try {
484                mServiceBinder.addSubscription(parentId, callback.mToken, options,
485                        mServiceCallbacks);
486            } catch (RemoteException ex) {
487                // Process is crashing. We will disconnect, and upon reconnect we will
488                // automatically reregister. So nothing to do here.
489                Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
490            }
491        }
492    }
493
494    private void unsubscribeInternal(String parentId, SubscriptionCallback callback) {
495        // Check arguments.
496        if (TextUtils.isEmpty(parentId)) {
497            throw new IllegalArgumentException("parentId is empty.");
498        }
499
500        Subscription sub = mSubscriptions.get(parentId);
501        if (sub == null) {
502            return;
503        }
504        // Tell the service if necessary.
505        try {
506            if (callback == null) {
507                if (mState == CONNECT_STATE_CONNECTED) {
508                    mServiceBinder.removeSubscription(parentId, null, mServiceCallbacks);
509                }
510            } else {
511                final List<SubscriptionCallback> callbacks = sub.getCallbacks();
512                final List<Bundle> optionsList = sub.getOptionsList();
513                for (int i = callbacks.size() - 1; i >= 0; --i) {
514                    if (callbacks.get(i) == callback) {
515                        if (mState == CONNECT_STATE_CONNECTED) {
516                            mServiceBinder.removeSubscription(
517                                    parentId, callback.mToken, mServiceCallbacks);
518                        }
519                        callbacks.remove(i);
520                        optionsList.remove(i);
521                    }
522                }
523            }
524        } catch (RemoteException ex) {
525            // Process is crashing. We will disconnect, and upon reconnect we will
526            // automatically reregister. So nothing to do here.
527            Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
528        }
529
530        if (sub.isEmpty() || callback == null) {
531            mSubscriptions.remove(parentId);
532        }
533    }
534
535    /**
536     * For debugging.
537     */
538    private static String getStateLabel(int state) {
539        switch (state) {
540            case CONNECT_STATE_DISCONNECTED:
541                return "CONNECT_STATE_DISCONNECTED";
542            case CONNECT_STATE_CONNECTING:
543                return "CONNECT_STATE_CONNECTING";
544            case CONNECT_STATE_CONNECTED:
545                return "CONNECT_STATE_CONNECTED";
546            case CONNECT_STATE_SUSPENDED:
547                return "CONNECT_STATE_SUSPENDED";
548            default:
549                return "UNKNOWN/" + state;
550        }
551    }
552
553    private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
554            final String root, final MediaSession.Token session, final Bundle extra) {
555        mHandler.post(new Runnable() {
556            @Override
557            public void run() {
558                // Check to make sure there hasn't been a disconnect or a different
559                // ServiceConnection.
560                if (!isCurrent(callback, "onConnect")) {
561                    return;
562                }
563                // Don't allow them to call us twice.
564                if (mState != CONNECT_STATE_CONNECTING) {
565                    Log.w(TAG, "onConnect from service while mState="
566                            + getStateLabel(mState) + "... ignoring");
567                    return;
568                }
569                mRootId = root;
570                mMediaSessionToken = session;
571                mExtras = extra;
572                mState = CONNECT_STATE_CONNECTED;
573
574                if (DBG) {
575                    Log.d(TAG, "ServiceCallbacks.onConnect...");
576                    dump();
577                }
578                mCallback.onConnected();
579
580                // we may receive some subscriptions before we are connected, so re-subscribe
581                // everything now
582                for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) {
583                    String id = subscriptionEntry.getKey();
584                    Subscription sub = subscriptionEntry.getValue();
585                    List<SubscriptionCallback> callbackList = sub.getCallbacks();
586                    List<Bundle> optionsList = sub.getOptionsList();
587                    for (int i = 0; i < callbackList.size(); ++i) {
588                        try {
589                            mServiceBinder.addSubscription(id, callbackList.get(i).mToken,
590                                    optionsList.get(i), mServiceCallbacks);
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        Binder mToken;
861
862        public SubscriptionCallback() {
863            mToken = new Binder();
864        }
865
866        /**
867         * Called when the list of children is loaded or updated.
868         *
869         * @param parentId The media id of the parent media item.
870         * @param children The children which were loaded.
871         */
872        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
873        }
874
875        /**
876         * Called when the list of children is loaded or updated.
877         *
878         * @param parentId The media id of the parent media item.
879         * @param children The children which were loaded.
880         * @param options A bundle of service-specific arguments sent to the media
881         *            browse service. The contents of this bundle may affect the
882         *            information returned when browsing.
883         */
884        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
885                @NonNull Bundle options) {
886        }
887
888        /**
889         * Called when the id doesn't exist or other errors in subscribing.
890         * <p>
891         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
892         * called, because some errors may heal themselves.
893         * </p>
894         *
895         * @param parentId The media id of the parent media item whose children could
896         *            not be loaded.
897         */
898        public void onError(@NonNull String parentId) {
899        }
900
901        /**
902         * Called when the id doesn't exist or other errors in subscribing.
903         * <p>
904         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
905         * called, because some errors may heal themselves.
906         * </p>
907         *
908         * @param parentId The media id of the parent media item whose children could
909         *            not be loaded.
910         * @param options A bundle of service-specific arguments sent to the media
911         *            browse service.
912         */
913        public void onError(@NonNull String parentId, @NonNull Bundle options) {
914        }
915    }
916
917    /**
918     * Callback for receiving the result of {@link #getItem}.
919     */
920    public static abstract class ItemCallback {
921        /**
922         * Called when the item has been returned by the browser service.
923         *
924         * @param item The item that was returned or null if it doesn't exist.
925         */
926        public void onItemLoaded(MediaItem item) {
927        }
928
929        /**
930         * Called when the item doesn't exist or there was an error retrieving it.
931         *
932         * @param itemId The media id of the media item which could not be loaded.
933         */
934        public void onError(@NonNull String itemId) {
935        }
936    }
937
938    /**
939     * ServiceConnection to the other app.
940     */
941    private class MediaServiceConnection implements ServiceConnection {
942        @Override
943        public void onServiceConnected(final ComponentName name, final IBinder binder) {
944            postOrRun(new Runnable() {
945                @Override
946                public void run() {
947                    if (DBG) {
948                        Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
949                                + " binder=" + binder);
950                        dump();
951                    }
952
953                    // Make sure we are still the current connection, and that they haven't called
954                    // disconnect().
955                    if (!isCurrent("onServiceConnected")) {
956                        return;
957                    }
958
959                    // Save their binder
960                    mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
961
962                    // We make a new mServiceCallbacks each time we connect so that we can drop
963                    // responses from previous connections.
964                    mServiceCallbacks = getNewServiceCallbacks();
965                    mState = CONNECT_STATE_CONNECTING;
966
967                    // Call connect, which is async. When we get a response from that we will
968                    // say that we're connected.
969                    try {
970                        if (DBG) {
971                            Log.d(TAG, "ServiceCallbacks.onConnect...");
972                            dump();
973                        }
974                        mServiceBinder.connect(mContext.getPackageName(), mRootHints,
975                                mServiceCallbacks);
976                    } catch (RemoteException ex) {
977                        // Connect failed, which isn't good. But the auto-reconnect on the service
978                        // will take over and we will come back. We will also get the
979                        // onServiceDisconnected, which has all the cleanup code. So let that do
980                        // it.
981                        Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
982                        if (DBG) {
983                            Log.d(TAG, "ServiceCallbacks.onConnect...");
984                            dump();
985                        }
986                    }
987                }
988            });
989        }
990
991        @Override
992        public void onServiceDisconnected(final ComponentName name) {
993            postOrRun(new Runnable() {
994                @Override
995                public void run() {
996                    if (DBG) {
997                        Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
998                                + " this=" + this + " mServiceConnection=" + mServiceConnection);
999                        dump();
1000                    }
1001
1002                    // Make sure we are still the current connection, and that they haven't called
1003                    // disconnect().
1004                    if (!isCurrent("onServiceDisconnected")) {
1005                        return;
1006                    }
1007
1008                    // Clear out what we set in onServiceConnected
1009                    mServiceBinder = null;
1010                    mServiceCallbacks = null;
1011
1012                    // And tell the app that it's suspended.
1013                    mState = CONNECT_STATE_SUSPENDED;
1014                    mCallback.onConnectionSuspended();
1015                }
1016            });
1017        }
1018
1019        private void postOrRun(Runnable r) {
1020            if (Thread.currentThread() == mHandler.getLooper().getThread()) {
1021                r.run();
1022            } else {
1023                mHandler.post(r);
1024            }
1025        }
1026
1027        /**
1028         * Return true if this is the current ServiceConnection. Also logs if it's not.
1029         */
1030        private boolean isCurrent(String funcName) {
1031            if (mServiceConnection != this) {
1032                if (mState != CONNECT_STATE_DISCONNECTED) {
1033                    // Check mState, because otherwise this log is noisy.
1034                    Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
1035                            + mServiceConnection + " this=" + this);
1036                }
1037                return false;
1038            }
1039            return true;
1040        }
1041    }
1042
1043    /**
1044     * Callbacks from the service.
1045     */
1046    private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
1047        private WeakReference<MediaBrowser> mMediaBrowser;
1048
1049        public ServiceCallbacks(MediaBrowser mediaBrowser) {
1050            mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
1051        }
1052
1053        /**
1054         * The other side has acknowledged our connection. The parameters to this function
1055         * are the initial data as requested.
1056         */
1057        @Override
1058        public void onConnect(String root, MediaSession.Token session,
1059                final Bundle extras) {
1060            MediaBrowser mediaBrowser = mMediaBrowser.get();
1061            if (mediaBrowser != null) {
1062                mediaBrowser.onServiceConnected(this, root, session, extras);
1063            }
1064        }
1065
1066        /**
1067         * The other side does not like us. Tell the app via onConnectionFailed.
1068         */
1069        @Override
1070        public void onConnectFailed() {
1071            MediaBrowser mediaBrowser = mMediaBrowser.get();
1072            if (mediaBrowser != null) {
1073                mediaBrowser.onConnectionFailed(this);
1074            }
1075        }
1076
1077        @Override
1078        public void onLoadChildren(String parentId, ParceledListSlice list,
1079                final Bundle options) {
1080            MediaBrowser mediaBrowser = mMediaBrowser.get();
1081            if (mediaBrowser != null) {
1082                mediaBrowser.onLoadChildren(this, parentId, list, options);
1083            }
1084        }
1085    }
1086
1087    private static class Subscription {
1088        private final List<SubscriptionCallback> mCallbacks;
1089        private final List<Bundle> mOptionsList;
1090
1091        public Subscription() {
1092            mCallbacks = new ArrayList<>();
1093            mOptionsList = new ArrayList<>();
1094        }
1095
1096        public boolean isEmpty() {
1097            return mCallbacks.isEmpty();
1098        }
1099
1100        public List<Bundle> getOptionsList() {
1101            return mOptionsList;
1102        }
1103
1104        public List<SubscriptionCallback> getCallbacks() {
1105            return mCallbacks;
1106        }
1107
1108        public SubscriptionCallback getCallback(Bundle options) {
1109            for (int i = 0; i < mOptionsList.size(); ++i) {
1110                if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1111                    return mCallbacks.get(i);
1112                }
1113            }
1114            return null;
1115        }
1116
1117        public void putCallback(Bundle options, SubscriptionCallback callback) {
1118            for (int i = 0; i < mOptionsList.size(); ++i) {
1119                if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1120                    mCallbacks.set(i, callback);
1121                    return;
1122                }
1123            }
1124            mCallbacks.add(callback);
1125            mOptionsList.add(options);
1126        }
1127    }
1128}
1129