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