[go: nahoru, domu]

MediaBrowserServiceCompat.java revision 82387d9f1b1ed30028e4c9090ec5fadce7d146f1
1/*
2 * Copyright (C) 2015 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.support.v4.media;
18
19import android.app.Service;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.os.Binder;
23import android.os.Build;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.IBinder;
27import android.os.Message;
28import android.os.Messenger;
29import android.os.Parcel;
30import android.os.RemoteException;
31import android.support.annotation.IntDef;
32import android.support.annotation.NonNull;
33import android.support.annotation.Nullable;
34import android.support.v4.app.BundleCompat;
35import android.support.v4.media.session.MediaSessionCompat;
36import android.support.v4.os.ResultReceiver;
37import android.support.v4.util.ArrayMap;
38import android.text.TextUtils;
39import android.util.Log;
40
41import java.io.FileDescriptor;
42import java.io.PrintWriter;
43import java.lang.annotation.Retention;
44import java.lang.annotation.RetentionPolicy;
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.HashMap;
48import java.util.List;
49
50import static android.support.v4.media.MediaBrowserProtocol.*;
51
52/**
53 * Base class for media browse services.
54 * <p>
55 * Media browse services enable applications to browse media content provided by an application
56 * and ask the application to start playing it. They may also be used to control content that
57 * is already playing by way of a {@link MediaSessionCompat}.
58 * </p>
59 *
60 * To extend this class, you must declare the service in your manifest file with
61 * an intent filter with the {@link #SERVICE_INTERFACE} action.
62 *
63 * For example:
64 * </p><pre>
65 * &lt;service android:name=".MyMediaBrowserServiceCompat"
66 *          android:label="&#64;string/service_name" >
67 *     &lt;intent-filter>
68 *         &lt;action android:name="android.media.browse.MediaBrowserService" />
69 *     &lt;/intent-filter>
70 * &lt;/service>
71 * </pre>
72 */
73public abstract class MediaBrowserServiceCompat extends Service {
74    private static final String TAG = "MediaBrowserServiceCompat";
75    private static final boolean DBG = false;
76
77    private MediaBrowserServiceImpl mImpl;
78
79    /**
80     * The {@link Intent} that must be declared as handled by the service.
81     */
82    public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
83
84    /**
85     * A key for passing the MediaItem to the ResultReceiver in getItem.
86     *
87     * @hide
88     */
89    public static final String KEY_MEDIA_ITEM = "media_item";
90
91    private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001;
92
93    /** @hide */
94    @Retention(RetentionPolicy.SOURCE)
95    @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED })
96    private @interface ResultFlags { }
97
98    private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
99    private final ServiceHandler mHandler = new ServiceHandler();
100    MediaSessionCompat.Token mSession;
101
102    interface MediaBrowserServiceImpl {
103        void onCreate();
104        IBinder onBind(Intent intent);
105    }
106
107    class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
108        private Messenger mMessenger;
109
110        @Override
111        public void onCreate() {
112            mMessenger = new Messenger(mHandler);
113        }
114
115        @Override
116        public IBinder onBind(Intent intent) {
117            if (SERVICE_INTERFACE.equals(intent.getAction())) {
118                return mMessenger.getBinder();
119            }
120            return null;
121        }
122    }
123
124    class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl {
125        private Object mServiceObj;
126
127        @Override
128        public void onCreate() {
129            mServiceObj = MediaBrowserServiceCompatApi21.createService();
130            MediaBrowserServiceCompatApi21.onCreate(mServiceObj, new ServiceImplApi21());
131        }
132
133        @Override
134        public IBinder onBind(Intent intent) {
135            return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
136        }
137    }
138
139    class MediaBrowserServiceImplApi23 implements MediaBrowserServiceImpl {
140        private Object mServiceObj;
141
142        @Override
143        public void onCreate() {
144            mServiceObj = MediaBrowserServiceCompatApi23.createService();
145            MediaBrowserServiceCompatApi23.onCreate(mServiceObj, new ServiceImplApi23());
146        }
147
148        @Override
149        public IBinder onBind(Intent intent) {
150            return MediaBrowserServiceCompatApi23.onBind(mServiceObj, intent);
151        }
152    }
153
154    private final class ServiceHandler extends Handler {
155        private final ServiceImpl mServiceImpl = new ServiceImpl();
156
157        @Override
158        public void handleMessage(Message msg) {
159            Bundle data = msg.getData();
160            switch (msg.what) {
161                case CLIENT_MSG_CONNECT:
162                    mServiceImpl.connect(data.getString(DATA_PACKAGE_NAME),
163                            data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS),
164                            new ServiceCallbacksCompat(msg.replyTo));
165                    break;
166                case CLIENT_MSG_DISCONNECT:
167                    mServiceImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo));
168                    break;
169                case CLIENT_MSG_ADD_SUBSCRIPTION:
170                    mServiceImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID),
171                            data.getBundle(DATA_OPTIONS), new ServiceCallbacksCompat(msg.replyTo));
172                    break;
173                case CLIENT_MSG_REMOVE_SUBSCRIPTION:
174                    mServiceImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID),
175                            data.getBundle(DATA_OPTIONS), new ServiceCallbacksCompat(msg.replyTo));
176                    break;
177                case CLIENT_MSG_GET_MEDIA_ITEM:
178                    mServiceImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID),
179                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER));
180                    break;
181                default:
182                    Log.w(TAG, "Unhandled message: " + msg
183                            + "\n  Service version: " + SERVICE_VERSION_CURRENT
184                            + "\n  Client version: " + msg.arg1);
185            }
186        }
187
188        @Override
189        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
190            // Binder.getCallingUid() in handleMessage will return the uid of this process.
191            // In order to get the right calling uid, Binder.getCallingUid() should be called here.
192            Bundle data = msg.getData();
193            data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
194            data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
195            return super.sendMessageAtTime(msg, uptimeMillis);
196        }
197
198        public void postOrRun(Runnable r) {
199            if (Thread.currentThread() == getLooper().getThread()) {
200                r.run();
201            } else {
202                post(r);
203            }
204        }
205
206        public ServiceImpl getServiceImpl() {
207            return mServiceImpl;
208        }
209    }
210
211    /**
212     * All the info about a connection.
213     */
214    private class ConnectionRecord {
215        String pkg;
216        Bundle rootHints;
217        ServiceCallbacks callbacks;
218        BrowserRoot root;
219        HashMap<String, List<Bundle>> subscriptions = new HashMap();
220    }
221
222    /**
223     * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
224     * <p>
225     * Each of the methods that takes one of these to send the result must call
226     * {@link #sendResult} to respond to the caller with the given results. If those
227     * functions return without calling {@link #sendResult}, they must instead call
228     * {@link #detach} before returning, and then may call {@link #sendResult} when
229     * they are done. If more than one of those methods is called, an exception will
230     * be thrown.
231     *
232     * @see MediaBrowserServiceCompat#onLoadChildren
233     * @see MediaBrowserServiceCompat#onLoadItem
234     */
235    public static class Result<T> {
236        private Object mDebug;
237        private boolean mDetachCalled;
238        private boolean mSendResultCalled;
239        private int mFlags;
240
241        Result(Object debug) {
242            mDebug = debug;
243        }
244
245        /**
246         * Send the result back to the caller.
247         */
248        public void sendResult(T result) {
249            if (mSendResultCalled) {
250                throw new IllegalStateException("sendResult() called twice for: " + mDebug);
251            }
252            mSendResultCalled = true;
253            onResultSent(result, mFlags);
254        }
255
256        /**
257         * Detach this message from the current thread and allow the {@link #sendResult}
258         * call to happen later.
259         */
260        public void detach() {
261            if (mDetachCalled) {
262                throw new IllegalStateException("detach() called when detach() had already"
263                        + " been called for: " + mDebug);
264            }
265            if (mSendResultCalled) {
266                throw new IllegalStateException("detach() called when sendResult() had already"
267                        + " been called for: " + mDebug);
268            }
269            mDetachCalled = true;
270        }
271
272        boolean isDone() {
273            return mDetachCalled || mSendResultCalled;
274        }
275
276        void setFlags(@ResultFlags int flags) {
277            mFlags = flags;
278        }
279
280        /**
281         * Called when the result is sent, after assertions about not being called twice
282         * have happened.
283         */
284        void onResultSent(T result, @ResultFlags int flags) {
285        }
286    }
287
288    private class ServiceImpl {
289        public void connect(final String pkg, final int uid, final Bundle rootHints,
290                final ServiceCallbacks callbacks) {
291
292            if (!isValidPackage(pkg, uid)) {
293                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
294                        + " package=" + pkg);
295            }
296
297            mHandler.postOrRun(new Runnable() {
298                @Override
299                public void run() {
300                    final IBinder b = callbacks.asBinder();
301
302                    // Clear out the old subscriptions. We are getting new ones.
303                    mConnections.remove(b);
304
305                    final ConnectionRecord connection = new ConnectionRecord();
306                    connection.pkg = pkg;
307                    connection.rootHints = rootHints;
308                    connection.callbacks = callbacks;
309
310                    connection.root =
311                            MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
312
313                    // If they didn't return something, don't allow this client.
314                    if (connection.root == null) {
315                        Log.i(TAG, "No root for client " + pkg + " from service "
316                                + getClass().getName());
317                        try {
318                            callbacks.onConnectFailed();
319                        } catch (RemoteException ex) {
320                            Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
321                                    + "pkg=" + pkg);
322                        }
323                    } else {
324                        try {
325                            mConnections.put(b, connection);
326                            if (mSession != null) {
327                                callbacks.onConnect(connection.root.getRootId(),
328                                        mSession, connection.root.getExtras());
329                            }
330                        } catch (RemoteException ex) {
331                            Log.w(TAG, "Calling onConnect() failed. Dropping client. "
332                                    + "pkg=" + pkg);
333                            mConnections.remove(b);
334                        }
335                    }
336                }
337            });
338        }
339
340        public void disconnect(final ServiceCallbacks callbacks) {
341            mHandler.postOrRun(new Runnable() {
342                @Override
343                public void run() {
344                    final IBinder b = callbacks.asBinder();
345
346                    // Clear out the old subscriptions. We are getting new ones.
347                    final ConnectionRecord old = mConnections.remove(b);
348                    if (old != null) {
349                        // TODO
350                    }
351                }
352            });
353        }
354
355        public void addSubscription(final String id, final Bundle options,
356                final ServiceCallbacks callbacks) {
357            mHandler.postOrRun(new Runnable() {
358                @Override
359                public void run() {
360                    final IBinder b = callbacks.asBinder();
361
362                    // Get the record for the connection
363                    final ConnectionRecord connection = mConnections.get(b);
364                    if (connection == null) {
365                        Log.w(TAG, "addSubscription for callback that isn't registered id="
366                                + id);
367                        return;
368                    }
369
370                    MediaBrowserServiceCompat.this.addSubscription(id, connection, options);
371                }
372            });
373        }
374
375        public void removeSubscription(final String id, final Bundle options,
376                final ServiceCallbacks callbacks) {
377            mHandler.postOrRun(new Runnable() {
378                @Override
379                public void run() {
380                    final IBinder b = callbacks.asBinder();
381
382                    ConnectionRecord connection = mConnections.get(b);
383                    if (connection == null) {
384                        Log.w(TAG, "removeSubscription for callback that isn't registered id="
385                                + id);
386                        return;
387                    }
388                    if (!MediaBrowserServiceCompat.this.removeSubscription(
389                            id, connection, options)) {
390                        Log.w(TAG, "removeSubscription called for " + id
391                                + " which is not subscribed");
392                    }
393                }
394            });
395        }
396
397        public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
398            if (TextUtils.isEmpty(mediaId) || receiver == null) {
399                return;
400            }
401
402            mHandler.postOrRun(new Runnable() {
403                @Override
404                public void run() {
405                    performLoadItem(mediaId, receiver);
406                }
407            });
408        }
409    }
410
411    private class ServiceImplApi21 implements MediaBrowserServiceCompatApi21.ServiceImplApi21 {
412        final ServiceImpl mServiceImpl;
413
414        ServiceImplApi21() {
415            mServiceImpl = mHandler.getServiceImpl();
416        }
417
418        @Override
419        public void connect(final String pkg, final Bundle rootHints,
420                final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
421            mServiceImpl.connect(pkg, Binder.getCallingUid(), rootHints,
422                    new ServiceCallbacksApi21(callbacks));
423        }
424
425        @Override
426        public void disconnect(final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
427            mServiceImpl.disconnect(new ServiceCallbacksApi21(callbacks));
428        }
429
430
431        @Override
432        public void addSubscription(
433                final String id, final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
434            mServiceImpl.addSubscription(id, null, new ServiceCallbacksApi21(callbacks));
435        }
436
437        @Override
438        public void removeSubscription(final String id,
439                final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
440            mServiceImpl.removeSubscription(id, null, new ServiceCallbacksApi21(callbacks));
441        }
442    }
443
444    private class ServiceImplApi23 extends ServiceImplApi21
445            implements MediaBrowserServiceCompatApi23.ServiceImplApi23 {
446        @Override
447        public void getMediaItem(final String mediaId,
448                final MediaBrowserServiceCompatApi23.ItemCallback cb) {
449            ResultReceiver receiverCompat = new ResultReceiver(mHandler) {
450                @Override
451                protected void onReceiveResult(int resultCode, Bundle resultData) {
452                    MediaBrowserCompat.MediaItem item = resultData.getParcelable(KEY_MEDIA_ITEM);
453                    Parcel itemParcel = null;
454                    if (item != null) {
455                        itemParcel = Parcel.obtain();
456                        item.writeToParcel(itemParcel, 0);
457                    }
458                    cb.onItemLoaded(resultCode, resultData, itemParcel);
459                }
460            };
461            mServiceImpl.getMediaItem(mediaId, receiverCompat);
462        }
463    }
464
465    private interface ServiceCallbacks {
466        IBinder asBinder();
467        void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
468                throws RemoteException;
469        void onConnectFailed() throws RemoteException;
470        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
471                throws RemoteException;
472    }
473
474    private class ServiceCallbacksCompat implements ServiceCallbacks {
475        final Messenger mCallbacks;
476
477        ServiceCallbacksCompat(Messenger callbacks) {
478            mCallbacks = callbacks;
479        }
480
481        public IBinder asBinder() {
482            return mCallbacks.getBinder();
483        }
484
485        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
486                throws RemoteException {
487            if (extras == null) {
488                extras = new Bundle();
489            }
490            extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
491            Bundle data = new Bundle();
492            data.putString(DATA_MEDIA_ITEM_ID, root);
493            data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session);
494            data.putBundle(DATA_ROOT_HINTS, extras);
495            sendRequest(SERVICE_MSG_ON_CONNECT, data);
496        }
497
498        public void onConnectFailed() throws RemoteException {
499            sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
500        }
501
502        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
503                Bundle options) throws RemoteException {
504            Bundle data = new Bundle();
505            data.putString(DATA_MEDIA_ITEM_ID, mediaId);
506            data.putBundle(DATA_OPTIONS, options);
507            if (list != null) {
508                data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
509                        list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
510            }
511            sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data);
512        }
513
514        private void sendRequest(int what, Bundle data) throws RemoteException {
515            Message msg = Message.obtain();
516            msg.what = what;
517            msg.arg1 = SERVICE_VERSION_CURRENT;
518            msg.setData(data);
519            mCallbacks.send(msg);
520        }
521    }
522
523    private class ServiceCallbacksApi21 implements ServiceCallbacks {
524        final MediaBrowserServiceCompatApi21.ServiceCallbacks mCallbacks;
525        Messenger mMessenger;
526
527        ServiceCallbacksApi21(MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
528            mCallbacks = callbacks;
529        }
530
531        public IBinder asBinder() {
532            return mCallbacks.asBinder();
533        }
534
535        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
536                throws RemoteException {
537            if (extras == null) {
538                extras = new Bundle();
539            }
540            mMessenger = new Messenger(mHandler);
541            BundleCompat.putBinder(extras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder());
542            extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
543            mCallbacks.onConnect(root, session.getToken(), extras);
544        }
545
546        public void onConnectFailed() throws RemoteException {
547            mCallbacks.onConnectFailed();
548        }
549
550        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
551                Bundle options) throws RemoteException {
552            List<Parcel> parcelList = null;
553            if (list != null) {
554                parcelList = new ArrayList<>();
555                for (MediaBrowserCompat.MediaItem item : list) {
556                    Parcel parcel = Parcel.obtain();
557                    item.writeToParcel(parcel, 0);
558                    parcelList.add(parcel);
559                }
560            }
561            mCallbacks.onLoadChildren(mediaId, parcelList);
562        }
563    }
564
565    @Override
566    public void onCreate() {
567        super.onCreate();
568        if (Build.VERSION.SDK_INT >= 23) {
569            mImpl = new MediaBrowserServiceImplApi23();
570        } else if (Build.VERSION.SDK_INT >= 21) {
571            mImpl = new MediaBrowserServiceImplApi21();
572        } else {
573            mImpl = new MediaBrowserServiceImplBase();
574        }
575        mImpl.onCreate();
576    }
577
578    @Override
579    public IBinder onBind(Intent intent) {
580        return mImpl.onBind(intent);
581    }
582
583    @Override
584    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
585    }
586
587    /**
588     * Called to get the root information for browsing by a particular client.
589     * <p>
590     * The implementation should verify that the client package has permission
591     * to access browse media information before returning the root id; it
592     * should return null if the client is not allowed to access this
593     * information.
594     * </p>
595     *
596     * @param clientPackageName The package name of the application which is
597     *            requesting access to browse media.
598     * @param clientUid The uid of the application which is requesting access to
599     *            browse media.
600     * @param rootHints An optional bundle of service-specific arguments to send
601     *            to the media browse service when connecting and retrieving the
602     *            root id for browsing, or null if none. The contents of this
603     *            bundle may affect the information returned when browsing.
604     * @return The {@link BrowserRoot} for accessing this app's content or null.
605     */
606    public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
607            int clientUid, @Nullable Bundle rootHints);
608
609    /**
610     * Called to get information about the children of a media item.
611     * <p>
612     * Implementations must call {@link Result#sendResult result.sendResult}
613     * with the list of children. If loading the children will be an expensive
614     * operation that should be performed on another thread,
615     * {@link Result#detach result.detach} may be called before returning from
616     * this function, and then {@link Result#sendResult result.sendResult}
617     * called when the loading is complete.
618     *
619     * @param parentId The id of the parent media item whose children are to be
620     *            queried.
621     * @param result The Result to send the list of children to, or null if the
622     *            id is invalid.
623     */
624    public abstract void onLoadChildren(@NonNull String parentId,
625            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
626
627    /**
628     * Called to get information about the children of a media item.
629     * <p>
630     * Implementations must call {@link Result#sendResult result.sendResult}
631     * with the list of children. If loading the children will be an expensive
632     * operation that should be performed on another thread,
633     * {@link Result#detach result.detach} may be called before returning from
634     * this function, and then {@link Result#sendResult result.sendResult}
635     * called when the loading is complete.
636     *
637     * @param parentId The id of the parent media item whose children are to be
638     *            queried.
639     * @param result The Result to send the list of children to, or null if the
640     *            id is invalid.
641     * @param options A bundle of service-specific arguments sent from the media
642     *            browse. The information returned through the result should be
643     *            affected by the contents of this bundle.
644     * {@hide}
645     */
646    public void onLoadChildren(@NonNull String parentId,
647            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) {
648        // To support backward compatibility, when the implementation of MediaBrowserService doesn't
649        // override onLoadChildren() with options, onLoadChildren() without options will be used
650        // instead, and the options will be applied in the implementation of result.onResultSent().
651        result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
652        onLoadChildren(parentId, result);
653    }
654
655    /**
656     * Called to get information about a specific media item.
657     * <p>
658     * Implementations must call {@link Result#sendResult result.sendResult}. If
659     * loading the item will be an expensive operation {@link Result#detach
660     * result.detach} may be called before returning from this function, and
661     * then {@link Result#sendResult result.sendResult} called when the item has
662     * been loaded.
663     * <p>
664     * The default implementation sends a null result.
665     *
666     * @param itemId The id for the specific
667     *            {@link MediaBrowserCompat.MediaItem}.
668     * @param result The Result to send the item to, or null if the id is
669     *            invalid.
670     */
671    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
672        result.sendResult(null);
673    }
674
675    /**
676     * Call to set the media session.
677     * <p>
678     * This should be called as soon as possible during the service's startup.
679     * It may only be called once.
680     *
681     * @param token The token for the service's {@link MediaSessionCompat}.
682     */
683    public void setSessionToken(final MediaSessionCompat.Token token) {
684        if (token == null) {
685            throw new IllegalArgumentException("Session token may not be null.");
686        }
687        if (mSession != null) {
688            throw new IllegalStateException("The session token has already been set.");
689        }
690        mSession = token;
691        mHandler.post(new Runnable() {
692            @Override
693            public void run() {
694                for (IBinder key : mConnections.keySet()) {
695                    ConnectionRecord connection = mConnections.get(key);
696                    try {
697                        connection.callbacks.onConnect(connection.root.getRootId(), token,
698                                connection.root.getExtras());
699                    } catch (RemoteException e) {
700                        Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
701                        mConnections.remove(key);
702                    }
703                }
704            }
705        });
706    }
707
708    /**
709     * Gets the session token, or null if it has not yet been created
710     * or if it has been destroyed.
711     */
712    public @Nullable MediaSessionCompat.Token getSessionToken() {
713        return mSession;
714    }
715
716    /**
717     * Notifies all connected media browsers that the children of
718     * the specified parent id have changed in some way.
719     * This will cause browsers to fetch subscribed content again.
720     *
721     * @param parentId The id of the parent media item whose
722     * children changed.
723     */
724    public void notifyChildrenChanged(@NonNull String parentId) {
725        notifyChildrenChangedInternal(parentId, null);
726    }
727
728    /**
729     * Notifies all connected media browsers that the children of
730     * the specified parent id have changed in some way.
731     * This will cause browsers to fetch subscribed content again.
732     *
733     * @param parentId The id of the parent media item whose
734     *            children changed.
735     * @param options A bundle of service-specific arguments to send
736     *            to the media browse. The contents of this bundle may
737     *            contain the information about the change.
738     * {@hide}
739     */
740    public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
741        if (options == null) {
742            throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
743        }
744        notifyChildrenChangedInternal(parentId, options);
745    }
746
747    private void notifyChildrenChangedInternal(final String parentId, final Bundle options) {
748        if (parentId == null) {
749            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
750        }
751        mHandler.post(new Runnable() {
752            @Override
753            public void run() {
754                for (IBinder binder : mConnections.keySet()) {
755                    ConnectionRecord connection = mConnections.get(binder);
756                    List<Bundle> optionsList = connection.subscriptions.get(parentId);
757                    if (optionsList != null) {
758                        for (Bundle bundle : optionsList) {
759                            if (MediaBrowserCompatUtils.hasDuplicatedItems(options, bundle)) {
760                                performLoadChildren(parentId, connection, bundle);
761                                break;
762                            }
763                        }
764                    }
765                }
766            }
767        });
768    }
769
770    /**
771     * Return whether the given package is one of the ones that is owned by the uid.
772     */
773    private boolean isValidPackage(String pkg, int uid) {
774        if (pkg == null) {
775            return false;
776        }
777        final PackageManager pm = getPackageManager();
778        final String[] packages = pm.getPackagesForUid(uid);
779        final int N = packages.length;
780        for (int i=0; i<N; i++) {
781            if (packages[i].equals(pkg)) {
782                return true;
783            }
784        }
785        return false;
786    }
787
788    /**
789     * Save the subscription and if it is a new subscription send the results.
790     */
791    private void addSubscription(String id, ConnectionRecord connection, Bundle options) {
792        // Save the subscription
793        List<Bundle> optionsList = connection.subscriptions.get(id);
794        if (optionsList == null) {
795            optionsList = new ArrayList();
796        }
797        for (Bundle bundle : optionsList) {
798            if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) {
799                return;
800            }
801        }
802        optionsList.add(options);
803        connection.subscriptions.put(id, optionsList);
804        // send the results
805        performLoadChildren(id, connection, options);
806    }
807
808    /**
809     * Remove the subscription.
810     */
811    private boolean removeSubscription(String id, ConnectionRecord connection, Bundle options) {
812        boolean removed = false;
813        List<Bundle> optionsList = connection.subscriptions.get(id);
814        if (optionsList != null) {
815            for (Bundle bundle : optionsList) {
816                if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) {
817                    removed = true;
818                    optionsList.remove(bundle);
819                    break;
820                }
821            }
822            if (optionsList.size() == 0) {
823                connection.subscriptions.remove(id);
824            }
825        }
826        return removed;
827    }
828
829    /**
830     * Call onLoadChildren and then send the results back to the connection.
831     * <p>
832     * Callers must make sure that this connection is still connected.
833     */
834    private void performLoadChildren(final String parentId, final ConnectionRecord connection,
835            final Bundle options) {
836        final Result<List<MediaBrowserCompat.MediaItem>> result
837                = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
838            @Override
839            void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flag) {
840                if (mConnections.get(connection.callbacks.asBinder()) != connection) {
841                    if (DBG) {
842                        Log.d(TAG, "Not sending onLoadChildren result for connection that has"
843                                + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
844                    }
845                    return;
846                }
847
848                List<MediaBrowserCompat.MediaItem> filteredList =
849                        (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
850                                ? applyOptions(list, options) : list;
851                try {
852                    connection.callbacks.onLoadChildren(parentId, filteredList, options);
853                } catch (RemoteException ex) {
854                    // The other side is in the process of crashing.
855                    Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
856                            + " package=" + connection.pkg);
857                }
858            }
859        };
860
861        if (options == null) {
862            onLoadChildren(parentId, result);
863        } else {
864            onLoadChildren(parentId, result, options);
865        }
866
867        if (!result.isDone()) {
868            throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
869                    + " before returning for package=" + connection.pkg + " id=" + parentId);
870        }
871    }
872
873    private List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
874            final Bundle options) {
875        int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
876        int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
877        if (page == -1 && pageSize == -1) {
878            return list;
879        }
880        int fromIndex = pageSize * (page - 1);
881        int toIndex = fromIndex + pageSize;
882        if (page < 1 || pageSize < 1 || fromIndex >= list.size()) {
883            return Collections.emptyList();
884        }
885        if (toIndex > list.size()) {
886            toIndex = list.size();
887        }
888        return list.subList(fromIndex, toIndex);
889    }
890
891    private void performLoadItem(String itemId, final ResultReceiver receiver) {
892        final Result<MediaBrowserCompat.MediaItem> result =
893                new Result<MediaBrowserCompat.MediaItem>(itemId) {
894            @Override
895            void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flag) {
896                Bundle bundle = new Bundle();
897                bundle.putParcelable(KEY_MEDIA_ITEM, item);
898                receiver.send(0, bundle);
899            }
900        };
901
902        MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
903
904        if (!result.isDone()) {
905            throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
906                    + " before returning for id=" + itemId);
907        }
908    }
909
910    /**
911     * Contains information that the browser service needs to send to the client
912     * when first connected.
913     */
914    public static final class BrowserRoot {
915        final private String mRootId;
916        final private Bundle mExtras;
917
918        /**
919         * Constructs a browser root.
920         * @param rootId The root id for browsing.
921         * @param extras Any extras about the browser service.
922         */
923        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
924            if (rootId == null) {
925                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
926                        "Use null for BrowserRoot instead.");
927            }
928            mRootId = rootId;
929            mExtras = extras;
930        }
931
932        /**
933         * Gets the root id for browsing.
934         */
935        public String getRootId() {
936            return mRootId;
937        }
938
939        /**
940         * Gets any extras about the browser service.
941         */
942        public Bundle getExtras() {
943            return mExtras;
944        }
945    }
946}
947