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