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