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