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