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