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