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