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