MediaBrowser.java revision 5f3e1f2a2dfaa4d1abdda4d0cd7871aea82ffcbd
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 root Uri.
229     * <p>
230     * Note that the root uri may become invalid or change when when the
231     * browser is disconnected.
232     * </p>
233     *
234     * @throws IllegalStateException if not connected.
235     */
236    public @NonNull Uri getRoot() {
237        if (mState != CONNECT_STATE_CONNECTED) {
238            throw new IllegalStateException("getSessionToken() called while not connected (state="
239                    + getStateLabel(mState) + ")");
240        }
241        return mRootUri;
242    }
243
244    /**
245     * Gets any extras for the media service.
246     *
247     * @throws IllegalStateException if not connected.
248     */
249    public @Nullable Bundle getExtras() {
250        if (mState != CONNECT_STATE_CONNECTED) {
251            throw new IllegalStateException("getExtras() called while not connected (state="
252                    + getStateLabel(mState) + ")");
253        }
254        return mExtras;
255    }
256
257    /**
258     * Gets the media session token associated with the media browser.
259     * <p>
260     * Note that the session token may become invalid or change when when the
261     * browser is disconnected.
262     * </p>
263     *
264     * @return The session token for the browser, never null.
265     *
266     * @throws IllegalStateException if not connected.
267     */
268     public @NonNull MediaSession.Token getSessionToken() {
269        if (mState != CONNECT_STATE_CONNECTED) {
270            throw new IllegalStateException("getSessionToken() called while not connected (state="
271                    + mState + ")");
272        }
273        return mMediaSessionToken;
274    }
275
276    /**
277     * Queries for information about the media items that are contained within
278     * the specified Uri and subscribes to receive updates when they change.
279     * <p>
280     * The list of subscriptions is maintained even when not connected and is
281     * restored after reconnection.  It is ok to subscribe while not connected
282     * but the results will not be returned until the connection completes.
283     * </p><p>
284     * If the uri is already subscribed with a different callback then the new
285     * callback will replace the previous one.
286     * </p>
287     *
288     * @param parentUri The uri of the parent media item whose list of children
289     * will be subscribed.
290     * @param callback The callback to receive the list of children.
291     */
292    public void subscribe(@NonNull Uri parentUri, @NonNull SubscriptionCallback callback) {
293        // Check arguments.
294        if (parentUri == null) {
295            throw new IllegalArgumentException("parentUri is null");
296        }
297        if (callback == null) {
298            throw new IllegalArgumentException("callback is null");
299        }
300
301        // Update or create the subscription.
302        Subscription sub = mSubscriptions.get(parentUri);
303        boolean newSubscription = sub == null;
304        if (newSubscription) {
305            sub = new Subscription(parentUri);
306            mSubscriptions.put(parentUri, sub);
307        }
308        sub.callback = callback;
309
310        // If we are connected, tell the service that we are watching.  If we aren't
311        // connected, the service will be told when we connect.
312        if (mState == CONNECT_STATE_CONNECTED && newSubscription) {
313            try {
314                mServiceBinder.addSubscription(parentUri, mServiceCallbacks);
315            } catch (RemoteException ex) {
316                // Process is crashing.  We will disconnect, and upon reconnect we will
317                // automatically reregister. So nothing to do here.
318                Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + parentUri);
319            }
320        }
321    }
322
323    /**
324     * Unsubscribes for changes to the children of the specified Uri.
325     * <p>
326     * The query callback will no longer be invoked for results associated with
327     * this Uri once this method returns.
328     * </p>
329     *
330     * @param parentUri The uri of the parent media item whose list of children
331     * will be unsubscribed.
332     */
333    public void unsubscribe(@NonNull Uri parentUri) {
334        // Check arguments.
335        if (parentUri == null) {
336            throw new IllegalArgumentException("parentUri is null");
337        }
338
339        // Remove from our list.
340        final Subscription sub = mSubscriptions.remove(parentUri);
341
342        // Tell the service if necessary.
343        if (mState == CONNECT_STATE_CONNECTED && sub != null) {
344            try {
345                mServiceBinder.removeSubscription(parentUri, mServiceCallbacks);
346            } catch (RemoteException ex) {
347                // Process is crashing.  We will disconnect, and upon reconnect we will
348                // automatically reregister. So nothing to do here.
349                Log.d(TAG, "removeSubscription failed with RemoteException parentUri=" + parentUri);
350            }
351        }
352    }
353
354    /**
355     * Loads the icon of a media item.
356     *
357     * @param uri The uri of the Icon.
358     * @param width The preferred width of the icon in dp.
359     * @param height The preferred width of the icon in dp.
360     * @param callback The callback to receive the icon.
361     */
362    public void loadIcon(final @NonNull Uri uri, final int width, final int height,
363            final @NonNull IconCallback callback) {
364        if (uri == null) {
365            throw new IllegalArgumentException("Icon uri cannot be null");
366        }
367        if (callback == null) {
368            throw new IllegalArgumentException("Icon callback cannot be null");
369        }
370        mHandler.post(new Runnable() {
371            @Override
372            public void run() {
373                for (int i = 0; i < mIconRequests.size(); i++) {
374                    IconRequest existingRequest = mIconRequests.valueAt(i);
375                    if (existingRequest.isSameRequest(uri, width, height)) {
376                        existingRequest.addCallback(callback);
377                        return;
378                    }
379                }
380                final int seq = mNextSeq++;
381                IconRequest request = new IconRequest(seq, uri, width, height);
382                request.addCallback(callback);
383                mIconRequests.put(seq, request);
384                if (mState == CONNECT_STATE_CONNECTED) {
385                    try {
386                        mServiceBinder.loadIcon(seq, uri, width, height, mServiceCallbacks);
387                    } catch (RemoteException e) {
388                        // Process is crashing.  We will disconnect, and upon reconnect we will
389                        // automatically reload the icons. So nothing to do here.
390                        Log.d(TAG, "loadIcon failed with RemoteException uri=" + uri);
391                    }
392                }
393            }
394        });
395    }
396
397    /**
398     * For debugging.
399     */
400    private static String getStateLabel(int state) {
401        switch (state) {
402            case CONNECT_STATE_DISCONNECTED:
403                return "CONNECT_STATE_DISCONNECTED";
404            case CONNECT_STATE_CONNECTING:
405                return "CONNECT_STATE_CONNECTING";
406            case CONNECT_STATE_CONNECTED:
407                return "CONNECT_STATE_CONNECTED";
408            case CONNECT_STATE_SUSPENDED:
409                return "CONNECT_STATE_SUSPENDED";
410            default:
411                return "UNKNOWN/" + state;
412        }
413    }
414
415    private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
416            final Uri root, final MediaSession.Token session, final Bundle extra) {
417        mHandler.post(new Runnable() {
418            @Override
419            public void run() {
420                // Check to make sure there hasn't been a disconnect or a different
421                // ServiceConnection.
422                if (!isCurrent(callback, "onConnect")) {
423                    return;
424                }
425                // Don't allow them to call us twice.
426                if (mState != CONNECT_STATE_CONNECTING) {
427                    Log.w(TAG, "onConnect from service while mState="
428                            + getStateLabel(mState) + "... ignoring");
429                    return;
430                }
431                mRootUri = root;
432                mMediaSessionToken = session;
433                mExtras = extra;
434                mState = CONNECT_STATE_CONNECTED;
435
436                if (DBG) {
437                    Log.d(TAG, "ServiceCallbacks.onConnect...");
438                    dump();
439                }
440                mCallback.onConnected();
441
442                // we may receive some subscriptions before we are connected, so re-subscribe
443                // everything now
444                for (Uri uri : mSubscriptions.keySet()) {
445                    try {
446                        mServiceBinder.addSubscription(uri, mServiceCallbacks);
447                    } catch (RemoteException ex) {
448                        // Process is crashing.  We will disconnect, and upon reconnect we will
449                        // automatically reregister. So nothing to do here.
450                        Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + uri);
451                    }
452                }
453
454                for (int i = 0; i < mIconRequests.size(); i++) {
455                    IconRequest request = mIconRequests.valueAt(i);
456                    try {
457                        mServiceBinder.loadIcon(request.mSeq, request.mUri,
458                                request.mWidth, request.mHeight, mServiceCallbacks);
459                    } catch (RemoteException e) {
460                        // Process is crashing.  We will disconnect, and upon reconnect we will
461                        // automatically reload. So nothing to do here.
462                        Log.d(TAG, "loadIcon failed with RemoteException request=" + request);
463                    }
464                }
465            }
466        });
467    }
468
469    private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
470        mHandler.post(new Runnable() {
471            @Override
472            public void run() {
473                Log.e(TAG, "onConnectFailed for " + mServiceComponent);
474
475                // Check to make sure there hasn't been a disconnect or a different
476                // ServiceConnection.
477                if (!isCurrent(callback, "onConnectFailed")) {
478                    return;
479                }
480                // Don't allow them to call us twice.
481                if (mState != CONNECT_STATE_CONNECTING) {
482                    Log.w(TAG, "onConnect from service while mState="
483                            + getStateLabel(mState) + "... ignoring");
484                    return;
485                }
486
487                // Clean up
488                forceCloseConnection();
489
490                // Tell the app.
491                mCallback.onConnectionFailed();
492            }
493        });
494    }
495
496    private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, final Uri uri,
497            final ParceledListSlice list) {
498        mHandler.post(new Runnable() {
499            @Override
500            public void run() {
501                // Check that there hasn't been a disconnect or a different
502                // ServiceConnection.
503                if (!isCurrent(callback, "onLoadChildren")) {
504                    return;
505                }
506
507                List<MediaBrowserItem> data = list.getList();
508                if (DBG) {
509                    Log.d(TAG, "onLoadChildren for " + mServiceComponent + " uri=" + uri);
510                }
511                if (data == null) {
512                    data = Collections.emptyList();
513                }
514
515                // Check that the subscription is still subscribed.
516                final Subscription subscription = mSubscriptions.get(uri);
517                if (subscription == null) {
518                    if (DBG) {
519                        Log.d(TAG, "onLoadChildren for uri that isn't subscribed uri="
520                                + uri);
521                    }
522                    return;
523                }
524
525                // Tell the app.
526                subscription.callback.onChildrenLoaded(uri, data);
527            }
528        });
529    }
530
531    private final void onLoadIcon(final IMediaBrowserServiceCallbacks callback,
532            final int seqNum, final Bitmap bitmap) {
533        mHandler.post(new Runnable() {
534            @Override
535            public void run() {
536                // Check that there hasn't been a disconnect or a different
537                // ServiceConnection.
538                if (!isCurrent(callback, "onLoadIcon")) {
539                    return;
540                }
541
542                IconRequest request = mIconRequests.get(seqNum);
543                if (request == null) {
544                    Log.d(TAG, "onLoadIcon called for seqNum=" + seqNum + " request="
545                            + request + " but the request is not registered");
546                    return;
547                }
548                mIconRequests.delete(seqNum);
549                for (IconCallback IconCallback : request.getCallbacks()) {
550                    IconCallback.onIconLoaded(request.mUri, bitmap);
551                }
552            }
553        });
554    }
555
556
557    /**
558     * Return true if {@code callback} is the current ServiceCallbacks.  Also logs if it's not.
559     */
560    private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
561        if (mServiceCallbacks != callback) {
562            if (mState != CONNECT_STATE_DISCONNECTED) {
563                Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
564                        + mServiceCallbacks + " this=" + this);
565            }
566            return false;
567        }
568        return true;
569    }
570
571    private ServiceCallbacks getNewServiceCallbacks() {
572        return new ServiceCallbacks(this);
573    }
574
575    /**
576     * Log internal state.
577     * @hide
578     */
579    void dump() {
580        Log.d(TAG, "MediaBrowser...");
581        Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
582        Log.d(TAG, "  mCallback=" + mCallback);
583        Log.d(TAG, "  mRootHints=" + mRootHints);
584        Log.d(TAG, "  mState=" + getStateLabel(mState));
585        Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
586        Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
587        Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
588        Log.d(TAG, "  mRootUri=" + mRootUri);
589        Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
590    }
591
592
593    /**
594     * Callbacks for connection related events.
595     */
596    public static class ConnectionCallback {
597        /**
598         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
599         */
600        public void onConnected() {
601        }
602
603        /**
604         * Invoked when the client is disconnected from the media browser.
605         */
606        public void onConnectionSuspended() {
607        }
608
609        /**
610         * Invoked when the connection to the media browser failed.
611         */
612        public void onConnectionFailed() {
613        }
614    }
615
616    /**
617     * Callbacks for subscription related events.
618     */
619    public static abstract class SubscriptionCallback {
620        /**
621         * Called when the list of children is loaded or updated.
622         */
623        public void onChildrenLoaded(@NonNull Uri parentUri,
624                                     @NonNull List<MediaBrowserItem> children) {
625        }
626
627        /**
628         * Called when the Uri doesn't exist or other errors in subscribing.
629         * <p>
630         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
631         * called, because some errors may heal themselves.
632         * </p>
633         */
634        public void onError(@NonNull Uri uri) {
635        }
636    }
637
638    /**
639     * Callbacks for icon loading.
640     */
641    public static abstract class IconCallback {
642        /**
643         * Called when the icon is loaded.
644         */
645        public void onIconLoaded(@NonNull Uri uri, @NonNull Bitmap bitmap) {
646        }
647
648        /**
649         * Called when the Uri doesn’t exist or the bitmap cannot be loaded.
650         */
651        public void onError(@NonNull Uri uri) {
652        }
653    }
654
655    private static class IconRequest {
656        final int mSeq;
657        final Uri mUri;
658        final int mWidth;
659        final int mHeight;
660        final List<IconCallback> mCallbacks;
661
662        /**
663         * Constructs an icon request.
664         * @param seq The unique sequence number assigned to the request by the media browser.
665         * @param uri The Uri for the icon.
666         * @param width The width for the icon.
667         * @param height The height for the icon.
668         */
669        IconRequest(int seq, @NonNull Uri uri, int width, int height) {
670            if (uri == null) {
671                throw new IllegalArgumentException("Icon uri cannot be null");
672            }
673            this.mSeq = seq;
674            this.mUri = uri;
675            this.mWidth = width;
676            this.mHeight = height;
677            mCallbacks = new ArrayList<IconCallback>();
678        }
679
680        /**
681         * Adds a callback to the icon request.
682         * If the callback already exists, it will not be added again.
683         */
684        public void addCallback(@NonNull IconCallback callback) {
685            if (callback == null) {
686                throw new IllegalArgumentException("callback cannot be null in IconRequest");
687            }
688            if (!mCallbacks.contains(callback)) {
689                mCallbacks.add(callback);
690            }
691        }
692
693        /**
694         * Checks if the icon request has the same uri, width, and height as the given values.
695         */
696        public boolean isSameRequest(@Nullable Uri uri, int width, int height) {
697            return Objects.equals(mUri, uri) && mWidth == width && mHeight == height;
698        }
699
700        @Override
701        public String toString() {
702            final StringBuilder sb = new StringBuilder("IconRequest{");
703            sb.append("uri=").append(mUri);
704            sb.append(", width=").append(mWidth);
705            sb.append(", height=").append(mHeight);
706            sb.append(", seq=").append(mSeq);
707            sb.append('}');
708            return sb.toString();
709        }
710
711        /**
712         * Gets an unmodifiable view of the list of callbacks associated with the request.
713         */
714        public List<IconCallback> getCallbacks() {
715            return Collections.unmodifiableList(mCallbacks);
716        }
717    }
718
719    /**
720     * ServiceConnection to the other app.
721     */
722    private class MediaServiceConnection implements ServiceConnection {
723        @Override
724        public void onServiceConnected(ComponentName name, IBinder binder) {
725            if (DBG) {
726                Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
727                        + " binder=" + binder);
728                dump();
729            }
730
731            // Make sure we are still the current connection, and that they haven't called
732            // disconnect().
733            if (!isCurrent("onServiceConnected")) {
734                return;
735            }
736
737            // Save their binder
738            mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
739
740            // We make a new mServiceCallbacks each time we connect so that we can drop
741            // responses from previous connections.
742            mServiceCallbacks = getNewServiceCallbacks();
743
744            // Call connect, which is async. When we get a response from that we will
745            // say that we're connected.
746            try {
747                if (DBG) {
748                    Log.d(TAG, "ServiceCallbacks.onConnect...");
749                    dump();
750                }
751                mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
752            } catch (RemoteException ex) {
753                // Connect failed, which isn't good. But the auto-reconnect on the service
754                // will take over and we will come back.  We will also get the
755                // onServiceDisconnected, which has all the cleanup code.  So let that do it.
756                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
757                if (DBG) {
758                    Log.d(TAG, "ServiceCallbacks.onConnect...");
759                    dump();
760                }
761            }
762        }
763
764        @Override
765        public void onServiceDisconnected(ComponentName name) {
766            if (DBG) {
767                Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
768                        + " this=" + this + " mServiceConnection=" + mServiceConnection);
769                dump();
770            }
771
772            // Make sure we are still the current connection, and that they haven't called
773            // disconnect().
774            if (!isCurrent("onServiceDisconnected")) {
775                return;
776            }
777
778            // Clear out what we set in onServiceConnected
779            mServiceBinder = null;
780            mServiceCallbacks = null;
781
782            // And tell the app that it's suspended.
783            mState = CONNECT_STATE_SUSPENDED;
784            mCallback.onConnectionSuspended();
785        }
786
787        /**
788         * Return true if this is the current ServiceConnection.  Also logs if it's not.
789         */
790        private boolean isCurrent(String funcName) {
791            if (mServiceConnection != this) {
792                if (mState != CONNECT_STATE_DISCONNECTED) {
793                    // Check mState, because otherwise this log is noisy.
794                    Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
795                            + mServiceConnection + " this=" + this);
796                }
797                return false;
798            }
799            return true;
800        }
801    };
802
803    /**
804     * Callbacks from the service.
805     */
806    private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
807        private WeakReference<MediaBrowser> mMediaBrowser;
808
809        public ServiceCallbacks(MediaBrowser mediaBrowser) {
810            mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
811        }
812
813        /**
814         * The other side has acknowledged our connection.  The parameters to this function
815         * are the initial data as requested.
816         */
817        @Override
818        public void onConnect(final Uri root, final MediaSession.Token session,
819                final Bundle extras) {
820            MediaBrowser mediaBrowser = mMediaBrowser.get();
821            if (mediaBrowser != null) {
822                mediaBrowser.onServiceConnected(this, root, session, extras);
823            }
824        }
825
826        /**
827         * The other side does not like us.  Tell the app via onConnectionFailed.
828         */
829        @Override
830        public void onConnectFailed() {
831            MediaBrowser mediaBrowser = mMediaBrowser.get();
832            if (mediaBrowser != null) {
833                mediaBrowser.onConnectionFailed(this);
834            }
835        }
836
837        @Override
838        public void onLoadChildren(final Uri uri, final ParceledListSlice list) {
839            MediaBrowser mediaBrowser = mMediaBrowser.get();
840            if (mediaBrowser != null) {
841                mediaBrowser.onLoadChildren(this, uri, list);
842            }
843        }
844
845        @Override
846        public void onLoadIcon(final int seqNum, final Bitmap bitmap) {
847            MediaBrowser mediaBrowser = mMediaBrowser.get();
848            if (mediaBrowser != null) {
849                mediaBrowser.onLoadIcon(this, seqNum, bitmap);
850            }
851        }
852    }
853
854    private static class Subscription {
855        final Uri uri;
856        SubscriptionCallback callback;
857
858        Subscription(Uri u) {
859            this.uri = u;
860        }
861    }
862}
863