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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
21import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
22import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
23import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
24import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
25import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
26import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
27import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
28import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
29import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
30import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID;
31import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
32import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
33import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
34import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
35import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
36import static android.support.v4.media.MediaBrowserProtocol.DATA_OPTIONS;
37import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
38import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
39import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
40import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
41import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
42import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
43import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
44import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
45import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
46import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
47import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
48import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
49import static android.support.v4.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT;
50
51import android.app.Service;
52import android.content.Intent;
53import android.content.pm.PackageManager;
54import android.os.Binder;
55import android.os.Build;
56import android.os.Bundle;
57import android.os.Handler;
58import android.os.IBinder;
59import android.os.Message;
60import android.os.Messenger;
61import android.os.Parcel;
62import android.os.RemoteException;
63import android.support.annotation.IntDef;
64import android.support.annotation.NonNull;
65import android.support.annotation.Nullable;
66import android.support.annotation.RequiresApi;
67import android.support.annotation.RestrictTo;
68import android.support.v4.app.BundleCompat;
69import android.support.v4.media.session.IMediaSession;
70import android.support.v4.media.session.MediaSessionCompat;
71import android.support.v4.os.BuildCompat;
72import android.support.v4.os.ResultReceiver;
73import android.support.v4.util.ArrayMap;
74import android.support.v4.util.Pair;
75import android.text.TextUtils;
76import android.util.Log;
77
78import java.io.FileDescriptor;
79import java.io.PrintWriter;
80import java.lang.annotation.Retention;
81import java.lang.annotation.RetentionPolicy;
82import java.util.ArrayList;
83import java.util.Collections;
84import java.util.HashMap;
85import java.util.Iterator;
86import java.util.List;
87
88/**
89 * Base class for media browse services.
90 * <p>
91 * Media browse services enable applications to browse media content provided by an application
92 * and ask the application to start playing it. They may also be used to control content that
93 * is already playing by way of a {@link MediaSessionCompat}.
94 * </p>
95 *
96 * To extend this class, you must declare the service in your manifest file with
97 * an intent filter with the {@link #SERVICE_INTERFACE} action.
98 *
99 * For example:
100 * </p><pre>
101 * &lt;service android:name=".MyMediaBrowserServiceCompat"
102 *          android:label="&#64;string/service_name" >
103 *     &lt;intent-filter>
104 *         &lt;action android:name="android.media.browse.MediaBrowserService" />
105 *     &lt;/intent-filter>
106 * &lt;/service>
107 * </pre>
108 *
109 * <div class="special reference">
110 * <h3>Developer Guides</h3>
111 * <p>For information about building your media application, read the
112 * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
113 * </div>
114 */
115public abstract class MediaBrowserServiceCompat extends Service {
116    static final String TAG = "MBServiceCompat";
117    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
118
119    private static final float EPSILON = 0.00001f;
120
121    private MediaBrowserServiceImpl mImpl;
122
123    /**
124     * The {@link Intent} that must be declared as handled by the service.
125     */
126    public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
127
128    /**
129     * A key for passing the MediaItem to the ResultReceiver in getItem.
130     *
131     * @hide
132     */
133    @RestrictTo(LIBRARY_GROUP)
134    public static final String KEY_MEDIA_ITEM = "media_item";
135
136    /**
137     * A key for passing the list of MediaItems to the ResultReceiver in search.
138     *
139     * @hide
140     */
141    @RestrictTo(LIBRARY_GROUP)
142    public static final String KEY_SEARCH_RESULTS = "search_results";
143
144    static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
145    static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
146    static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2;
147
148    static final int RESULT_ERROR = -1;
149    static final int RESULT_OK = 0;
150    static final int RESULT_PROGRESS_UPDATE = 1;
151
152    /** @hide */
153    @RestrictTo(LIBRARY_GROUP)
154    @Retention(RetentionPolicy.SOURCE)
155    @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
156            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
157    private @interface ResultFlags { }
158
159    final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
160    ConnectionRecord mCurConnection;
161    final ServiceHandler mHandler = new ServiceHandler();
162    MediaSessionCompat.Token mSession;
163
164    interface MediaBrowserServiceImpl {
165        void onCreate();
166        IBinder onBind(Intent intent);
167        void setSessionToken(MediaSessionCompat.Token token);
168        void notifyChildrenChanged(final String parentId, final Bundle options);
169        Bundle getBrowserRootHints();
170    }
171
172    class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
173        private Messenger mMessenger;
174
175        @Override
176        public void onCreate() {
177            mMessenger = new Messenger(mHandler);
178        }
179
180        @Override
181        public IBinder onBind(Intent intent) {
182            if (SERVICE_INTERFACE.equals(intent.getAction())) {
183                return mMessenger.getBinder();
184            }
185            return null;
186        }
187
188        @Override
189        public void setSessionToken(final MediaSessionCompat.Token token) {
190            mHandler.post(new Runnable() {
191                @Override
192                public void run() {
193                    Iterator<ConnectionRecord> iter = mConnections.values().iterator();
194                    while (iter.hasNext()){
195                        ConnectionRecord connection = iter.next();
196                        try {
197                            connection.callbacks.onConnect(connection.root.getRootId(), token,
198                                    connection.root.getExtras());
199                        } catch (RemoteException e) {
200                            Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
201                            iter.remove();
202                        }
203                    }
204                }
205            });
206        }
207
208        @Override
209        public void notifyChildrenChanged(@NonNull final String parentId, final Bundle options) {
210            mHandler.post(new Runnable() {
211                @Override
212                public void run() {
213                    for (IBinder binder : mConnections.keySet()) {
214                        ConnectionRecord connection = mConnections.get(binder);
215                        List<Pair<IBinder, Bundle>> callbackList =
216                                connection.subscriptions.get(parentId);
217                        if (callbackList != null) {
218                            for (Pair<IBinder, Bundle> callback : callbackList) {
219                                if (MediaBrowserCompatUtils.hasDuplicatedItems(
220                                        options, callback.second)) {
221                                    performLoadChildren(parentId, connection, callback.second);
222                                }
223                            }
224                        }
225                    }
226                }
227            });
228        }
229
230        @Override
231        public Bundle getBrowserRootHints() {
232            if (mCurConnection == null) {
233                throw new IllegalStateException("This should be called inside of onLoadChildren,"
234                        + " onLoadItem or onSearch methods");
235            }
236            return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
237        }
238    }
239
240    @RequiresApi(21)
241    class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl,
242            MediaBrowserServiceCompatApi21.ServiceCompatProxy {
243        final List<Bundle> mRootExtrasList = new ArrayList<>();
244        Object mServiceObj;
245        Messenger mMessenger;
246
247        @Override
248        public void onCreate() {
249            mServiceObj = MediaBrowserServiceCompatApi21.createService(
250                    MediaBrowserServiceCompat.this, this);
251            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
252        }
253
254        @Override
255        public IBinder onBind(Intent intent) {
256            return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
257        }
258
259        @Override
260        public void setSessionToken(final MediaSessionCompat.Token token) {
261            mHandler.postOrRun(new Runnable() {
262                @Override
263                public void run() {
264                    if (!mRootExtrasList.isEmpty()) {
265                        IMediaSession extraBinder = token.getExtraBinder();
266                        if (extraBinder != null) {
267                            for (Bundle rootExtras : mRootExtrasList) {
268                                BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER,
269                                        extraBinder.asBinder());
270                            }
271                        }
272                        mRootExtrasList.clear();
273                    }
274                    MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
275                }
276            });
277        }
278
279        @Override
280        public void notifyChildrenChanged(final String parentId, final Bundle options) {
281            if (mMessenger == null) {
282                MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
283            } else {
284                mHandler.post(new Runnable() {
285                    @Override
286                    public void run() {
287                        for (IBinder binder : mConnections.keySet()) {
288                            ConnectionRecord connection = mConnections.get(binder);
289                            List<Pair<IBinder, Bundle>> callbackList =
290                                    connection.subscriptions.get(parentId);
291                            if (callbackList != null) {
292                                for (Pair<IBinder, Bundle> callback : callbackList) {
293                                    if (MediaBrowserCompatUtils.hasDuplicatedItems(
294                                            options, callback.second)) {
295                                        performLoadChildren(parentId, connection, callback.second);
296                                    }
297                                }
298                            }
299                        }
300                    }
301                });
302            }
303        }
304
305        @Override
306        public Bundle getBrowserRootHints() {
307            if (mMessenger == null) {
308                // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
309                return null;
310            }
311            if (mCurConnection == null) {
312                throw new IllegalStateException("This should be called inside of onLoadChildren,"
313                        + " onLoadItem or onSearch methods");
314            }
315            return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
316        }
317
318        @Override
319        public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
320                String clientPackageName, int clientUid, Bundle rootHints) {
321            Bundle rootExtras = null;
322            if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) {
323                rootHints.remove(EXTRA_CLIENT_VERSION);
324                mMessenger = new Messenger(mHandler);
325                rootExtras = new Bundle();
326                rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
327                BundleCompat.putBinder(rootExtras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder());
328                if (mSession != null) {
329                    IMediaSession extraBinder = mSession.getExtraBinder();
330                    BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER,
331                            extraBinder == null ? null : extraBinder.asBinder());
332                } else {
333                    mRootExtrasList.add(rootExtras);
334                }
335            }
336            BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
337                    clientPackageName, clientUid, rootHints);
338            if (root == null) {
339                return null;
340            }
341            if (rootExtras == null) {
342                rootExtras = root.getExtras();
343            } else if (root.getExtras() != null) {
344                rootExtras.putAll(root.getExtras());
345            }
346            return new MediaBrowserServiceCompatApi21.BrowserRoot(
347                    root.getRootId(), rootExtras);
348        }
349
350        @Override
351        public void onLoadChildren(String parentId,
352                final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper) {
353            final Result<List<MediaBrowserCompat.MediaItem>> result
354                    = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
355                @Override
356                void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
357                    List<Parcel> parcelList = null;
358                    if (list != null) {
359                        parcelList = new ArrayList<>();
360                        for (MediaBrowserCompat.MediaItem item : list) {
361                            Parcel parcel = Parcel.obtain();
362                            item.writeToParcel(parcel, 0);
363                            parcelList.add(parcel);
364                        }
365                    }
366                    resultWrapper.sendResult(parcelList);
367                }
368
369                @Override
370                public void detach() {
371                    resultWrapper.detach();
372                }
373            };
374            MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
375        }
376    }
377
378    @RequiresApi(23)
379    class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 implements
380            MediaBrowserServiceCompatApi23.ServiceCompatProxy {
381        @Override
382        public void onCreate() {
383            mServiceObj = MediaBrowserServiceCompatApi23.createService(
384                    MediaBrowserServiceCompat.this, this);
385            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
386        }
387
388        @Override
389        public void onLoadItem(String itemId,
390                final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper) {
391            final Result<MediaBrowserCompat.MediaItem> result
392                    = new Result<MediaBrowserCompat.MediaItem>(itemId) {
393                @Override
394                void onResultSent(MediaBrowserCompat.MediaItem item) {
395                    if (item == null) {
396                        resultWrapper.sendResult(null);
397                    } else {
398                        Parcel parcelItem = Parcel.obtain();
399                        item.writeToParcel(parcelItem, 0);
400                        resultWrapper.sendResult(parcelItem);
401                    }
402                }
403
404                @Override
405                public void detach() {
406                    resultWrapper.detach();
407                }
408            };
409            MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
410        }
411    }
412
413    // TODO: Rename to MediaBrowserServiceImplApi26 once O is released
414    @RequiresApi(26)
415    class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements
416            MediaBrowserServiceCompatApi24.ServiceCompatProxy {
417        @Override
418        public void onCreate() {
419            mServiceObj = MediaBrowserServiceCompatApi24.createService(
420                    MediaBrowserServiceCompat.this, this);
421            MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
422        }
423
424        @Override
425        public void notifyChildrenChanged(final String parentId, final Bundle options) {
426            if (options == null) {
427                MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
428            } else {
429                MediaBrowserServiceCompatApi24.notifyChildrenChanged(mServiceObj, parentId,
430                        options);
431            }
432        }
433
434        @Override
435        public void onLoadChildren(String parentId,
436                final MediaBrowserServiceCompatApi24.ResultWrapper resultWrapper, Bundle options) {
437            final Result<List<MediaBrowserCompat.MediaItem>> result
438                    = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
439                @Override
440                void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
441                    List<Parcel> parcelList = null;
442                    if (list != null) {
443                        parcelList = new ArrayList<>();
444                        for (MediaBrowserCompat.MediaItem item : list) {
445                            Parcel parcel = Parcel.obtain();
446                            item.writeToParcel(parcel, 0);
447                            parcelList.add(parcel);
448                        }
449                    }
450                    resultWrapper.sendResult(parcelList, getFlags());
451                }
452
453                @Override
454                public void detach() {
455                    resultWrapper.detach();
456                }
457            };
458            MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options);
459        }
460
461        @Override
462        public Bundle getBrowserRootHints() {
463            // If EXTRA_MESSENGER_BINDER is used, mCurConnection is not null.
464            if (mCurConnection != null) {
465                return mCurConnection.rootHints == null ? null
466                        : new Bundle(mCurConnection.rootHints);
467            }
468            return MediaBrowserServiceCompatApi24.getBrowserRootHints(mServiceObj);
469        }
470    }
471
472    private final class ServiceHandler extends Handler {
473        private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl();
474
475        ServiceHandler() {
476        }
477
478        @Override
479        public void handleMessage(Message msg) {
480            Bundle data = msg.getData();
481            switch (msg.what) {
482                case CLIENT_MSG_CONNECT:
483                    mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME),
484                            data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS),
485                            new ServiceCallbacksCompat(msg.replyTo));
486                    break;
487                case CLIENT_MSG_DISCONNECT:
488                    mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo));
489                    break;
490                case CLIENT_MSG_ADD_SUBSCRIPTION:
491                    mServiceBinderImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID),
492                            BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
493                            data.getBundle(DATA_OPTIONS),
494                            new ServiceCallbacksCompat(msg.replyTo));
495                    break;
496                case CLIENT_MSG_REMOVE_SUBSCRIPTION:
497                    mServiceBinderImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID),
498                            BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
499                            new ServiceCallbacksCompat(msg.replyTo));
500                    break;
501                case CLIENT_MSG_GET_MEDIA_ITEM:
502                    mServiceBinderImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID),
503                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
504                            new ServiceCallbacksCompat(msg.replyTo));
505                    break;
506                case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
507                    mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo),
508                            data.getBundle(DATA_ROOT_HINTS));
509                    break;
510                case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
511                    mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
512                    break;
513                case CLIENT_MSG_SEARCH:
514                    mServiceBinderImpl.search(data.getString(DATA_SEARCH_QUERY),
515                            data.getBundle(DATA_SEARCH_EXTRAS),
516                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
517                            new ServiceCallbacksCompat(msg.replyTo));
518                    break;
519                case CLIENT_MSG_SEND_CUSTOM_ACTION:
520                    mServiceBinderImpl.sendCustomAction(data.getString(DATA_CUSTOM_ACTION),
521                            data.getBundle(DATA_CUSTOM_ACTION_EXTRAS),
522                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
523                            new ServiceCallbacksCompat(msg.replyTo));
524                    break;
525                default:
526                    Log.w(TAG, "Unhandled message: " + msg
527                            + "\n  Service version: " + SERVICE_VERSION_CURRENT
528                            + "\n  Client version: " + msg.arg1);
529            }
530        }
531
532        @Override
533        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
534            // Binder.getCallingUid() in handleMessage will return the uid of this process.
535            // In order to get the right calling uid, Binder.getCallingUid() should be called here.
536            Bundle data = msg.getData();
537            data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
538            data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
539            return super.sendMessageAtTime(msg, uptimeMillis);
540        }
541
542        public void postOrRun(Runnable r) {
543            if (Thread.currentThread() == getLooper().getThread()) {
544                r.run();
545            } else {
546                post(r);
547            }
548        }
549    }
550
551    /**
552     * All the info about a connection.
553     */
554    private static class ConnectionRecord {
555        String pkg;
556        Bundle rootHints;
557        ServiceCallbacks callbacks;
558        BrowserRoot root;
559        HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
560
561        ConnectionRecord() {
562        }
563    }
564
565    /**
566     * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
567     * <p>
568     * Each of the methods that takes one of these to send the result must call either
569     * {@link #sendResult} or {@link #sendError} to respond to the caller with the given results or
570     * errors. If those functions return without calling {@link #sendResult} or {@link #sendError},
571     * they must instead call {@link #detach} before returning, and then may call
572     * {@link #sendResult} or {@link #sendError} when they are done. If {@link #sendResult},
573     * {@link #sendError}, or {@link #detach} is called twice, an exception will be thrown.
574     * </p><p>
575     * Those functions might also want to call {@link #sendProgressUpdate} to send interim updates
576     * to the caller. If it is called after calling {@link #sendResult} or {@link #sendError}, an
577     * exception will be thrown.
578     * </p>
579     *
580     * @see MediaBrowserServiceCompat#onLoadChildren
581     * @see MediaBrowserServiceCompat#onLoadItem
582     * @see MediaBrowserServiceCompat#onSearch
583     * @see MediaBrowserServiceCompat#onCustomAction
584     */
585    public static class Result<T> {
586        private final Object mDebug;
587        private boolean mDetachCalled;
588        private boolean mSendResultCalled;
589        private boolean mSendProgressUpdateCalled;
590        private boolean mSendErrorCalled;
591        private int mFlags;
592
593        Result(Object debug) {
594            mDebug = debug;
595        }
596
597        /**
598         * Send the result back to the caller.
599         */
600        public void sendResult(T result) {
601            if (mSendResultCalled || mSendErrorCalled) {
602                throw new IllegalStateException("sendResult() called when either sendResult() or "
603                        + "sendError() had already been called for: " + mDebug);
604            }
605            mSendResultCalled = true;
606            onResultSent(result);
607        }
608
609        /**
610         * Send an interim update to the caller. This method is supported only when it is used in
611         * {@link #onCustomAction}.
612         *
613         * @param extras A bundle that contains extra data.
614         */
615        public void sendProgressUpdate(Bundle extras) {
616            if (mSendResultCalled || mSendErrorCalled) {
617                throw new IllegalStateException("sendProgressUpdate() called when either "
618                        + "sendResult() or sendError() had already been called for: " + mDebug);
619            }
620            checkExtraFields(extras);
621            mSendProgressUpdateCalled = true;
622            onProgressUpdateSent(extras);
623        }
624
625        /**
626         * Notify the caller of a failure. This is supported only when it is used in
627         * {@link #onCustomAction}.
628         *
629         * @param extras A bundle that contains extra data.
630         */
631        public void sendError(Bundle extras) {
632            if (mSendResultCalled || mSendErrorCalled) {
633                throw new IllegalStateException("sendError() called when either sendResult() or "
634                        + "sendError() had already been called for: " + mDebug);
635            }
636            mSendErrorCalled = true;
637            onErrorSent(extras);
638        }
639
640        /**
641         * Detach this message from the current thread and allow the {@link #sendResult}
642         * call to happen later.
643         */
644        public void detach() {
645            if (mDetachCalled) {
646                throw new IllegalStateException("detach() called when detach() had already"
647                        + " been called for: " + mDebug);
648            }
649            if (mSendResultCalled) {
650                throw new IllegalStateException("detach() called when sendResult() had already"
651                        + " been called for: " + mDebug);
652            }
653            if (mSendErrorCalled) {
654                throw new IllegalStateException("detach() called when sendError() had already"
655                        + " been called for: " + mDebug);
656            }
657            mDetachCalled = true;
658        }
659
660        boolean isDone() {
661            return mDetachCalled || mSendResultCalled || mSendErrorCalled;
662        }
663
664        void setFlags(@ResultFlags int flags) {
665            mFlags = flags;
666        }
667
668        int getFlags() {
669            return mFlags;
670        }
671
672        /**
673         * Called when the result is sent, after assertions about not being called twice have
674         * happened.
675         */
676        void onResultSent(T result) {
677        }
678
679        /**
680         * Called when an interim update is sent.
681         */
682        void onProgressUpdateSent(Bundle extras) {
683            throw new UnsupportedOperationException("It is not supported to send an interim update "
684                    + "for " + mDebug);
685        }
686
687        /**
688         * Called when an error is sent, after assertions about not being called twice have
689         * happened.
690         */
691        void onErrorSent(Bundle extras) {
692            throw new UnsupportedOperationException("It is not supported to send an error for "
693                    + mDebug);
694        }
695
696        private void checkExtraFields(Bundle extras) {
697            if (extras == null) {
698                return;
699            }
700            if (extras.containsKey(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS)) {
701                float value = extras.getFloat(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS);
702                if (value < -EPSILON || value > 1.0f + EPSILON) {
703                    throw new IllegalArgumentException("The value of the EXTRA_DOWNLOAD_PROGRESS "
704                            + "field must be a float number within [0.0, 1.0].");
705                }
706            }
707        }
708    }
709
710    private class ServiceBinderImpl {
711        ServiceBinderImpl() {
712        }
713
714        public void connect(final String pkg, final int uid, final Bundle rootHints,
715                final ServiceCallbacks callbacks) {
716
717            if (!isValidPackage(pkg, uid)) {
718                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
719                        + " package=" + pkg);
720            }
721
722            mHandler.postOrRun(new Runnable() {
723                @Override
724                public void run() {
725                    final IBinder b = callbacks.asBinder();
726
727                    // Clear out the old subscriptions. We are getting new ones.
728                    mConnections.remove(b);
729
730                    final ConnectionRecord connection = new ConnectionRecord();
731                    connection.pkg = pkg;
732                    connection.rootHints = rootHints;
733                    connection.callbacks = callbacks;
734
735                    connection.root =
736                            MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
737
738                    // If they didn't return something, don't allow this client.
739                    if (connection.root == null) {
740                        Log.i(TAG, "No root for client " + pkg + " from service "
741                                + getClass().getName());
742                        try {
743                            callbacks.onConnectFailed();
744                        } catch (RemoteException ex) {
745                            Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
746                                    + "pkg=" + pkg);
747                        }
748                    } else {
749                        try {
750                            mConnections.put(b, connection);
751                            if (mSession != null) {
752                                callbacks.onConnect(connection.root.getRootId(),
753                                        mSession, connection.root.getExtras());
754                            }
755                        } catch (RemoteException ex) {
756                            Log.w(TAG, "Calling onConnect() failed. Dropping client. "
757                                    + "pkg=" + pkg);
758                            mConnections.remove(b);
759                        }
760                    }
761                }
762            });
763        }
764
765        public void disconnect(final ServiceCallbacks callbacks) {
766            mHandler.postOrRun(new Runnable() {
767                @Override
768                public void run() {
769                    final IBinder b = callbacks.asBinder();
770
771                    // Clear out the old subscriptions. We are getting new ones.
772                    final ConnectionRecord old = mConnections.remove(b);
773                    if (old != null) {
774                        // TODO
775                    }
776                }
777            });
778        }
779
780        public void addSubscription(final String id, final IBinder token, final Bundle options,
781                final ServiceCallbacks callbacks) {
782            mHandler.postOrRun(new Runnable() {
783                @Override
784                public void run() {
785                    final IBinder b = callbacks.asBinder();
786
787                    // Get the record for the connection
788                    final ConnectionRecord connection = mConnections.get(b);
789                    if (connection == null) {
790                        Log.w(TAG, "addSubscription for callback that isn't registered id="
791                                + id);
792                        return;
793                    }
794
795                    MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options);
796                }
797            });
798        }
799
800        public void removeSubscription(final String id, final IBinder token,
801                final ServiceCallbacks callbacks) {
802            mHandler.postOrRun(new Runnable() {
803                @Override
804                public void run() {
805                    final IBinder b = callbacks.asBinder();
806
807                    ConnectionRecord connection = mConnections.get(b);
808                    if (connection == null) {
809                        Log.w(TAG, "removeSubscription for callback that isn't registered id="
810                                + id);
811                        return;
812                    }
813                    if (!MediaBrowserServiceCompat.this.removeSubscription(
814                            id, connection, token)) {
815                        Log.w(TAG, "removeSubscription called for " + id
816                                + " which is not subscribed");
817                    }
818                }
819            });
820        }
821
822        public void getMediaItem(final String mediaId, final ResultReceiver receiver,
823                final ServiceCallbacks callbacks) {
824            if (TextUtils.isEmpty(mediaId) || receiver == null) {
825                return;
826            }
827
828            mHandler.postOrRun(new Runnable() {
829                @Override
830                public void run() {
831                    final IBinder b = callbacks.asBinder();
832
833                    ConnectionRecord connection = mConnections.get(b);
834                    if (connection == null) {
835                        Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
836                        return;
837                    }
838                    performLoadItem(mediaId, connection, receiver);
839                }
840            });
841        }
842
843        // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
844        public void registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints) {
845            mHandler.postOrRun(new Runnable() {
846                @Override
847                public void run() {
848                    final IBinder b = callbacks.asBinder();
849                    // Clear out the old subscriptions. We are getting new ones.
850                    mConnections.remove(b);
851
852                    final ConnectionRecord connection = new ConnectionRecord();
853                    connection.callbacks = callbacks;
854                    connection.rootHints = rootHints;
855                    mConnections.put(b, connection);
856                }
857            });
858        }
859
860        // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
861        public void unregisterCallbacks(final ServiceCallbacks callbacks) {
862            mHandler.postOrRun(new Runnable() {
863                @Override
864                public void run() {
865                    final IBinder b = callbacks.asBinder();
866                    mConnections.remove(b);
867                }
868            });
869        }
870
871        public void search(final String query, final Bundle extras, final ResultReceiver receiver,
872                final ServiceCallbacks callbacks) {
873            if (TextUtils.isEmpty(query) || receiver == null) {
874                return;
875            }
876
877            mHandler.postOrRun(new Runnable() {
878                @Override
879                public void run() {
880                    final IBinder b = callbacks.asBinder();
881
882                    ConnectionRecord connection = mConnections.get(b);
883                    if (connection == null) {
884                        Log.w(TAG, "search for callback that isn't registered query=" + query);
885                        return;
886                    }
887                    performSearch(query, extras, connection, receiver);
888                }
889            });
890        }
891
892        public void sendCustomAction(final String action, final Bundle extras,
893                final ResultReceiver receiver, final ServiceCallbacks callbacks) {
894            if (TextUtils.isEmpty(action) || receiver == null) {
895                return;
896            }
897
898            mHandler.postOrRun(new Runnable() {
899                @Override
900                public void run() {
901                    final IBinder b = callbacks.asBinder();
902
903                    ConnectionRecord connection = mConnections.get(b);
904                    if (connection == null) {
905                        Log.w(TAG, "sendCustomAction for callback that isn't registered action="
906                                + action + ", extras=" + extras);
907                        return;
908                    }
909                    performCustomAction(action, extras, connection, receiver);
910                }
911            });
912        }
913    }
914
915    private interface ServiceCallbacks {
916        IBinder asBinder();
917        void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
918                throws RemoteException;
919        void onConnectFailed() throws RemoteException;
920        void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
921                throws RemoteException;
922    }
923
924    private static class ServiceCallbacksCompat implements ServiceCallbacks {
925        final Messenger mCallbacks;
926
927        ServiceCallbacksCompat(Messenger callbacks) {
928            mCallbacks = callbacks;
929        }
930
931        @Override
932        public IBinder asBinder() {
933            return mCallbacks.getBinder();
934        }
935
936        @Override
937        public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
938                throws RemoteException {
939            if (extras == null) {
940                extras = new Bundle();
941            }
942            extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
943            Bundle data = new Bundle();
944            data.putString(DATA_MEDIA_ITEM_ID, root);
945            data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session);
946            data.putBundle(DATA_ROOT_HINTS, extras);
947            sendRequest(SERVICE_MSG_ON_CONNECT, data);
948        }
949
950        @Override
951        public void onConnectFailed() throws RemoteException {
952            sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
953        }
954
955        @Override
956        public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
957                Bundle options) throws RemoteException {
958            Bundle data = new Bundle();
959            data.putString(DATA_MEDIA_ITEM_ID, mediaId);
960            data.putBundle(DATA_OPTIONS, options);
961            if (list != null) {
962                data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
963                        list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
964            }
965            sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data);
966        }
967
968        private void sendRequest(int what, Bundle data) throws RemoteException {
969            Message msg = Message.obtain();
970            msg.what = what;
971            msg.arg1 = SERVICE_VERSION_CURRENT;
972            msg.setData(data);
973            mCallbacks.send(msg);
974        }
975    }
976
977    @Override
978    public void onCreate() {
979        super.onCreate();
980        if (BuildCompat.isAtLeastO()) {
981            mImpl = new MediaBrowserServiceImplApi24();
982        } else if (Build.VERSION.SDK_INT >= 23) {
983            mImpl = new MediaBrowserServiceImplApi23();
984        } else if (Build.VERSION.SDK_INT >= 21) {
985            mImpl = new MediaBrowserServiceImplApi21();
986        } else {
987            mImpl = new MediaBrowserServiceImplBase();
988        }
989        mImpl.onCreate();
990    }
991
992    @Override
993    public IBinder onBind(Intent intent) {
994        return mImpl.onBind(intent);
995    }
996
997    @Override
998    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
999    }
1000
1001    /**
1002     * Called to get the root information for browsing by a particular client.
1003     * <p>
1004     * The implementation should verify that the client package has permission
1005     * to access browse media information before returning the root id; it
1006     * should return null if the client is not allowed to access this
1007     * information.
1008     * </p>
1009     *
1010     * @param clientPackageName The package name of the application which is
1011     *            requesting access to browse media.
1012     * @param clientUid The uid of the application which is requesting access to
1013     *            browse media.
1014     * @param rootHints An optional bundle of service-specific arguments to send
1015     *            to the media browse service when connecting and retrieving the
1016     *            root id for browsing, or null if none. The contents of this
1017     *            bundle may affect the information returned when browsing.
1018     * @return The {@link BrowserRoot} for accessing this app's content or null.
1019     * @see BrowserRoot#EXTRA_RECENT
1020     * @see BrowserRoot#EXTRA_OFFLINE
1021     * @see BrowserRoot#EXTRA_SUGGESTED
1022     */
1023    public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
1024            int clientUid, @Nullable Bundle rootHints);
1025
1026    /**
1027     * Called to get information about the children of a media item.
1028     * <p>
1029     * Implementations must call {@link Result#sendResult result.sendResult}
1030     * with the list of children. If loading the children will be an expensive
1031     * operation that should be performed on another thread,
1032     * {@link Result#detach result.detach} may be called before returning from
1033     * this function, and then {@link Result#sendResult result.sendResult}
1034     * called when the loading is complete.
1035     * </p><p>
1036     * In case the media item does not have any children, call {@link Result#sendResult}
1037     * with an empty list. When the given {@code parentId} is invalid, implementations must
1038     * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
1039     * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
1040     * </p>
1041     *
1042     * @param parentId The id of the parent media item whose children are to be
1043     *            queried.
1044     * @param result The Result to send the list of children to.
1045     */
1046    public abstract void onLoadChildren(@NonNull String parentId,
1047            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
1048
1049    /**
1050     * Called to get information about the children of a media item.
1051     * <p>
1052     * Implementations must call {@link Result#sendResult result.sendResult}
1053     * with the list of children. If loading the children will be an expensive
1054     * operation that should be performed on another thread,
1055     * {@link Result#detach result.detach} may be called before returning from
1056     * this function, and then {@link Result#sendResult result.sendResult}
1057     * called when the loading is complete.
1058     * </p><p>
1059     * In case the media item does not have any children, call {@link Result#sendResult}
1060     * with an empty list. When the given {@code parentId} is invalid, implementations must
1061     * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
1062     * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
1063     * </p>
1064     *
1065     * @param parentId The id of the parent media item whose children are to be
1066     *            queried.
1067     * @param result The Result to send the list of children to.
1068     * @param options A bundle of service-specific arguments sent from the media
1069     *            browse. The information returned through the result should be
1070     *            affected by the contents of this bundle.
1071     */
1072    public void onLoadChildren(@NonNull String parentId,
1073            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) {
1074        // To support backward compatibility, when the implementation of MediaBrowserService doesn't
1075        // override onLoadChildren() with options, onLoadChildren() without options will be used
1076        // instead, and the options will be applied in the implementation of result.onResultSent().
1077        result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
1078        onLoadChildren(parentId, result);
1079    }
1080
1081    /**
1082     * Called to get information about a specific media item.
1083     * <p>
1084     * Implementations must call {@link Result#sendResult result.sendResult}. If
1085     * loading the item will be an expensive operation {@link Result#detach
1086     * result.detach} may be called before returning from this function, and
1087     * then {@link Result#sendResult result.sendResult} called when the item has
1088     * been loaded.
1089     * </p><p>
1090     * When the given {@code itemId} is invalid, implementations must call
1091     * {@link Result#sendResult result.sendResult} with {@code null}.
1092     * </p><p>
1093     * The default implementation will invoke {@link MediaBrowserCompat.ItemCallback#onError}.
1094     *
1095     * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}.
1096     * @param result The Result to send the item to, or null if the id is
1097     *            invalid.
1098     */
1099    public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) {
1100        result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
1101        result.sendResult(null);
1102    }
1103
1104    /**
1105     * Called to get the search result.
1106     * <p>
1107     * Implementations must call {@link Result#sendResult result.sendResult}. If the search will be
1108     * an expensive operation {@link Result#detach result.detach} may be called before returning
1109     * from this function, and then {@link Result#sendResult result.sendResult} called when the
1110     * search has been completed.
1111     * </p><p>
1112     * In case there are no search results, call {@link Result#sendResult result.sendResult} with an
1113     * empty list. In case there are some errors happened, call {@link Result#sendResult
1114     * result.sendResult} with {@code null}, which will invoke {@link
1115     * MediaBrowserCompat.SearchCallback#onError}.
1116     * </p><p>
1117     * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}.
1118     * </p>
1119     *
1120     * @param query The search query sent from the media browser. It contains keywords separated
1121     *            by space.
1122     * @param extras The bundle of service-specific arguments sent from the media browser.
1123     * @param result The {@link Result} to send the search result.
1124     */
1125    public void onSearch(@NonNull String query, Bundle extras,
1126            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
1127        result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED);
1128        result.sendResult(null);
1129    }
1130
1131    /**
1132     * Called to request a custom action to this service.
1133     * <p>
1134     * Implementations must call either {@link Result#sendResult} or {@link Result#sendError}. If
1135     * the requested custom action will be an expensive operation {@link Result#detach} may be
1136     * called before returning from this function, and then the service can send the result later
1137     * when the custom action is completed. Implementation can also call
1138     * {@link Result#sendProgressUpdate} to send an interim update to the requester.
1139     * </p><p>
1140     * If the requested custom action is not supported by this service, call
1141     * {@link Result#sendError}. The default implementation will invoke {@link Result#sendError}.
1142     * </p>
1143     *
1144     * @param action The custom action sent from the media browser.
1145     * @param extras The bundle of service-specific arguments sent from the media browser.
1146     * @param result The {@link Result} to send the result of the requested custom action.
1147     * @see MediaBrowserCompat#CUSTOM_ACTION_DOWNLOAD
1148     * @see MediaBrowserCompat#CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
1149     */
1150    public void onCustomAction(@NonNull String action, Bundle extras,
1151            @NonNull Result<Bundle> result) {
1152        result.sendError(null);
1153    }
1154
1155    /**
1156     * Call to set the media session.
1157     * <p>
1158     * This should be called as soon as possible during the service's startup.
1159     * It may only be called once.
1160     *
1161     * @param token The token for the service's {@link MediaSessionCompat}.
1162     */
1163    public void setSessionToken(MediaSessionCompat.Token token) {
1164        if (token == null) {
1165            throw new IllegalArgumentException("Session token may not be null.");
1166        }
1167        if (mSession != null) {
1168            throw new IllegalStateException("The session token has already been set.");
1169        }
1170        mSession = token;
1171        mImpl.setSessionToken(token);
1172    }
1173
1174    /**
1175     * Gets the session token, or null if it has not yet been created
1176     * or if it has been destroyed.
1177     */
1178    public @Nullable MediaSessionCompat.Token getSessionToken() {
1179        return mSession;
1180    }
1181
1182    /**
1183     * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}.
1184     * The root hints are service-specific arguments included in an optional bundle sent to the
1185     * media browser service when connecting and retrieving the root id for browsing, or null if
1186     * none. The contents of this bundle may affect the information returned when browsing.
1187     * <p>
1188     * Note that this will return null when connected to {@link android.media.browse.MediaBrowser}
1189     * and running on API 23 or lower.
1190     *
1191     * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren},
1192     *             {@link #onLoadItem} or {@link #onSearch}.
1193     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
1194     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
1195     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
1196     */
1197    public final Bundle getBrowserRootHints() {
1198        return mImpl.getBrowserRootHints();
1199    }
1200
1201    /**
1202     * Notifies all connected media browsers that the children of
1203     * the specified parent id have changed in some way.
1204     * This will cause browsers to fetch subscribed content again.
1205     *
1206     * @param parentId The id of the parent media item whose
1207     * children changed.
1208     */
1209    public void notifyChildrenChanged(@NonNull String parentId) {
1210        if (parentId == null) {
1211            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
1212        }
1213        mImpl.notifyChildrenChanged(parentId, null);
1214    }
1215
1216    /**
1217     * Notifies all connected media browsers that the children of
1218     * the specified parent id have changed in some way.
1219     * This will cause browsers to fetch subscribed content again.
1220     *
1221     * @param parentId The id of the parent media item whose
1222     *            children changed.
1223     * @param options A bundle of service-specific arguments to send
1224     *            to the media browse. The contents of this bundle may
1225     *            contain the information about the change.
1226     */
1227    public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
1228        if (parentId == null) {
1229            throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
1230        }
1231        if (options == null) {
1232            throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
1233        }
1234        mImpl.notifyChildrenChanged(parentId, options);
1235    }
1236
1237    /**
1238     * Return whether the given package is one of the ones that is owned by the uid.
1239     */
1240    boolean isValidPackage(String pkg, int uid) {
1241        if (pkg == null) {
1242            return false;
1243        }
1244        final PackageManager pm = getPackageManager();
1245        final String[] packages = pm.getPackagesForUid(uid);
1246        final int N = packages.length;
1247        for (int i=0; i<N; i++) {
1248            if (packages[i].equals(pkg)) {
1249                return true;
1250            }
1251        }
1252        return false;
1253    }
1254
1255    /**
1256     * Save the subscription and if it is a new subscription send the results.
1257     */
1258    void addSubscription(String id, ConnectionRecord connection, IBinder token,
1259            Bundle options) {
1260        // Save the subscription
1261        List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
1262        if (callbackList == null) {
1263            callbackList = new ArrayList<>();
1264        }
1265        for (Pair<IBinder, Bundle> callback : callbackList) {
1266            if (token == callback.first
1267                    && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) {
1268                return;
1269            }
1270        }
1271        callbackList.add(new Pair<>(token, options));
1272        connection.subscriptions.put(id, callbackList);
1273        // send the results
1274        performLoadChildren(id, connection, options);
1275    }
1276
1277    /**
1278     * Remove the subscription.
1279     */
1280    boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
1281        if (token == null) {
1282            return connection.subscriptions.remove(id) != null;
1283        }
1284        boolean removed = false;
1285        List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
1286        if (callbackList != null) {
1287            Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
1288            while (iter.hasNext()){
1289                if (token == iter.next().first) {
1290                    removed = true;
1291                    iter.remove();
1292                }
1293            }
1294            if (callbackList.size() == 0) {
1295                connection.subscriptions.remove(id);
1296            }
1297        }
1298        return removed;
1299    }
1300
1301    /**
1302     * Call onLoadChildren and then send the results back to the connection.
1303     * <p>
1304     * Callers must make sure that this connection is still connected.
1305     */
1306    void performLoadChildren(final String parentId, final ConnectionRecord connection,
1307            final Bundle options) {
1308        final Result<List<MediaBrowserCompat.MediaItem>> result
1309                = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
1310            @Override
1311            void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
1312                if (mConnections.get(connection.callbacks.asBinder()) != connection) {
1313                    if (DEBUG) {
1314                        Log.d(TAG, "Not sending onLoadChildren result for connection that has"
1315                                + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
1316                    }
1317                    return;
1318                }
1319
1320                List<MediaBrowserCompat.MediaItem> filteredList =
1321                        (getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
1322                                ? applyOptions(list, options) : list;
1323                try {
1324                    connection.callbacks.onLoadChildren(parentId, filteredList, options);
1325                } catch (RemoteException ex) {
1326                    // The other side is in the process of crashing.
1327                    Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
1328                            + " package=" + connection.pkg);
1329                }
1330            }
1331        };
1332
1333        mCurConnection = connection;
1334        if (options == null) {
1335            onLoadChildren(parentId, result);
1336        } else {
1337            onLoadChildren(parentId, result, options);
1338        }
1339        mCurConnection = null;
1340
1341        if (!result.isDone()) {
1342            throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
1343                    + " before returning for package=" + connection.pkg + " id=" + parentId);
1344        }
1345    }
1346
1347    List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
1348            final Bundle options) {
1349        if (list == null) {
1350            return null;
1351        }
1352        int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
1353        int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
1354        if (page == -1 && pageSize == -1) {
1355            return list;
1356        }
1357        int fromIndex = pageSize * page;
1358        int toIndex = fromIndex + pageSize;
1359        if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
1360            return Collections.EMPTY_LIST;
1361        }
1362        if (toIndex > list.size()) {
1363            toIndex = list.size();
1364        }
1365        return list.subList(fromIndex, toIndex);
1366    }
1367
1368    void performLoadItem(String itemId, ConnectionRecord connection,
1369            final ResultReceiver receiver) {
1370        final Result<MediaBrowserCompat.MediaItem> result =
1371                new Result<MediaBrowserCompat.MediaItem>(itemId) {
1372                    @Override
1373                    void onResultSent(MediaBrowserCompat.MediaItem item) {
1374                        if ((getFlags() & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
1375                            receiver.send(RESULT_ERROR, null);
1376                            return;
1377                        }
1378                        Bundle bundle = new Bundle();
1379                        bundle.putParcelable(KEY_MEDIA_ITEM, item);
1380                        receiver.send(RESULT_OK, bundle);
1381                    }
1382                };
1383
1384        mCurConnection = connection;
1385        onLoadItem(itemId, result);
1386        mCurConnection = null;
1387
1388        if (!result.isDone()) {
1389            throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
1390                    + " before returning for id=" + itemId);
1391        }
1392    }
1393
1394    void performSearch(final String query, Bundle extras, ConnectionRecord connection,
1395            final ResultReceiver receiver) {
1396        final Result<List<MediaBrowserCompat.MediaItem>> result =
1397                new Result<List<MediaBrowserCompat.MediaItem>>(query) {
1398            @Override
1399            void onResultSent(List<MediaBrowserCompat.MediaItem> items) {
1400                if ((getFlags() & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0
1401                        || items == null) {
1402                    receiver.send(RESULT_ERROR, null);
1403                    return;
1404                }
1405                Bundle bundle = new Bundle();
1406                bundle.putParcelableArray(KEY_SEARCH_RESULTS,
1407                        items.toArray(new MediaBrowserCompat.MediaItem[0]));
1408                receiver.send(RESULT_OK, bundle);
1409            }
1410        };
1411
1412        mCurConnection = connection;
1413        onSearch(query, extras, result);
1414        mCurConnection = null;
1415
1416        if (!result.isDone()) {
1417            throw new IllegalStateException("onSearch must call detach() or sendResult()"
1418                    + " before returning for query=" + query);
1419        }
1420    }
1421
1422    void performCustomAction(final String action, Bundle extras, ConnectionRecord connection,
1423            final ResultReceiver receiver) {
1424        final Result<Bundle> result = new Result<Bundle>(action) {
1425                @Override
1426                void onResultSent(Bundle result) {
1427                    receiver.send(RESULT_OK, result);
1428                }
1429
1430                @Override
1431                void onProgressUpdateSent(Bundle data) {
1432                    receiver.send(RESULT_PROGRESS_UPDATE, data);
1433                }
1434
1435                @Override
1436                void onErrorSent(Bundle data) {
1437                    receiver.send(RESULT_ERROR, data);
1438                }
1439            };
1440
1441        mCurConnection = connection;
1442        onCustomAction(action, extras, result);
1443        mCurConnection = null;
1444
1445        if (!result.isDone()) {
1446            throw new IllegalStateException("onCustomAction must call detach() or sendResult() or "
1447                    + "sendError() before returning for action=" + action + " extras="
1448                    + extras);
1449        }
1450    }
1451
1452    /**
1453     * Contains information that the browser service needs to send to the client
1454     * when first connected.
1455     */
1456    public static final class BrowserRoot {
1457        /**
1458         * The lookup key for a boolean that indicates whether the browser service should return a
1459         * browser root for recently played media items.
1460         *
1461         * <p>When creating a media browser for a given media browser service, this key can be
1462         * supplied as a root hint for retrieving media items that are recently played.
1463         * If the media browser service can provide such media items, the implementation must return
1464         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1465         *
1466         * <p>The root hint may contain multiple keys.
1467         *
1468         * @see #EXTRA_OFFLINE
1469         * @see #EXTRA_SUGGESTED
1470         */
1471        public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
1472
1473        /**
1474         * The lookup key for a boolean that indicates whether the browser service should return a
1475         * browser root for offline media items.
1476         *
1477         * <p>When creating a media browser for a given media browser service, this key can be
1478         * supplied as a root hint for retrieving media items that are can be played without an
1479         * internet connection.
1480         * If the media browser service can provide such media items, the implementation must return
1481         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1482         *
1483         * <p>The root hint may contain multiple keys.
1484         *
1485         * @see #EXTRA_RECENT
1486         * @see #EXTRA_SUGGESTED
1487         */
1488        public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
1489
1490        /**
1491         * The lookup key for a boolean that indicates whether the browser service should return a
1492         * browser root for suggested media items.
1493         *
1494         * <p>When creating a media browser for a given media browser service, this key can be
1495         * supplied as a root hint for retrieving the media items suggested by the media browser
1496         * service. The list of media items passed in {@link android.support.v4.media.MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)}
1497         * is considered ordered by relevance, first being the top suggestion.
1498         * If the media browser service can provide such media items, the implementation must return
1499         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1500         *
1501         * <p>The root hint may contain multiple keys.
1502         *
1503         * @see #EXTRA_RECENT
1504         * @see #EXTRA_OFFLINE
1505         */
1506        public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
1507
1508        /**
1509         * The lookup key for a string that indicates specific keywords which will be considered
1510         * when the browser service suggests media items.
1511         *
1512         * <p>When creating a media browser for a given media browser service, this key can be
1513         * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested
1514         * media items related with the keywords. The list of media items passed in
1515         * {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
1516         * is considered ordered by relevance, first being the top suggestion.
1517         * If the media browser service can provide such media items, the implementation must return
1518         * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1519         *
1520         * <p>The root hint may contain multiple keys.
1521         *
1522         * @see #EXTRA_RECENT
1523         * @see #EXTRA_OFFLINE
1524         * @see #EXTRA_SUGGESTED
1525         * @deprecated The search functionality is now supported by the methods
1526         *             {@link MediaBrowserCompat#search} and {@link #onSearch}. Use those methods
1527         *             instead.
1528         */
1529        @Deprecated
1530        public static final String EXTRA_SUGGESTION_KEYWORDS
1531                = "android.service.media.extra.SUGGESTION_KEYWORDS";
1532
1533        final private String mRootId;
1534        final private Bundle mExtras;
1535
1536        /**
1537         * Constructs a browser root.
1538         * @param rootId The root id for browsing.
1539         * @param extras Any extras about the browser service.
1540         */
1541        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
1542            if (rootId == null) {
1543                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
1544                        "Use null for BrowserRoot instead.");
1545            }
1546            mRootId = rootId;
1547            mExtras = extras;
1548        }
1549
1550        /**
1551         * Gets the root id for browsing.
1552         */
1553        public String getRootId() {
1554            return mRootId;
1555        }
1556
1557        /**
1558         * Gets any extras about the browser service.
1559         */
1560        public Bundle getExtras() {
1561            return mExtras;
1562        }
1563    }
1564}
1565