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