[go: nahoru, domu]

MediaBrowserServiceCompat.java revision 05ef0bb8aa7f86fea5ab65c54237eae1d3973588
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                case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
182                    mServiceImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo));
183                    break;
184                default:
185                    Log.w(TAG, "Unhandled message: " + msg
186                            + "\n  Service version: " + SERVICE_VERSION_CURRENT
187                            + "\n  Client version: " + msg.arg1);
188            }
189        }
190
191        @Override
192        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
193            // Binder.getCallingUid() in handleMessage will return the uid of this process.
194            // In order to get the right calling uid, Binder.getCallingUid() should be called here.
195            Bundle data = msg.getData();
196            data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
197            data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
198            return super.sendMessageAtTime(msg, uptimeMillis);
199        }
200
201        public void postOrRun(Runnable r) {
202            if (Thread.currentThread() == getLooper().getThread()) {
203                r.run();
204            } else {
205                post(r);
206            }
207        }
208
209        public ServiceImpl getServiceImpl() {
210            return mServiceImpl;
211        }
212    }
213
214    /**
215     * All the info about a connection.
216     */
217    private class ConnectionRecord {
218        String pkg;
219        Bundle rootHints;
220        ServiceCallbacks callbacks;
221        BrowserRoot root;
222        HashMap<String, List<Bundle>> subscriptions = new HashMap();
223    }
224
225    /**
226     * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
227     * <p>
228     * Each of the methods that takes one of these to send the result must call
229     * {@link #sendResult} to respond to the caller with the given results. If those
230     * functions return without calling {@link #sendResult}, they must instead call
231     * {@link #detach} before returning, and then may call {@link #sendResult} when
232     * they are done. If more than one of those methods is called, an exception will
233     * be thrown.
234     *
235     * @see MediaBrowserServiceCompat#onLoadChildren
236     * @see MediaBrowserServiceCompat#onLoadItem
237     */
238    public static class Result<T> {
239        private Object mDebug;
240        private boolean mDetachCalled;
241        private boolean mSendResultCalled;
242        private int mFlags;
243
244        Result(Object debug) {
245            mDebug = debug;
246        }
247
248        /**
249         * Send the result back to the caller.
250         */
251        public void sendResult(T result) {
252            if (mSendResultCalled) {
253                throw new IllegalStateException("sendResult() called twice for: " + mDebug);
254            }
255            mSendResultCalled = true;
256            onResultSent(result, mFlags);
257        }
258
259        /**
260         * Detach this message from the current thread and allow the {@link #sendResult}
261         * call to happen later.
262         */
263        public void detach() {
264            if (mDetachCalled) {
265                throw new IllegalStateException("detach() called when detach() had already"
266                        + " been called for: " + mDebug);
267            }
268            if (mSendResultCalled) {
269                throw new IllegalStateException("detach() called when sendResult() had already"
270                        + " been called for: " + mDebug);
271            }
272            mDetachCalled = true;
273        }
274
275        boolean isDone() {
276            return mDetachCalled || mSendResultCalled;
277        }
278
279        void setFlags(@ResultFlags int flags) {
280            mFlags = flags;
281        }
282
283        /**
284         * Called when the result is sent, after assertions about not being called twice
285         * have happened.
286         */
287        void onResultSent(T result, @ResultFlags int flags) {
288        }
289    }
290
291    private class ServiceImpl {
292        public void connect(final String pkg, final int uid, final Bundle rootHints,
293                final ServiceCallbacks callbacks) {
294
295            if (!isValidPackage(pkg, uid)) {
296                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
297                        + " package=" + pkg);
298            }
299
300            mHandler.postOrRun(new Runnable() {
301                @Override
302                public void run() {
303                    final IBinder b = callbacks.asBinder();
304
305                    // Clear out the old subscriptions. We are getting new ones.
306                    mConnections.remove(b);
307
308                    final ConnectionRecord connection = new ConnectionRecord();
309                    connection.pkg = pkg;
310                    connection.rootHints = rootHints;
311                    connection.callbacks = callbacks;
312
313                    connection.root =
314                            MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
315
316                    // If they didn't return something, don't allow this client.
317                    if (connection.root == null) {
318                        Log.i(TAG, "No root for client " + pkg + " from service "
319                                + getClass().getName());
320                        try {
321                            callbacks.onConnectFailed();
322                        } catch (RemoteException ex) {
323                            Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
324                                    + "pkg=" + pkg);
325                        }
326                    } else {
327                        try {
328                            mConnections.put(b, connection);
329                            if (mSession != null) {
330                                callbacks.onConnect(connection.root.getRootId(),
331                                        mSession, connection.root.getExtras());
332                            }
333                        } catch (RemoteException ex) {
334                            Log.w(TAG, "Calling onConnect() failed. Dropping client. "
335                                    + "pkg=" + pkg);
336                            mConnections.remove(b);
337                        }
338                    }
339                }
340            });
341        }
342
343        public void disconnect(final ServiceCallbacks callbacks) {
344            mHandler.postOrRun(new Runnable() {
345                @Override
346                public void run() {
347                    final IBinder b = callbacks.asBinder();
348
349                    // Clear out the old subscriptions. We are getting new ones.
350                    final ConnectionRecord old = mConnections.remove(b);
351                    if (old != null) {
352                        // TODO
353                    }
354                }
355            });
356        }
357
358        public void addSubscription(final String id, final Bundle options,
359                final ServiceCallbacks callbacks) {
360            mHandler.postOrRun(new Runnable() {
361                @Override
362                public void run() {
363                    final IBinder b = callbacks.asBinder();
364
365                    // Get the record for the connection
366                    final ConnectionRecord connection = mConnections.get(b);
367                    if (connection == null) {
368                        Log.w(TAG, "addSubscription for callback that isn't registered id="
369                                + id);
370                        return;
371                    }
372
373                    MediaBrowserServiceCompat.this.addSubscription(id, connection, options);
374                }
375            });
376        }
377
378        public void removeSubscription(final String id, final Bundle options,
379                final ServiceCallbacks callbacks) {
380            mHandler.postOrRun(new Runnable() {
381                @Override
382                public void run() {
383                    final IBinder b = callbacks.asBinder();
384
385                    ConnectionRecord connection = mConnections.get(b);
386                    if (connection == null) {
387                        Log.w(TAG, "removeSubscription for callback that isn't registered id="
388                                + id);
389                        return;
390                    }
391                    if (!MediaBrowserServiceCompat.this.removeSubscription(
392                            id, connection, options)) {
393                        Log.w(TAG, "removeSubscription called for " + id
394                                + " which is not subscribed");
395                    }
396                }
397            });
398        }
399
400        public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
401            if (TextUtils.isEmpty(mediaId) || receiver == null) {
402                return;
403            }
404
405            mHandler.postOrRun(new Runnable() {
406                @Override
407                public void run() {
408                    performLoadItem(mediaId, receiver);
409                }
410            });
411        }
412
413        // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
414        public void registerCallbacks(final ServiceCallbacks callbacks) {
415            mHandler.postOrRun(new Runnable() {
416                @Override
417                public void run() {
418                    final IBinder b = callbacks.asBinder();
419                    // Clear out the old subscriptions. We are getting new ones.
420                    mConnections.remove(b);
421
422                    final ConnectionRecord connection = new ConnectionRecord();
423                    connection.callbacks = callbacks;
424                    mConnections.put(b, connection);
425                }
426            });
427        }
428    }
429
430    private class ServiceImplApi21 implements MediaBrowserServiceCompatApi21.ServiceImplApi21 {
431        final ServiceImpl mServiceImpl;
432
433        ServiceImplApi21() {
434            mServiceImpl = mHandler.getServiceImpl();
435        }
436
437        @Override
438        public void connect(final String pkg, final Bundle rootHints,
439                final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
440            mServiceImpl.connect(pkg, Binder.getCallingUid(), rootHints,
441                    new ServiceCallbacksApi21(callbacks));
442        }
443
444        @Override
445        public void disconnect(final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
446            mServiceImpl.disconnect(new ServiceCallbacksApi21(callbacks));
447        }
448
449
450        @Override
451        public void addSubscription(
452                final String id, final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
453            mServiceImpl.addSubscription(id, null, new ServiceCallbacksApi21(callbacks));
454        }
455
456        @Override
457        public void removeSubscription(final String id,
458                final MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
459            mServiceImpl.removeSubscription(id, null, new ServiceCallbacksApi21(callbacks));
460        }
461    }
462
463    private class ServiceImplApi23 extends ServiceImplApi21
464            implements MediaBrowserServiceCompatApi23.ServiceImplApi23 {
465        @Override
466        public void getMediaItem(final String mediaId,
467                final MediaBrowserServiceCompatApi23.ItemCallback cb) {
468            ResultReceiver receiverCompat = new ResultReceiver(mHandler) {
469                @Override
470                protected void onReceiveResult(int resultCode, Bundle resultData) {
471                    MediaBrowserCompat.MediaItem item = resultData.getParcelable(KEY_MEDIA_ITEM);
472                    Parcel itemParcel = null;
473                    if (item != null) {
474                        itemParcel = Parcel.obtain();
475                        item.writeToParcel(itemParcel, 0);
476                    }
477                    cb.onItemLoaded(resultCode, resultData, itemParcel);
478                }
479            };
480            mServiceImpl.getMediaItem(mediaId, receiverCompat);
481        }
482    }
483
484    private interface ServiceCallbacks {
485        IBinder asBinder();
486        void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
487                throws RemoteException;
488        void onConnectFailed() throws RemoteException;
489        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
490                throws RemoteException;
491    }
492
493    private class ServiceCallbacksCompat implements ServiceCallbacks {
494        final Messenger mCallbacks;
495
496        ServiceCallbacksCompat(Messenger callbacks) {
497            mCallbacks = callbacks;
498        }
499
500        public IBinder asBinder() {
501            return mCallbacks.getBinder();
502        }
503
504        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
505                throws RemoteException {
506            if (extras == null) {
507                extras = new Bundle();
508            }
509            extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
510            Bundle data = new Bundle();
511            data.putString(DATA_MEDIA_ITEM_ID, root);
512            data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session);
513            data.putBundle(DATA_ROOT_HINTS, extras);
514            sendRequest(SERVICE_MSG_ON_CONNECT, data);
515        }
516
517        public void onConnectFailed() throws RemoteException {
518            sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
519        }
520
521        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
522                Bundle options) throws RemoteException {
523            Bundle data = new Bundle();
524            data.putString(DATA_MEDIA_ITEM_ID, mediaId);
525            data.putBundle(DATA_OPTIONS, options);
526            if (list != null) {
527                data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
528                        list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
529            }
530            sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data);
531        }
532
533        private void sendRequest(int what, Bundle data) throws RemoteException {
534            Message msg = Message.obtain();
535            msg.what = what;
536            msg.arg1 = SERVICE_VERSION_CURRENT;
537            msg.setData(data);
538            mCallbacks.send(msg);
539        }
540    }
541
542    private class ServiceCallbacksApi21 implements ServiceCallbacks {
543        final MediaBrowserServiceCompatApi21.ServiceCallbacks mCallbacks;
544        Messenger mMessenger;
545
546        ServiceCallbacksApi21(MediaBrowserServiceCompatApi21.ServiceCallbacks callbacks) {
547            mCallbacks = callbacks;
548        }
549
550        public IBinder asBinder() {
551            return mCallbacks.asBinder();
552        }
553
554        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
555                throws RemoteException {
556            if (extras == null) {
557                extras = new Bundle();
558            }
559            mMessenger = new Messenger(mHandler);
560            BundleCompat.putBinder(extras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder());
561            extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
562            mCallbacks.onConnect(root, session.getToken(), extras);
563        }
564
565        public void onConnectFailed() throws RemoteException {
566            mCallbacks.onConnectFailed();
567        }
568
569        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
570                Bundle options) throws RemoteException {
571            List<Parcel> parcelList = null;
572            if (list != null) {
573                parcelList = new ArrayList<>();
574                for (MediaBrowserCompat.MediaItem item : list) {
575                    Parcel parcel = Parcel.obtain();
576                    item.writeToParcel(parcel, 0);
577                    parcelList.add(parcel);
578                }
579            }
580            mCallbacks.onLoadChildren(mediaId, parcelList);
581        }
582    }
583
584    @Override
585    public void onCreate() {
586        super.onCreate();
587        if (Build.VERSION.SDK_INT >= 23) {
588            mImpl = new MediaBrowserServiceImplApi23();
589        } else if (Build.VERSION.SDK_INT >= 21) {
590            mImpl = new MediaBrowserServiceImplApi21();
591        } else {
592            mImpl = new MediaBrowserServiceImplBase();
593        }
594        mImpl.onCreate();
595    }
596
597    @Override
598    public IBinder onBind(Intent intent) {
599        return mImpl.onBind(intent);
600    }
601
602    @Override
603    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
604    }
605
606    /**
607     * Called to get the root information for browsing by a particular client.
608     * <p>
609     * The implementation should verify that the client package has permission
610     * to access browse media information before returning the root id; it
611     * should return null if the client is not allowed to access this
612     * information.
613     * </p>
614     *
615     * @param clientPackageName The package name of the application which is
616     *            requesting access to browse media.
617     * @param clientUid The uid of the application which is requesting access to
618     *            browse media.
619     * @param rootHints An optional bundle of service-specific arguments to send
620     *            to the media browse service when connecting and retrieving the
621     *            root id for browsing, or null if none. The contents of this
622     *            bundle may affect the information returned when browsing.
623     * @return The {@link BrowserRoot} for accessing this app's content or null.
624     */
625    public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
626            int clientUid, @Nullable Bundle rootHints);
627
628    /**
629     * Called to get information about the children of a media item.
630     * <p>
631     * Implementations must call {@link Result#sendResult result.sendResult}
632     * with the list of children. If loading the children will be an expensive
633     * operation that should be performed on another thread,
634     * {@link Result#detach result.detach} may be called before returning from
635     * this function, and then {@link Result#sendResult result.sendResult}
636     * called when the loading is complete.
637     *
638     * @param parentId The id of the parent media item whose children are to be
639     *            queried.
640     * @param result The Result to send the list of children to, or null if the
641     *            id is invalid.
642     */
643    public abstract void onLoadChildren(@NonNull String parentId,
644            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
645
646    /**
647     * Called to get information about the children of a media item.
648     * <p>
649     * Implementations must call {@link Result#sendResult result.sendResult}
650     * with the list of children. If loading the children will be an expensive
651     * operation that should be performed on another thread,
652     * {@link Result#detach result.detach} may be called before returning from
653     * this function, and then {@link Result#sendResult result.sendResult}
654     * called when the loading is complete.
655     *
656     * @param parentId The id of the parent media item whose children are to be
657     *            queried.
658     * @param result The Result to send the list of children to, or null if the
659     *            id is invalid.
660     * @param options A bundle of service-specific arguments sent from the media
661     *            browse. The information returned through the result should be
662     *            affected by the contents of this bundle.
663     * {@hide}
664     */
665    public void onLoadChildren(@NonNull String parentId,
666            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) {
667        // To support backward compatibility, when the implementation of MediaBrowserService doesn't
668        // override onLoadChildren() with options, onLoadChildren() without options will be used
669        // instead, and the options will be applied in the implementation of result.onResultSent().
670        result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
671        onLoadChildren(parentId, result);
672    }
673
674    /**
675     * Called to get information about a specific media item.
676     * <p>
677     * Implementations must call {@link Result#sendResult result.sendResult}. If
678     * loading the item will be an expensive operation {@link Result#detach
679     * result.detach} may be called before returning from this function, and
680     * then {@link Result#sendResult result.sendResult} called when the item has
681     * been loaded.
682     * <p>
683     * The default implementation sends a null result.
684     *
685     * @param itemId The id for the specific
686     *            {@link MediaBrowserCompat.MediaItem}.
687     * @param result The Result to send the item to, or null if the id is
688     *            invalid.
689     */
690    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
691        result.sendResult(null);
692    }
693
694    /**
695     * Call to set the media session.
696     * <p>
697     * This should be called as soon as possible during the service's startup.
698     * It may only be called once.
699     *
700     * @param token The token for the service's {@link MediaSessionCompat}.
701     */
702    public void setSessionToken(final MediaSessionCompat.Token token) {
703        if (token == null) {
704            throw new IllegalArgumentException("Session token may not be null.");
705        }
706        if (mSession != null) {
707            throw new IllegalStateException("The session token has already been set.");
708        }
709        mSession = token;
710        mHandler.post(new Runnable() {
711            @Override
712            public void run() {
713                for (IBinder key : mConnections.keySet()) {
714                    ConnectionRecord connection = mConnections.get(key);
715                    try {
716                        connection.callbacks.onConnect(connection.root.getRootId(), token,
717                                connection.root.getExtras());
718                    } catch (RemoteException e) {
719                        Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
720                        mConnections.remove(key);
721                    }
722                }
723            }
724        });
725    }
726
727    /**
728     * Gets the session token, or null if it has not yet been created
729     * or if it has been destroyed.
730     */
731    public @Nullable MediaSessionCompat.Token getSessionToken() {
732        return mSession;
733    }
734
735    /**
736     * Notifies all connected media browsers that the children of
737     * the specified parent id have changed in some way.
738     * This will cause browsers to fetch subscribed content again.
739     *
740     * @param parentId The id of the parent media item whose
741     * children changed.
742     */
743    public void notifyChildrenChanged(@NonNull String parentId) {
744        notifyChildrenChangedInternal(parentId, null);
745    }
746
747    /**
748     * Notifies all connected media browsers that the children of
749     * the specified parent id have changed in some way.
750     * This will cause browsers to fetch subscribed content again.
751     *
752     * @param parentId The id of the parent media item whose
753     *            children changed.
754     * @param options A bundle of service-specific arguments to send
755     *            to the media browse. The contents of this bundle may
756     *            contain the information about the change.
757     * {@hide}
758     */
759    public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
760        if (options == null) {
761            throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
762        }
763        notifyChildrenChangedInternal(parentId, options);
764    }
765
766    private void notifyChildrenChangedInternal(final String parentId, final Bundle options) {
767        if (parentId == null) {
768            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
769        }
770        mHandler.post(new Runnable() {
771            @Override
772            public void run() {
773                for (IBinder binder : mConnections.keySet()) {
774                    ConnectionRecord connection = mConnections.get(binder);
775                    List<Bundle> optionsList = connection.subscriptions.get(parentId);
776                    if (optionsList != null) {
777                        for (Bundle bundle : optionsList) {
778                            if (MediaBrowserCompatUtils.hasDuplicatedItems(options, bundle)) {
779                                performLoadChildren(parentId, connection, bundle);
780                                break;
781                            }
782                        }
783                    }
784                }
785            }
786        });
787    }
788
789    /**
790     * Return whether the given package is one of the ones that is owned by the uid.
791     */
792    private boolean isValidPackage(String pkg, int uid) {
793        if (pkg == null) {
794            return false;
795        }
796        final PackageManager pm = getPackageManager();
797        final String[] packages = pm.getPackagesForUid(uid);
798        final int N = packages.length;
799        for (int i=0; i<N; i++) {
800            if (packages[i].equals(pkg)) {
801                return true;
802            }
803        }
804        return false;
805    }
806
807    /**
808     * Save the subscription and if it is a new subscription send the results.
809     */
810    private void addSubscription(String id, ConnectionRecord connection, Bundle options) {
811        // Save the subscription
812        List<Bundle> optionsList = connection.subscriptions.get(id);
813        if (optionsList == null) {
814            optionsList = new ArrayList();
815        }
816        for (Bundle bundle : optionsList) {
817            if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) {
818                return;
819            }
820        }
821        optionsList.add(options);
822        connection.subscriptions.put(id, optionsList);
823        // send the results
824        performLoadChildren(id, connection, options);
825    }
826
827    /**
828     * Remove the subscription.
829     */
830    private boolean removeSubscription(String id, ConnectionRecord connection, Bundle options) {
831        boolean removed = false;
832        List<Bundle> optionsList = connection.subscriptions.get(id);
833        if (optionsList != null) {
834            for (Bundle bundle : optionsList) {
835                if (MediaBrowserCompatUtils.areSameOptions(options, bundle)) {
836                    removed = true;
837                    optionsList.remove(bundle);
838                    break;
839                }
840            }
841            if (optionsList.size() == 0) {
842                connection.subscriptions.remove(id);
843            }
844        }
845        return removed;
846    }
847
848    /**
849     * Call onLoadChildren and then send the results back to the connection.
850     * <p>
851     * Callers must make sure that this connection is still connected.
852     */
853    private void performLoadChildren(final String parentId, final ConnectionRecord connection,
854            final Bundle options) {
855        final Result<List<MediaBrowserCompat.MediaItem>> result
856                = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
857            @Override
858            void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flag) {
859                if (mConnections.get(connection.callbacks.asBinder()) != connection) {
860                    if (DBG) {
861                        Log.d(TAG, "Not sending onLoadChildren result for connection that has"
862                                + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
863                    }
864                    return;
865                }
866
867                List<MediaBrowserCompat.MediaItem> filteredList =
868                        (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
869                                ? MediaBrowserCompatUtils.applyOptions(list, options) : list;
870                try {
871                    connection.callbacks.onLoadChildren(parentId, filteredList, options);
872                } catch (RemoteException ex) {
873                    // The other side is in the process of crashing.
874                    Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
875                            + " package=" + connection.pkg);
876                }
877            }
878        };
879
880        if (options == null) {
881            onLoadChildren(parentId, result);
882        } else {
883            onLoadChildren(parentId, result, options);
884        }
885
886        if (!result.isDone()) {
887            throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
888                    + " before returning for package=" + connection.pkg + " id=" + parentId);
889        }
890    }
891
892    private List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
893            final Bundle options) {
894        int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
895        int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
896        if (page == -1 && pageSize == -1) {
897            return list;
898        }
899        int fromIndex = pageSize * (page - 1);
900        int toIndex = fromIndex + pageSize;
901        if (page < 1 || pageSize < 1 || fromIndex >= list.size()) {
902            return Collections.emptyList();
903        }
904        if (toIndex > list.size()) {
905            toIndex = list.size();
906        }
907        return list.subList(fromIndex, toIndex);
908    }
909
910    private void performLoadItem(String itemId, final ResultReceiver receiver) {
911        final Result<MediaBrowserCompat.MediaItem> result =
912                new Result<MediaBrowserCompat.MediaItem>(itemId) {
913            @Override
914            void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flag) {
915                Bundle bundle = new Bundle();
916                bundle.putParcelable(KEY_MEDIA_ITEM, item);
917                receiver.send(0, bundle);
918            }
919        };
920
921        MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
922
923        if (!result.isDone()) {
924            throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
925                    + " before returning for id=" + itemId);
926        }
927    }
928
929    /**
930     * Contains information that the browser service needs to send to the client
931     * when first connected.
932     */
933    public static final class BrowserRoot {
934        /**
935         * The lookup key for a boolean that indicates whether the browser service should return a
936         * browser root for recently played media items.
937         *
938         * <p>When creating a media browser for a given media browser service, this key can be
939         * supplied as a root hint for retrieving media items that are recently played.
940         * If the media browser service can provide such media items, the implementation must return
941         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
942         *
943         * <p>The root hint may contain multiple keys.
944         *
945         * @see #EXTRA_OFFLINE
946         * @see #EXTRA_SUGGESTED
947         * @hide
948         */
949        public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
950
951        /**
952         * The lookup key for a boolean that indicates whether the browser service should return a
953         * browser root for offline media items.
954         *
955         * <p>When creating a media browser for a given media browser service, this key can be
956         * supplied as a root hint for retrieving media items that are can be played without an
957         * internet connection.
958         * If the media browser service can provide such media items, the implementation must return
959         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
960         *
961         * <p>The root hint may contain multiple keys.
962         *
963         * @see #EXTRA_RECENT
964         * @see #EXTRA_SUGGESTED
965         * @hide
966         */
967        public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
968
969        /**
970         * The lookup key for a boolean that indicates whether the browser service should return a
971         * browser root for suggested media items.
972         *
973         * <p>When creating a media browser for a given media browser service, this key can be
974         * supplied as a root hint for retrieving the media items suggested by the media browser
975         * service.
976         * If the media browser service can provide such media items, the implementation must return
977         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
978         *
979         * <p>The root hint may contain multiple keys.
980         *
981         * @see #EXTRA_RECENT
982         * @see #EXTRA_OFFLINE
983         * @hide
984         */
985        public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
986
987        final private String mRootId;
988        final private Bundle mExtras;
989
990        /**
991         * Constructs a browser root.
992         * @param rootId The root id for browsing.
993         * @param extras Any extras about the browser service.
994         */
995        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
996            if (rootId == null) {
997                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
998                        "Use null for BrowserRoot instead.");
999            }
1000            mRootId = rootId;
1001            mExtras = extras;
1002        }
1003
1004        /**
1005         * Gets the root id for browsing.
1006         */
1007        public String getRootId() {
1008            return mRootId;
1009        }
1010
1011        /**
1012         * Gets any extras about the browser service.
1013         */
1014        public Bundle getExtras() {
1015            return mExtras;
1016        }
1017    }
1018}
1019