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