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