1 /*
2  * Copyright (C) 2014 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.service.media;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SdkConstant;
23 import android.annotation.SdkConstant.SdkConstantType;
24 import android.app.Service;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.content.Intent;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ParceledListSlice;
29 import android.media.browse.MediaBrowser;
30 import android.media.browse.MediaBrowserUtils;
31 import android.media.session.MediaSession;
32 import android.media.session.MediaSessionManager;
33 import android.media.session.MediaSessionManager.RemoteUserInfo;
34 import android.os.Binder;
35 import android.os.Build;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.RemoteException;
40 import android.os.ResultReceiver;
41 import android.util.ArrayMap;
42 import android.util.Log;
43 import android.util.Pair;
44 
45 import java.io.FileDescriptor;
46 import java.io.PrintWriter;
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.lang.ref.WeakReference;
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.Iterator;
54 import java.util.List;
55 
56 /**
57  * Base class for media browser services.
58  * <p>
59  * Media browser services enable applications to browse media content provided by an application
60  * and ask the application to start playing it. They may also be used to control content that
61  * is already playing by way of a {@link MediaSession}.
62  * </p>
63  *
64  * To extend this class, you must declare the service in your manifest file with
65  * an intent filter with the {@link #SERVICE_INTERFACE} action.
66  *
67  * For example:
68  * </p><pre>
69  * &lt;service android:name=".MyMediaBrowserService"
70  *          android:label="&#64;string/service_name" >
71  *     &lt;intent-filter>
72  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
73  *     &lt;/intent-filter>
74  * &lt;/service>
75  * </pre>
76  *
77  */
78 public abstract class MediaBrowserService extends Service {
79     private static final String TAG = "MediaBrowserService";
80     private static final boolean DBG = false;
81 
82     /**
83      * The {@link Intent} that must be declared as handled by the service.
84      */
85     @SdkConstant(SdkConstantType.SERVICE_ACTION)
86     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
87 
88     /**
89      * A key for passing the MediaItem to the ResultReceiver in getItem.
90      * @hide
91      */
92     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
93     public static final String KEY_MEDIA_ITEM = "media_item";
94 
95     private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
96     private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
97 
98     private static final int RESULT_ERROR = -1;
99     private static final int RESULT_OK = 0;
100 
101     /** @hide */
102     @Retention(RetentionPolicy.SOURCE)
103     @IntDef(flag = true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
104             RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
105     private @interface ResultFlags { }
106 
107     private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
108     private ConnectionRecord mCurConnection;
109     private final Handler mHandler = new Handler();
110     private ServiceBinder mBinder;
111     MediaSession.Token mSession;
112 
113     /**
114      * All the info about a connection.
115      */
116     private static class ConnectionRecord implements IBinder.DeathRecipient {
117         public final MediaBrowserService service;
118         public final String pkg;
119         public final int pid;
120         public final int uid;
121         public final Bundle rootHints;
122         public final IMediaBrowserServiceCallbacks callbacks;
123         public final BrowserRoot root;
124         public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
125 
ConnectionRecord( MediaBrowserService service, String pkg, int pid, int uid, Bundle rootHints, IMediaBrowserServiceCallbacks callbacks, BrowserRoot root)126         ConnectionRecord(
127                 MediaBrowserService service, String pkg, int pid, int uid, Bundle rootHints,
128                 IMediaBrowserServiceCallbacks callbacks, BrowserRoot root) {
129             this.service = service;
130             this.pkg = pkg;
131             this.pid = pid;
132             this.uid = uid;
133             this.rootHints = rootHints;
134             this.callbacks = callbacks;
135             this.root = root;
136         }
137 
138         @Override
binderDied()139         public void binderDied() {
140             service.mHandler.post(new Runnable() {
141                 @Override
142                 public void run() {
143                     service.mConnections.remove(callbacks.asBinder());
144                 }
145             });
146         }
147     }
148 
149     /**
150      * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
151      * <p>
152      * Each of the methods that takes one of these to send the result must call
153      * {@link #sendResult} to respond to the caller with the given results. If those
154      * functions return without calling {@link #sendResult}, they must instead call
155      * {@link #detach} before returning, and then may call {@link #sendResult} when
156      * they are done. If more than one of those methods is called, an exception will
157      * be thrown.
158      *
159      * @see #onLoadChildren
160      * @see #onLoadItem
161      */
162     public class Result<T> {
163         private Object mDebug;
164         private boolean mDetachCalled;
165         private boolean mSendResultCalled;
166         @UnsupportedAppUsage
167         private int mFlags;
168 
Result(Object debug)169         Result(Object debug) {
170             mDebug = debug;
171         }
172 
173         /**
174          * Send the result back to the caller.
175          */
sendResult(T result)176         public void sendResult(T result) {
177             if (mSendResultCalled) {
178                 throw new IllegalStateException("sendResult() called twice for: " + mDebug);
179             }
180             mSendResultCalled = true;
181             onResultSent(result, mFlags);
182         }
183 
184         /**
185          * Detach this message from the current thread and allow the {@link #sendResult}
186          * call to happen later.
187          */
detach()188         public void detach() {
189             if (mDetachCalled) {
190                 throw new IllegalStateException("detach() called when detach() had already"
191                         + " been called for: " + mDebug);
192             }
193             if (mSendResultCalled) {
194                 throw new IllegalStateException("detach() called when sendResult() had already"
195                         + " been called for: " + mDebug);
196             }
197             mDetachCalled = true;
198         }
199 
isDone()200         boolean isDone() {
201             return mDetachCalled || mSendResultCalled;
202         }
203 
setFlags(@esultFlags int flags)204         void setFlags(@ResultFlags int flags) {
205             mFlags = flags;
206         }
207 
208         /**
209          * Called when the result is sent, after assertions about not being called twice
210          * have happened.
211          */
onResultSent(T result, @ResultFlags int flags)212         void onResultSent(T result, @ResultFlags int flags) {
213         }
214     }
215 
216     private static class ServiceBinder extends IMediaBrowserService.Stub {
217         private WeakReference<MediaBrowserService> mService;
218 
ServiceBinder(MediaBrowserService service)219         private ServiceBinder(MediaBrowserService service) {
220             mService = new WeakReference(service);
221         }
222 
223         @Override
connect(final String pkg, final Bundle rootHints, final IMediaBrowserServiceCallbacks callbacks)224         public void connect(final String pkg, final Bundle rootHints,
225                 final IMediaBrowserServiceCallbacks callbacks) {
226             MediaBrowserService service = mService.get();
227             if (service == null) {
228                 return;
229             }
230 
231             final int pid = Binder.getCallingPid();
232             final int uid = Binder.getCallingUid();
233             if (!service.isValidPackage(pkg, uid)) {
234                 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
235                         + " package=" + pkg);
236             }
237 
238             service.mHandler.post(new Runnable() {
239                     @Override
240                     public void run() {
241                         final IBinder b = callbacks.asBinder();
242 
243                         // Clear out the old subscriptions. We are getting new ones.
244                         service.mConnections.remove(b);
245 
246                         // Temporarily sets a placeholder ConnectionRecord to make
247                         // getCurrentBrowserInfo() work in onGetRoot().
248                         service.mCurConnection =
249                                 new ConnectionRecord(
250                                         service, pkg, pid, uid, rootHints, callbacks, null);
251                         BrowserRoot root = service.onGetRoot(pkg, uid, rootHints);
252                         service.mCurConnection = null;
253 
254                         // If they didn't return something, don't allow this client.
255                         if (root == null) {
256                             Log.i(TAG, "No root for client " + pkg + " from service "
257                                     + getClass().getName());
258                             try {
259                                 callbacks.onConnectFailed();
260                             } catch (RemoteException ex) {
261                                 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
262                                         + "pkg=" + pkg);
263                             }
264                         } else {
265                             try {
266                                 ConnectionRecord connection =
267                                         new ConnectionRecord(
268                                                 service, pkg, pid, uid, rootHints, callbacks, root);
269                                 service.mConnections.put(b, connection);
270                                 b.linkToDeath(connection, 0);
271                                 if (service.mSession != null) {
272                                     callbacks.onConnect(connection.root.getRootId(),
273                                             service.mSession, connection.root.getExtras());
274                                 }
275                             } catch (RemoteException ex) {
276                                 Log.w(TAG, "Calling onConnect() failed. Dropping client. "
277                                         + "pkg=" + pkg);
278                                 service.mConnections.remove(b);
279                             }
280                         }
281                     }
282                 });
283         }
284 
285         @Override
disconnect(final IMediaBrowserServiceCallbacks callbacks)286         public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
287             MediaBrowserService service = mService.get();
288             if (service == null) {
289                 return;
290             }
291 
292             service.mHandler.post(new Runnable() {
293                     @Override
294                     public void run() {
295                         final IBinder b = callbacks.asBinder();
296 
297                         // Clear out the old subscriptions. We are getting new ones.
298                         final ConnectionRecord old = service.mConnections.remove(b);
299                         if (old != null) {
300                             // TODO
301                             old.callbacks.asBinder().unlinkToDeath(old, 0);
302                         }
303                     }
304                 });
305         }
306 
307         @Override
addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks)308         public void addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) {
309             // do-nothing
310         }
311 
312         @Override
addSubscription(final String id, final IBinder token, final Bundle options, final IMediaBrowserServiceCallbacks callbacks)313         public void addSubscription(final String id, final IBinder token, final Bundle options,
314                 final IMediaBrowserServiceCallbacks callbacks) {
315             MediaBrowserService service = mService.get();
316             if (service == null) {
317                 return;
318             }
319 
320             service.mHandler.post(new Runnable() {
321                     @Override
322                     public void run() {
323                         final IBinder b = callbacks.asBinder();
324 
325                         // Get the record for the connection
326                         final ConnectionRecord connection = service.mConnections.get(b);
327                         if (connection == null) {
328                             Log.w(TAG, "addSubscription for callback that isn't registered id="
329                                     + id);
330                             return;
331                         }
332 
333                         service.addSubscription(id, connection, token, options);
334                     }
335                 });
336         }
337 
338         @Override
removeSubscriptionDeprecated( String id, IMediaBrowserServiceCallbacks callbacks)339         public void removeSubscriptionDeprecated(
340                 String id, IMediaBrowserServiceCallbacks callbacks) {
341             // do-nothing
342         }
343 
344         @Override
removeSubscription(final String id, final IBinder token, final IMediaBrowserServiceCallbacks callbacks)345         public void removeSubscription(final String id, final IBinder token,
346                 final IMediaBrowserServiceCallbacks callbacks) {
347             MediaBrowserService service = mService.get();
348             if (service == null) {
349                 return;
350             }
351 
352             service.mHandler.post(new Runnable() {
353                 @Override
354                 public void run() {
355                     final IBinder b = callbacks.asBinder();
356 
357                     ConnectionRecord connection = service.mConnections.get(b);
358                     if (connection == null) {
359                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
360                                 + id);
361                         return;
362                     }
363                     if (!service.removeSubscription(id, connection, token)) {
364                         Log.w(TAG, "removeSubscription called for " + id
365                                 + " which is not subscribed");
366                     }
367                 }
368             });
369         }
370 
371         @Override
getMediaItem(final String mediaId, final ResultReceiver receiver, final IMediaBrowserServiceCallbacks callbacks)372         public void getMediaItem(final String mediaId, final ResultReceiver receiver,
373                 final IMediaBrowserServiceCallbacks callbacks) {
374             MediaBrowserService service = mService.get();
375             if (service == null) {
376                 return;
377             }
378 
379             service.mHandler.post(new Runnable() {
380                 @Override
381                 public void run() {
382                     final IBinder b = callbacks.asBinder();
383                     ConnectionRecord connection = service.mConnections.get(b);
384                     if (connection == null) {
385                         Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
386                         return;
387                     }
388                     service.performLoadItem(mediaId, connection, receiver);
389                 }
390             });
391         }
392     }
393 
394     @Override
onCreate()395     public void onCreate() {
396         super.onCreate();
397         mBinder = new ServiceBinder(this);
398     }
399 
400     @Override
onBind(Intent intent)401     public IBinder onBind(Intent intent) {
402         if (SERVICE_INTERFACE.equals(intent.getAction())) {
403             return mBinder;
404         }
405         return null;
406     }
407 
408     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)409     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
410     }
411 
412     /**
413      * Called to get the root information for browsing by a particular client.
414      * <p>
415      * The implementation should verify that the client package has permission
416      * to access browse media information before returning the root id; it
417      * should return null if the client is not allowed to access this
418      * information.
419      * </p>
420      *
421      * @param clientPackageName The package name of the application which is
422      *            requesting access to browse media.
423      * @param clientUid The uid of the application which is requesting access to
424      *            browse media.
425      * @param rootHints An optional bundle of service-specific arguments to send
426      *            to the media browser service when connecting and retrieving the
427      *            root id for browsing, or null if none. The contents of this
428      *            bundle may affect the information returned when browsing.
429      * @return The {@link BrowserRoot} for accessing this app's content or null.
430      * @see BrowserRoot#EXTRA_RECENT
431      * @see BrowserRoot#EXTRA_OFFLINE
432      * @see BrowserRoot#EXTRA_SUGGESTED
433      */
onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)434     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
435             int clientUid, @Nullable Bundle rootHints);
436 
437     /**
438      * Called to get information about the children of a media item.
439      * <p>
440      * Implementations must call {@link Result#sendResult result.sendResult}
441      * with the list of children. If loading the children will be an expensive
442      * operation that should be performed on another thread,
443      * {@link Result#detach result.detach} may be called before returning from
444      * this function, and then {@link Result#sendResult result.sendResult}
445      * called when the loading is complete.
446      * </p><p>
447      * In case the media item does not have any children, call {@link Result#sendResult}
448      * with an empty list. When the given {@code parentId} is invalid, implementations must
449      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
450      * {@link MediaBrowser.SubscriptionCallback#onError}.
451      * </p>
452      *
453      * @param parentId The id of the parent media item whose children are to be
454      *            queried.
455      * @param result The Result to send the list of children to.
456      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result)457     public abstract void onLoadChildren(@NonNull String parentId,
458             @NonNull Result<List<MediaBrowser.MediaItem>> result);
459 
460     /**
461      * Called to get information about the children of a media item.
462      * <p>
463      * Implementations must call {@link Result#sendResult result.sendResult}
464      * with the list of children. If loading the children will be an expensive
465      * operation that should be performed on another thread,
466      * {@link Result#detach result.detach} may be called before returning from
467      * this function, and then {@link Result#sendResult result.sendResult}
468      * called when the loading is complete.
469      * </p><p>
470      * In case the media item does not have any children, call {@link Result#sendResult}
471      * with an empty list. When the given {@code parentId} is invalid, implementations must
472      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
473      * {@link MediaBrowser.SubscriptionCallback#onError}.
474      * </p>
475      *
476      * @param parentId The id of the parent media item whose children are to be
477      *            queried.
478      * @param result The Result to send the list of children to.
479      * @param options The bundle of service-specific arguments sent from the media
480      *            browser. The information returned through the result should be
481      *            affected by the contents of this bundle.
482      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options)483     public void onLoadChildren(@NonNull String parentId,
484             @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) {
485         // To support backward compatibility, when the implementation of MediaBrowserService doesn't
486         // override onLoadChildren() with options, onLoadChildren() without options will be used
487         // instead, and the options will be applied in the implementation of result.onResultSent().
488         result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
489         onLoadChildren(parentId, result);
490     }
491 
492     /**
493      * Called to get information about a specific media item.
494      * <p>
495      * Implementations must call {@link Result#sendResult result.sendResult}. If
496      * loading the item will be an expensive operation {@link Result#detach
497      * result.detach} may be called before returning from this function, and
498      * then {@link Result#sendResult result.sendResult} called when the item has
499      * been loaded.
500      * </p><p>
501      * When the given {@code itemId} is invalid, implementations must call
502      * {@link Result#sendResult result.sendResult} with {@code null}.
503      * </p><p>
504      * The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}.
505      * </p>
506      *
507      * @param itemId The id for the specific
508      *            {@link android.media.browse.MediaBrowser.MediaItem}.
509      * @param result The Result to send the item to.
510      */
onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)511     public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
512         result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
513         result.sendResult(null);
514     }
515 
516     /**
517      * Call to set the media session.
518      * <p>
519      * This should be called as soon as possible during the service's startup.
520      * It may only be called once.
521      *
522      * @param token The token for the service's {@link MediaSession}.
523      */
setSessionToken(final MediaSession.Token token)524     public void setSessionToken(final MediaSession.Token token) {
525         if (token == null) {
526             throw new IllegalArgumentException("Session token may not be null.");
527         }
528         if (mSession != null) {
529             throw new IllegalStateException("The session token has already been set.");
530         }
531         mSession = token;
532         mHandler.post(new Runnable() {
533             @Override
534             public void run() {
535                 Iterator<ConnectionRecord> iter = mConnections.values().iterator();
536                 while (iter.hasNext()) {
537                     ConnectionRecord connection = iter.next();
538                     try {
539                         connection.callbacks.onConnect(connection.root.getRootId(), token,
540                                 connection.root.getExtras());
541                     } catch (RemoteException e) {
542                         Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
543                         iter.remove();
544                     }
545                 }
546             }
547         });
548     }
549 
550     /**
551      * Gets the session token, or null if it has not yet been created
552      * or if it has been destroyed.
553      */
getSessionToken()554     public @Nullable MediaSession.Token getSessionToken() {
555         return mSession;
556     }
557 
558     /**
559      * Gets the root hints sent from the currently connected {@link MediaBrowser}.
560      * The root hints are service-specific arguments included in an optional bundle sent to the
561      * media browser service when connecting and retrieving the root id for browsing, or null if
562      * none. The contents of this bundle may affect the information returned when browsing.
563      *
564      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
565      *             {@link #onLoadChildren} or {@link #onLoadItem}.
566      * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT
567      * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
568      * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
569      */
getBrowserRootHints()570     public final Bundle getBrowserRootHints() {
571         if (mCurConnection == null) {
572             throw new IllegalStateException("This should be called inside of onGetRoot or"
573                     + " onLoadChildren or onLoadItem methods");
574         }
575         return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
576     }
577 
578     /**
579      * Gets the browser information who sent the current request.
580      *
581      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
582      *             {@link #onLoadChildren} or {@link #onLoadItem}.
583      * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
584      */
getCurrentBrowserInfo()585     public final RemoteUserInfo getCurrentBrowserInfo() {
586         if (mCurConnection == null) {
587             throw new IllegalStateException("This should be called inside of onGetRoot or"
588                     + " onLoadChildren or onLoadItem methods");
589         }
590         return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid);
591     }
592 
593     /**
594      * Notifies all connected media browsers that the children of
595      * the specified parent id have changed in some way.
596      * This will cause browsers to fetch subscribed content again.
597      *
598      * @param parentId The id of the parent media item whose
599      * children changed.
600      */
notifyChildrenChanged(@onNull String parentId)601     public void notifyChildrenChanged(@NonNull String parentId) {
602         notifyChildrenChangedInternal(parentId, null);
603     }
604 
605     /**
606      * Notifies all connected media browsers that the children of
607      * the specified parent id have changed in some way.
608      * This will cause browsers to fetch subscribed content again.
609      *
610      * @param parentId The id of the parent media item whose
611      *            children changed.
612      * @param options The bundle of service-specific arguments to send
613      *            to the media browser. The contents of this bundle may
614      *            contain the information about the change.
615      */
notifyChildrenChanged(@onNull String parentId, @NonNull Bundle options)616     public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
617         if (options == null) {
618             throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
619         }
620         notifyChildrenChangedInternal(parentId, options);
621     }
622 
notifyChildrenChangedInternal(final String parentId, final Bundle options)623     private void notifyChildrenChangedInternal(final String parentId, final Bundle options) {
624         if (parentId == null) {
625             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
626         }
627         mHandler.post(new Runnable() {
628             @Override
629             public void run() {
630                 for (IBinder binder : mConnections.keySet()) {
631                     ConnectionRecord connection = mConnections.get(binder);
632                     List<Pair<IBinder, Bundle>> callbackList =
633                             connection.subscriptions.get(parentId);
634                     if (callbackList != null) {
635                         for (Pair<IBinder, Bundle> callback : callbackList) {
636                             if (MediaBrowserUtils.hasDuplicatedItems(options, callback.second)) {
637                                 performLoadChildren(parentId, connection, callback.second);
638                             }
639                         }
640                     }
641                 }
642             }
643         });
644     }
645 
646     /**
647      * Return whether the given package is one of the ones that is owned by the uid.
648      */
isValidPackage(String pkg, int uid)649     private boolean isValidPackage(String pkg, int uid) {
650         if (pkg == null) {
651             return false;
652         }
653         final PackageManager pm = getPackageManager();
654         final String[] packages = pm.getPackagesForUid(uid);
655         final int N = packages.length;
656         for (int i = 0; i < N; i++) {
657             if (packages[i].equals(pkg)) {
658                 return true;
659             }
660         }
661         return false;
662     }
663 
664     /**
665      * Save the subscription and if it is a new subscription send the results.
666      */
addSubscription(String id, ConnectionRecord connection, IBinder token, Bundle options)667     private void addSubscription(String id, ConnectionRecord connection, IBinder token,
668             Bundle options) {
669         // Save the subscription
670         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
671         if (callbackList == null) {
672             callbackList = new ArrayList<>();
673         }
674         for (Pair<IBinder, Bundle> callback : callbackList) {
675             if (token == callback.first
676                     && MediaBrowserUtils.areSameOptions(options, callback.second)) {
677                 return;
678             }
679         }
680         callbackList.add(new Pair<>(token, options));
681         connection.subscriptions.put(id, callbackList);
682         // send the results
683         performLoadChildren(id, connection, options);
684     }
685 
686     /**
687      * Remove the subscription.
688      */
removeSubscription(String id, ConnectionRecord connection, IBinder token)689     private boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
690         if (token == null) {
691             return connection.subscriptions.remove(id) != null;
692         }
693         boolean removed = false;
694         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
695         if (callbackList != null) {
696             Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
697             while (iter.hasNext()) {
698                 if (token == iter.next().first) {
699                     removed = true;
700                     iter.remove();
701                 }
702             }
703             if (callbackList.size() == 0) {
704                 connection.subscriptions.remove(id);
705             }
706         }
707         return removed;
708     }
709 
710     /**
711      * Call onLoadChildren and then send the results back to the connection.
712      * <p>
713      * Callers must make sure that this connection is still connected.
714      */
performLoadChildren(final String parentId, final ConnectionRecord connection, final Bundle options)715     private void performLoadChildren(final String parentId, final ConnectionRecord connection,
716             final Bundle options) {
717         final Result<List<MediaBrowser.MediaItem>> result =
718                 new Result<List<MediaBrowser.MediaItem>>(parentId) {
719             @Override
720             void onResultSent(List<MediaBrowser.MediaItem> list, @ResultFlags int flag) {
721                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
722                     if (DBG) {
723                         Log.d(TAG, "Not sending onLoadChildren result for connection that has"
724                                 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
725                     }
726                     return;
727                 }
728 
729                 List<MediaBrowser.MediaItem> filteredList =
730                         (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
731                                 ? applyOptions(list, options) : list;
732                 final ParceledListSlice<MediaBrowser.MediaItem> pls;
733                 if (filteredList == null) {
734                     pls = null;
735                 } else {
736                     pls = new ParceledListSlice<>(filteredList);
737                     // Limit the size of initial Parcel to prevent binder buffer overflow
738                     // as onLoadChildren is an async binder call.
739                     pls.setInlineCountLimit(1);
740                 }
741                 try {
742                     connection.callbacks.onLoadChildren(parentId, pls, options);
743                 } catch (RemoteException ex) {
744                     // The other side is in the process of crashing.
745                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
746                             + " package=" + connection.pkg);
747                 }
748             }
749         };
750 
751         mCurConnection = connection;
752         if (options == null) {
753             onLoadChildren(parentId, result);
754         } else {
755             onLoadChildren(parentId, result, options);
756         }
757         mCurConnection = null;
758 
759         if (!result.isDone()) {
760             throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
761                     + " before returning for package=" + connection.pkg + " id=" + parentId);
762         }
763     }
764 
applyOptions(List<MediaBrowser.MediaItem> list, final Bundle options)765     private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list,
766             final Bundle options) {
767         if (list == null) {
768             return null;
769         }
770         int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
771         int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
772         if (page == -1 && pageSize == -1) {
773             return list;
774         }
775         int fromIndex = pageSize * page;
776         int toIndex = fromIndex + pageSize;
777         if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
778             return Collections.EMPTY_LIST;
779         }
780         if (toIndex > list.size()) {
781             toIndex = list.size();
782         }
783         return list.subList(fromIndex, toIndex);
784     }
785 
performLoadItem(String itemId, final ConnectionRecord connection, final ResultReceiver receiver)786     private void performLoadItem(String itemId, final ConnectionRecord connection,
787             final ResultReceiver receiver) {
788         final Result<MediaBrowser.MediaItem> result =
789                 new Result<MediaBrowser.MediaItem>(itemId) {
790             @Override
791             void onResultSent(MediaBrowser.MediaItem item, @ResultFlags int flag) {
792                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
793                     if (DBG) {
794                         Log.d(TAG, "Not sending onLoadItem result for connection that has"
795                                 + " been disconnected. pkg=" + connection.pkg + " id=" + itemId);
796                     }
797                     return;
798                 }
799                 if ((flag & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
800                     receiver.send(RESULT_ERROR, null);
801                     return;
802                 }
803                 Bundle bundle = new Bundle();
804                 bundle.putParcelable(KEY_MEDIA_ITEM, item);
805                 receiver.send(RESULT_OK, bundle);
806             }
807         };
808 
809         mCurConnection = connection;
810         onLoadItem(itemId, result);
811         mCurConnection = null;
812 
813         if (!result.isDone()) {
814             throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
815                     + " before returning for id=" + itemId);
816         }
817     }
818 
819     /**
820      * Contains information that the browser service needs to send to the client
821      * when first connected.
822      */
823     public static final class BrowserRoot {
824         /**
825          * The lookup key for a boolean that indicates whether the browser service should return a
826          * browser root for recently played media items.
827          *
828          * <p>When creating a media browser for a given media browser service, this key can be
829          * supplied as a root hint for retrieving media items that are recently played.
830          * If the media browser service can provide such media items, the implementation must return
831          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
832          *
833          * <p>The root hint may contain multiple keys.
834          *
835          * @see #EXTRA_OFFLINE
836          * @see #EXTRA_SUGGESTED
837          */
838         public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
839 
840         /**
841          * The lookup key for a boolean that indicates whether the browser service should return a
842          * browser root for offline media items.
843          *
844          * <p>When creating a media browser for a given media browser service, this key can be
845          * supplied as a root hint for retrieving media items that are can be played without an
846          * internet connection.
847          * If the media browser service can provide such media items, the implementation must return
848          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
849          *
850          * <p>The root hint may contain multiple keys.
851          *
852          * @see #EXTRA_RECENT
853          * @see #EXTRA_SUGGESTED
854          */
855         public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
856 
857         /**
858          * The lookup key for a boolean that indicates whether the browser service should return a
859          * browser root for suggested media items.
860          *
861          * <p>When creating a media browser for a given media browser service, this key can be
862          * supplied as a root hint for retrieving the media items suggested by the media browser
863          * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
864          * is considered ordered by relevance, first being the top suggestion.
865          * If the media browser service can provide such media items, the implementation must return
866          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
867          *
868          * <p>The root hint may contain multiple keys.
869          *
870          * @see #EXTRA_RECENT
871          * @see #EXTRA_OFFLINE
872          */
873         public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
874 
875         private final String mRootId;
876         private final Bundle mExtras;
877 
878         /**
879          * Constructs a browser root.
880          * @param rootId The root id for browsing.
881          * @param extras Any extras about the browser service.
882          */
BrowserRoot(@onNull String rootId, @Nullable Bundle extras)883         public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
884             if (rootId == null) {
885                 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. "
886                         + "Use null for BrowserRoot instead.");
887             }
888             mRootId = rootId;
889             mExtras = extras;
890         }
891 
892         /**
893          * Gets the root id for browsing.
894          */
getRootId()895         public String getRootId() {
896             return mRootId;
897         }
898 
899         /**
900          * Gets any extras about the browser service.
901          */
getExtras()902         public Bundle getExtras() {
903             return mExtras;
904         }
905     }
906 }
907