[go: nahoru, domu]

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