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