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