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