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