1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media.browse;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.ServiceConnection;
26import android.content.pm.ParceledListSlice;
27import android.media.MediaDescription;
28import android.media.session.MediaController;
29import android.media.session.MediaSession;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.Parcel;
34import android.os.Parcelable;
35import android.os.RemoteException;
36import android.os.ResultReceiver;
37import android.service.media.MediaBrowserService;
38import android.service.media.IMediaBrowserService;
39import android.service.media.IMediaBrowserServiceCallbacks;
40import android.text.TextUtils;
41import android.util.ArrayMap;
42import android.util.Log;
43
44import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
46import java.lang.ref.WeakReference;
47import java.util.Collections;
48import java.util.List;
49
50/**
51 * Browses media content offered by a link MediaBrowserService.
52 * <p>
53 * This object is not thread-safe. All calls should happen on the thread on which the browser
54 * was constructed.
55 * </p>
56 */
57public final class MediaBrowser {
58    private static final String TAG = "MediaBrowser";
59    private static final boolean DBG = false;
60
61    private static final int CONNECT_STATE_DISCONNECTED = 0;
62    private static final int CONNECT_STATE_CONNECTING = 1;
63    private static final int CONNECT_STATE_CONNECTED = 2;
64    private static final int CONNECT_STATE_SUSPENDED = 3;
65
66    private final Context mContext;
67    private final ComponentName mServiceComponent;
68    private final ConnectionCallback mCallback;
69    private final Bundle mRootHints;
70    private final Handler mHandler = new Handler();
71    private final ArrayMap<String,Subscription> mSubscriptions =
72            new ArrayMap<String, MediaBrowser.Subscription>();
73
74    private int mState = CONNECT_STATE_DISCONNECTED;
75    private MediaServiceConnection mServiceConnection;
76    private IMediaBrowserService mServiceBinder;
77    private IMediaBrowserServiceCallbacks mServiceCallbacks;
78    private String mRootId;
79    private MediaSession.Token mMediaSessionToken;
80    private Bundle mExtras;
81
82    /**
83     * Creates a media browser for the specified media browse service.
84     *
85     * @param context The context.
86     * @param serviceComponent The component name of the media browse service.
87     * @param callback The connection callback.
88     * @param rootHints An optional bundle of service-specific arguments to send
89     * to the media browse service when connecting and retrieving the root id
90     * for browsing, or null if none.  The contents of this bundle may affect
91     * the information returned when browsing.
92     */
93    public MediaBrowser(Context context, ComponentName serviceComponent,
94            ConnectionCallback callback, Bundle rootHints) {
95        if (context == null) {
96            throw new IllegalArgumentException("context must not be null");
97        }
98        if (serviceComponent == null) {
99            throw new IllegalArgumentException("service component must not be null");
100        }
101        if (callback == null) {
102            throw new IllegalArgumentException("connection callback must not be null");
103        }
104        mContext = context;
105        mServiceComponent = serviceComponent;
106        mCallback = callback;
107        mRootHints = rootHints;
108    }
109
110    /**
111     * Connects to the media browse service.
112     * <p>
113     * The connection callback specified in the constructor will be invoked
114     * when the connection completes or fails.
115     * </p>
116     */
117    public void connect() {
118        if (mState != CONNECT_STATE_DISCONNECTED) {
119            throw new IllegalStateException("connect() called while not disconnected (state="
120                    + getStateLabel(mState) + ")");
121        }
122        // TODO: remove this extra check.
123        if (DBG) {
124            if (mServiceConnection != null) {
125                throw new RuntimeException("mServiceConnection should be null. Instead it is "
126                        + mServiceConnection);
127            }
128        }
129        if (mServiceBinder != null) {
130            throw new RuntimeException("mServiceBinder should be null. Instead it is "
131                    + mServiceBinder);
132        }
133        if (mServiceCallbacks != null) {
134            throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
135                    + mServiceCallbacks);
136        }
137
138        mState = CONNECT_STATE_CONNECTING;
139
140        final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
141        intent.setComponent(mServiceComponent);
142
143        final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
144
145        boolean bound = false;
146        try {
147            bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
148        } catch (Exception ex) {
149            Log.e(TAG, "Failed binding to service " + mServiceComponent);
150        }
151
152        if (!bound) {
153            // Tell them that it didn't work.  We are already on the main thread,
154            // but we don't want to do callbacks inside of connect().  So post it,
155            // and then check that we are on the same ServiceConnection.  We know
156            // we won't also get an onServiceConnected or onServiceDisconnected,
157            // so we won't be doing double callbacks.
158            mHandler.post(new Runnable() {
159                @Override
160                public void run() {
161                    // Ensure that nobody else came in or tried to connect again.
162                    if (thisConnection == mServiceConnection) {
163                        forceCloseConnection();
164                        mCallback.onConnectionFailed();
165                    }
166                }
167            });
168        }
169
170        if (DBG) {
171            Log.d(TAG, "connect...");
172            dump();
173        }
174    }
175
176    /**
177     * Disconnects from the media browse service.
178     * After this, no more callbacks will be received.
179     */
180    public void disconnect() {
181        // It's ok to call this any state, because allowing this lets apps not have
182        // to check isConnected() unnecessarily.  They won't appreciate the extra
183        // assertions for this.  We do everything we can here to go back to a sane state.
184        if (mServiceCallbacks != null) {
185            try {
186                mServiceBinder.disconnect(mServiceCallbacks);
187            } catch (RemoteException ex) {
188                // We are disconnecting anyway.  Log, just for posterity but it's not
189                // a big problem.
190                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
191            }
192        }
193        forceCloseConnection();
194
195        if (DBG) {
196            Log.d(TAG, "disconnect...");
197            dump();
198        }
199    }
200
201    /**
202     * Null out the variables and unbind from the service.  This doesn't include
203     * calling disconnect on the service, because we only try to do that in the
204     * clean shutdown cases.
205     * <p>
206     * Everywhere that calls this EXCEPT for disconnect() should follow it with
207     * a call to mCallback.onConnectionFailed().  Disconnect doesn't do that callback
208     * for a clean shutdown, but everywhere else is a dirty shutdown and should
209     * notify the app.
210     */
211    private void forceCloseConnection() {
212        if (mServiceConnection != null) {
213            mContext.unbindService(mServiceConnection);
214        }
215        mState = CONNECT_STATE_DISCONNECTED;
216        mServiceConnection = null;
217        mServiceBinder = null;
218        mServiceCallbacks = null;
219        mRootId = null;
220        mMediaSessionToken = null;
221    }
222
223    /**
224     * Returns whether the browser is connected to the service.
225     */
226    public boolean isConnected() {
227        return mState == CONNECT_STATE_CONNECTED;
228    }
229
230    /**
231     * Gets the service component that the media browser is connected to.
232     */
233    public @NonNull ComponentName getServiceComponent() {
234        if (!isConnected()) {
235            throw new IllegalStateException("getServiceComponent() called while not connected" +
236                    " (state=" + mState + ")");
237        }
238        return mServiceComponent;
239    }
240
241    /**
242     * Gets the root id.
243     * <p>
244     * Note that the root id may become invalid or change when when the
245     * browser is disconnected.
246     * </p>
247     *
248     * @throws IllegalStateException if not connected.
249     */
250    public @NonNull String getRoot() {
251        if (!isConnected()) {
252            throw new IllegalStateException("getSessionToken() called while not connected (state="
253                    + getStateLabel(mState) + ")");
254        }
255        return mRootId;
256    }
257
258    /**
259     * Gets any extras for the media service.
260     *
261     * @throws IllegalStateException if not connected.
262     */
263    public @Nullable Bundle getExtras() {
264        if (!isConnected()) {
265            throw new IllegalStateException("getExtras() called while not connected (state="
266                    + getStateLabel(mState) + ")");
267        }
268        return mExtras;
269    }
270
271    /**
272     * Gets the media session token associated with the media browser.
273     * <p>
274     * Note that the session token may become invalid or change when when the
275     * browser is disconnected.
276     * </p>
277     *
278     * @return The session token for the browser, never null.
279     *
280     * @throws IllegalStateException if not connected.
281     */
282     public @NonNull MediaSession.Token getSessionToken() {
283        if (!isConnected()) {
284            throw new IllegalStateException("getSessionToken() called while not connected (state="
285                    + mState + ")");
286        }
287        return mMediaSessionToken;
288    }
289
290    /**
291     * Queries for information about the media items that are contained within
292     * the specified id and subscribes to receive updates when they change.
293     * <p>
294     * The list of subscriptions is maintained even when not connected and is
295     * restored after reconnection. It is ok to subscribe while not connected
296     * but the results will not be returned until the connection completes.
297     * </p>
298     * <p>
299     * If the id is already subscribed with a different callback then the new
300     * callback will replace the previous one and the child data will be
301     * reloaded.
302     * </p>
303     *
304     * @param parentId The id of the parent media item whose list of children
305     *            will be subscribed.
306     * @param callback The callback to receive the list of children.
307     */
308    public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
309        // Check arguments.
310        if (parentId == null) {
311            throw new IllegalArgumentException("parentId is null");
312        }
313        if (callback == null) {
314            throw new IllegalArgumentException("callback is null");
315        }
316
317        // Update or create the subscription.
318        Subscription sub = mSubscriptions.get(parentId);
319        boolean newSubscription = sub == null;
320        if (newSubscription) {
321            sub = new Subscription(parentId);
322            mSubscriptions.put(parentId, sub);
323        }
324        sub.callback = callback;
325
326        // If we are connected, tell the service that we are watching.  If we aren't
327        // connected, the service will be told when we connect.
328        if (mState == CONNECT_STATE_CONNECTED) {
329            try {
330                mServiceBinder.addSubscription(parentId, mServiceCallbacks);
331            } catch (RemoteException ex) {
332                // Process is crashing.  We will disconnect, and upon reconnect we will
333                // automatically reregister. So nothing to do here.
334                Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
335            }
336        }
337    }
338
339    /**
340     * Unsubscribes for changes to the children of the specified media id.
341     * <p>
342     * The query callback will no longer be invoked for results associated with
343     * this id once this method returns.
344     * </p>
345     *
346     * @param parentId The id of the parent media item whose list of children
347     * will be unsubscribed.
348     */
349    public void unsubscribe(@NonNull String parentId) {
350        // Check arguments.
351        if (TextUtils.isEmpty(parentId)) {
352            throw new IllegalArgumentException("parentId is empty.");
353        }
354
355        // Remove from our list.
356        final Subscription sub = mSubscriptions.remove(parentId);
357
358        // Tell the service if necessary.
359        if (mState == CONNECT_STATE_CONNECTED && sub != null) {
360            try {
361                mServiceBinder.removeSubscription(parentId, mServiceCallbacks);
362            } catch (RemoteException ex) {
363                // Process is crashing.  We will disconnect, and upon reconnect we will
364                // automatically reregister. So nothing to do here.
365                Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
366            }
367        }
368    }
369
370    /**
371     * Retrieves a specific {@link MediaItem} from the connected service. Not
372     * all services may support this, so falling back to subscribing to the
373     * parent's id should be used when unavailable.
374     *
375     * @param mediaId The id of the item to retrieve.
376     * @param cb The callback to receive the result on.
377     */
378    public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
379        if (TextUtils.isEmpty(mediaId)) {
380            throw new IllegalArgumentException("mediaId is empty.");
381        }
382        if (cb == null) {
383            throw new IllegalArgumentException("cb is null.");
384        }
385        if (mState != CONNECT_STATE_CONNECTED) {
386            Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
387            mHandler.post(new Runnable() {
388                @Override
389                public void run() {
390                    cb.onError(mediaId);
391                }
392            });
393            return;
394        }
395        ResultReceiver receiver = new ResultReceiver(mHandler) {
396            @Override
397            protected void onReceiveResult(int resultCode, Bundle resultData) {
398                if (resultCode != 0 || resultData == null
399                        || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) {
400                    cb.onError(mediaId);
401                    return;
402                }
403                Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM);
404                if (!(item instanceof MediaItem)) {
405                    cb.onError(mediaId);
406                    return;
407                }
408                cb.onItemLoaded((MediaItem)item);
409            }
410        };
411        try {
412            mServiceBinder.getMediaItem(mediaId, receiver);
413        } catch (RemoteException e) {
414            Log.i(TAG, "Remote error getting media item.");
415            mHandler.post(new Runnable() {
416                @Override
417                public void run() {
418                    cb.onError(mediaId);
419                }
420            });
421        }
422    }
423
424    /**
425     * For debugging.
426     */
427    private static String getStateLabel(int state) {
428        switch (state) {
429            case CONNECT_STATE_DISCONNECTED:
430                return "CONNECT_STATE_DISCONNECTED";
431            case CONNECT_STATE_CONNECTING:
432                return "CONNECT_STATE_CONNECTING";
433            case CONNECT_STATE_CONNECTED:
434                return "CONNECT_STATE_CONNECTED";
435            case CONNECT_STATE_SUSPENDED:
436                return "CONNECT_STATE_SUSPENDED";
437            default:
438                return "UNKNOWN/" + state;
439        }
440    }
441
442    private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
443            final String root, final MediaSession.Token session, final Bundle extra) {
444        mHandler.post(new Runnable() {
445            @Override
446            public void run() {
447                // Check to make sure there hasn't been a disconnect or a different
448                // ServiceConnection.
449                if (!isCurrent(callback, "onConnect")) {
450                    return;
451                }
452                // Don't allow them to call us twice.
453                if (mState != CONNECT_STATE_CONNECTING) {
454                    Log.w(TAG, "onConnect from service while mState="
455                            + getStateLabel(mState) + "... ignoring");
456                    return;
457                }
458                mRootId = root;
459                mMediaSessionToken = session;
460                mExtras = extra;
461                mState = CONNECT_STATE_CONNECTED;
462
463                if (DBG) {
464                    Log.d(TAG, "ServiceCallbacks.onConnect...");
465                    dump();
466                }
467                mCallback.onConnected();
468
469                // we may receive some subscriptions before we are connected, so re-subscribe
470                // everything now
471                for (String id : mSubscriptions.keySet()) {
472                    try {
473                        mServiceBinder.addSubscription(id, mServiceCallbacks);
474                    } catch (RemoteException ex) {
475                        // Process is crashing.  We will disconnect, and upon reconnect we will
476                        // automatically reregister. So nothing to do here.
477                        Log.d(TAG, "addSubscription failed with RemoteException parentId=" + id);
478                    }
479                }
480            }
481        });
482    }
483
484    private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
485        mHandler.post(new Runnable() {
486            @Override
487            public void run() {
488                Log.e(TAG, "onConnectFailed for " + mServiceComponent);
489
490                // Check to make sure there hasn't been a disconnect or a different
491                // ServiceConnection.
492                if (!isCurrent(callback, "onConnectFailed")) {
493                    return;
494                }
495                // Don't allow them to call us twice.
496                if (mState != CONNECT_STATE_CONNECTING) {
497                    Log.w(TAG, "onConnect from service while mState="
498                            + getStateLabel(mState) + "... ignoring");
499                    return;
500                }
501
502                // Clean up
503                forceCloseConnection();
504
505                // Tell the app.
506                mCallback.onConnectionFailed();
507            }
508        });
509    }
510
511    private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback,
512            final String parentId, final ParceledListSlice list) {
513        mHandler.post(new Runnable() {
514            @Override
515            public void run() {
516                // Check that there hasn't been a disconnect or a different
517                // ServiceConnection.
518                if (!isCurrent(callback, "onLoadChildren")) {
519                    return;
520                }
521
522                List<MediaItem> data = list.getList();
523                if (DBG) {
524                    Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
525                }
526                if (data == null) {
527                    data = Collections.emptyList();
528                }
529
530                // Check that the subscription is still subscribed.
531                final Subscription subscription = mSubscriptions.get(parentId);
532                if (subscription == null) {
533                    if (DBG) {
534                        Log.d(TAG, "onLoadChildren for id that isn't subscribed id="
535                                + parentId);
536                    }
537                    return;
538                }
539
540                // Tell the app.
541                subscription.callback.onChildrenLoaded(parentId, data);
542            }
543        });
544    }
545
546    /**
547     * Return true if {@code callback} is the current ServiceCallbacks.  Also logs if it's not.
548     */
549    private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
550        if (mServiceCallbacks != callback) {
551            if (mState != CONNECT_STATE_DISCONNECTED) {
552                Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
553                        + mServiceCallbacks + " this=" + this);
554            }
555            return false;
556        }
557        return true;
558    }
559
560    private ServiceCallbacks getNewServiceCallbacks() {
561        return new ServiceCallbacks(this);
562    }
563
564    /**
565     * Log internal state.
566     * @hide
567     */
568    void dump() {
569        Log.d(TAG, "MediaBrowser...");
570        Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
571        Log.d(TAG, "  mCallback=" + mCallback);
572        Log.d(TAG, "  mRootHints=" + mRootHints);
573        Log.d(TAG, "  mState=" + getStateLabel(mState));
574        Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
575        Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
576        Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
577        Log.d(TAG, "  mRootId=" + mRootId);
578        Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
579    }
580
581    public static class MediaItem implements Parcelable {
582        private final int mFlags;
583        private final MediaDescription mDescription;
584
585        /** @hide */
586        @Retention(RetentionPolicy.SOURCE)
587        @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
588        public @interface Flags { }
589
590        /**
591         * Flag: Indicates that the item has children of its own.
592         */
593        public static final int FLAG_BROWSABLE = 1 << 0;
594
595        /**
596         * Flag: Indicates that the item is playable.
597         * <p>
598         * The id of this item may be passed to
599         * {@link MediaController.TransportControls#playFromMediaId(String, Bundle)}
600         * to start playing it.
601         * </p>
602         */
603        public static final int FLAG_PLAYABLE = 1 << 1;
604
605        /**
606         * Create a new MediaItem for use in browsing media.
607         * @param description The description of the media, which must include a
608         *            media id.
609         * @param flags The flags for this item.
610         */
611        public MediaItem(@NonNull MediaDescription description, @Flags int flags) {
612            if (description == null) {
613                throw new IllegalArgumentException("description cannot be null");
614            }
615            if (TextUtils.isEmpty(description.getMediaId())) {
616                throw new IllegalArgumentException("description must have a non-empty media id");
617            }
618            mFlags = flags;
619            mDescription = description;
620        }
621
622        /**
623         * Private constructor.
624         */
625        private MediaItem(Parcel in) {
626            mFlags = in.readInt();
627            mDescription = MediaDescription.CREATOR.createFromParcel(in);
628        }
629
630        @Override
631        public int describeContents() {
632            return 0;
633        }
634
635        @Override
636        public void writeToParcel(Parcel out, int flags) {
637            out.writeInt(mFlags);
638            mDescription.writeToParcel(out, flags);
639        }
640
641        @Override
642        public String toString() {
643            final StringBuilder sb = new StringBuilder("MediaItem{");
644            sb.append("mFlags=").append(mFlags);
645            sb.append(", mDescription=").append(mDescription);
646            sb.append('}');
647            return sb.toString();
648        }
649
650        public static final Parcelable.Creator<MediaItem> CREATOR =
651                new Parcelable.Creator<MediaItem>() {
652                    @Override
653                    public MediaItem createFromParcel(Parcel in) {
654                        return new MediaItem(in);
655                    }
656
657                    @Override
658                    public MediaItem[] newArray(int size) {
659                        return new MediaItem[size];
660                    }
661                };
662
663        /**
664         * Gets the flags of the item.
665         */
666        public @Flags int getFlags() {
667            return mFlags;
668        }
669
670        /**
671         * Returns whether this item is browsable.
672         * @see #FLAG_BROWSABLE
673         */
674        public boolean isBrowsable() {
675            return (mFlags & FLAG_BROWSABLE) != 0;
676        }
677
678        /**
679         * Returns whether this item is playable.
680         * @see #FLAG_PLAYABLE
681         */
682        public boolean isPlayable() {
683            return (mFlags & FLAG_PLAYABLE) != 0;
684        }
685
686        /**
687         * Returns the description of the media.
688         */
689        public @NonNull MediaDescription getDescription() {
690            return mDescription;
691        }
692
693        /**
694         * Returns the media id for this item.
695         */
696        public @NonNull String getMediaId() {
697            return mDescription.getMediaId();
698        }
699    }
700
701
702    /**
703     * Callbacks for connection related events.
704     */
705    public static class ConnectionCallback {
706        /**
707         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
708         */
709        public void onConnected() {
710        }
711
712        /**
713         * Invoked when the client is disconnected from the media browser.
714         */
715        public void onConnectionSuspended() {
716        }
717
718        /**
719         * Invoked when the connection to the media browser failed.
720         */
721        public void onConnectionFailed() {
722        }
723    }
724
725    /**
726     * Callbacks for subscription related events.
727     */
728    public static abstract class SubscriptionCallback {
729        /**
730         * Called when the list of children is loaded or updated.
731         *
732         * @param parentId The media id of the parent media item.
733         * @param children The children which were loaded.
734         */
735        public void onChildrenLoaded(@NonNull String parentId,
736                                     @NonNull List<MediaItem> children) {
737        }
738
739        /**
740         * Called when the id doesn't exist or other errors in subscribing.
741         * <p>
742         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
743         * called, because some errors may heal themselves.
744         * </p>
745         *
746         * @param parentId The media id of the parent media item whose children could
747         * not be loaded.
748         */
749        public void onError(@NonNull String parentId) {
750        }
751    }
752
753    /**
754     * Callback for receiving the result of {@link #getItem}.
755     */
756    public static abstract class ItemCallback {
757        /**
758         * Called when the item has been returned by the browser service.
759         *
760         * @param item The item that was returned or null if it doesn't exist.
761         */
762        public void onItemLoaded(MediaItem item) {
763        }
764
765        /**
766         * Called when the item doesn't exist or there was an error retrieving it.
767         *
768         * @param itemId The media id of the media item which could not be loaded.
769         */
770        public void onError(@NonNull String itemId) {
771        }
772    }
773
774    /**
775     * ServiceConnection to the other app.
776     */
777    private class MediaServiceConnection implements ServiceConnection {
778        @Override
779        public void onServiceConnected(ComponentName name, IBinder binder) {
780            if (DBG) {
781                Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
782                        + " binder=" + binder);
783                dump();
784            }
785
786            // Make sure we are still the current connection, and that they haven't called
787            // disconnect().
788            if (!isCurrent("onServiceConnected")) {
789                return;
790            }
791
792            // Save their binder
793            mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
794
795            // We make a new mServiceCallbacks each time we connect so that we can drop
796            // responses from previous connections.
797            mServiceCallbacks = getNewServiceCallbacks();
798            mState = CONNECT_STATE_CONNECTING;
799
800            // Call connect, which is async. When we get a response from that we will
801            // say that we're connected.
802            try {
803                if (DBG) {
804                    Log.d(TAG, "ServiceCallbacks.onConnect...");
805                    dump();
806                }
807                mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
808            } catch (RemoteException ex) {
809                // Connect failed, which isn't good. But the auto-reconnect on the service
810                // will take over and we will come back.  We will also get the
811                // onServiceDisconnected, which has all the cleanup code.  So let that do it.
812                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
813                if (DBG) {
814                    Log.d(TAG, "ServiceCallbacks.onConnect...");
815                    dump();
816                }
817            }
818        }
819
820        @Override
821        public void onServiceDisconnected(ComponentName name) {
822            if (DBG) {
823                Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
824                        + " this=" + this + " mServiceConnection=" + mServiceConnection);
825                dump();
826            }
827
828            // Make sure we are still the current connection, and that they haven't called
829            // disconnect().
830            if (!isCurrent("onServiceDisconnected")) {
831                return;
832            }
833
834            // Clear out what we set in onServiceConnected
835            mServiceBinder = null;
836            mServiceCallbacks = null;
837
838            // And tell the app that it's suspended.
839            mState = CONNECT_STATE_SUSPENDED;
840            mCallback.onConnectionSuspended();
841        }
842
843        /**
844         * Return true if this is the current ServiceConnection.  Also logs if it's not.
845         */
846        private boolean isCurrent(String funcName) {
847            if (mServiceConnection != this) {
848                if (mState != CONNECT_STATE_DISCONNECTED) {
849                    // Check mState, because otherwise this log is noisy.
850                    Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
851                            + mServiceConnection + " this=" + this);
852                }
853                return false;
854            }
855            return true;
856        }
857    }
858
859    /**
860     * Callbacks from the service.
861     */
862    private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
863        private WeakReference<MediaBrowser> mMediaBrowser;
864
865        public ServiceCallbacks(MediaBrowser mediaBrowser) {
866            mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
867        }
868
869        /**
870         * The other side has acknowledged our connection.  The parameters to this function
871         * are the initial data as requested.
872         */
873        @Override
874        public void onConnect(final String root, final MediaSession.Token session,
875                final Bundle extras) {
876            MediaBrowser mediaBrowser = mMediaBrowser.get();
877            if (mediaBrowser != null) {
878                mediaBrowser.onServiceConnected(this, root, session, extras);
879            }
880        }
881
882        /**
883         * The other side does not like us.  Tell the app via onConnectionFailed.
884         */
885        @Override
886        public void onConnectFailed() {
887            MediaBrowser mediaBrowser = mMediaBrowser.get();
888            if (mediaBrowser != null) {
889                mediaBrowser.onConnectionFailed(this);
890            }
891        }
892
893        @Override
894        public void onLoadChildren(final String parentId, final ParceledListSlice list) {
895            MediaBrowser mediaBrowser = mMediaBrowser.get();
896            if (mediaBrowser != null) {
897                mediaBrowser.onLoadChildren(this, parentId, list);
898            }
899        }
900    }
901
902    private static class Subscription {
903        final String id;
904        SubscriptionCallback callback;
905
906        Subscription(String id) {
907            this.id = id;
908        }
909    }
910}
911