| /* | |
| * Copyright (C) 2014 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package android.service.media; | |
| import android.annotation.IntDef; | |
| import android.annotation.NonNull; | |
| import android.annotation.Nullable; | |
| import android.annotation.SdkConstant; | |
| import android.annotation.SdkConstant.SdkConstantType; | |
| import android.app.Service; | |
| import android.compat.annotation.UnsupportedAppUsage; | |
| import android.content.Intent; | |
| import android.content.pm.PackageManager; | |
| import android.content.pm.ParceledListSlice; | |
| import android.media.browse.MediaBrowser; | |
| import android.media.browse.MediaBrowserUtils; | |
| import android.media.session.MediaSession; | |
| import android.media.session.MediaSessionManager; | |
| import android.media.session.MediaSessionManager.RemoteUserInfo; | |
| import android.os.Binder; | |
| import android.os.Build; | |
| import android.os.Bundle; | |
| import android.os.Handler; | |
| import android.os.IBinder; | |
| import android.os.RemoteException; | |
| import android.os.ResultReceiver; | |
| import android.util.ArrayMap; | |
| import android.util.Log; | |
| import android.util.Pair; | |
| import java.io.FileDescriptor; | |
| import java.io.PrintWriter; | |
| import java.lang.annotation.Retention; | |
| import java.lang.annotation.RetentionPolicy; | |
| import java.lang.ref.WeakReference; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.Iterator; | |
| import java.util.List; | |
| /** | |
| * Base class for media browser services. | |
| * <p> | |
| * Media browser services enable applications to browse media content provided by an application | |
| * and ask the application to start playing it. They may also be used to control content that | |
| * is already playing by way of a {@link MediaSession}. | |
| * </p> | |
| * | |
| * To extend this class, you must declare the service in your manifest file with | |
| * an intent filter with the {@link #SERVICE_INTERFACE} action. | |
| * | |
| * For example: | |
| * </p><pre> | |
| * <service android:name=".MyMediaBrowserService" | |
| * android:label="@string/service_name" > | |
| * <intent-filter> | |
| * <action android:name="android.media.browse.MediaBrowserService" /> | |
| * </intent-filter> | |
| * </service> | |
| * </pre> | |
| * | |
| */ | |
| public abstract class MediaBrowserService extends Service { | |
| private static final String TAG = "MediaBrowserService"; | |
| private static final boolean DBG = false; | |
| /** | |
| * The {@link Intent} that must be declared as handled by the service. | |
| */ | |
| @SdkConstant(SdkConstantType.SERVICE_ACTION) | |
| public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; | |
| /** | |
| * A key for passing the MediaItem to the ResultReceiver in getItem. | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) | |
| public static final String KEY_MEDIA_ITEM = "media_item"; | |
| private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0; | |
| private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1; | |
| private static final int RESULT_ERROR = -1; | |
| private static final int RESULT_OK = 0; | |
| /** @hide */ | |
| @Retention(RetentionPolicy.SOURCE) | |
| @IntDef(flag = true, value = { RESULT_FLAG_OPTION_NOT_HANDLED, | |
| RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED }) | |
| private @interface ResultFlags { } | |
| private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); | |
| private ConnectionRecord mCurConnection; | |
| private final Handler mHandler = new Handler(); | |
| private ServiceBinder mBinder; | |
| MediaSession.Token mSession; | |
| /** | |
| * All the info about a connection. | |
| */ | |
| private static class ConnectionRecord implements IBinder.DeathRecipient { | |
| public final MediaBrowserService service; | |
| public final String pkg; | |
| public final int pid; | |
| public final int uid; | |
| public final Bundle rootHints; | |
| public final IMediaBrowserServiceCallbacks callbacks; | |
| public final BrowserRoot root; | |
| public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>(); | |
| ConnectionRecord( | |
| MediaBrowserService service, String pkg, int pid, int uid, Bundle rootHints, | |
| IMediaBrowserServiceCallbacks callbacks, BrowserRoot root) { | |
| this.service = service; | |
| this.pkg = pkg; | |
| this.pid = pid; | |
| this.uid = uid; | |
| this.rootHints = rootHints; | |
| this.callbacks = callbacks; | |
| this.root = root; | |
| } | |
| @Override | |
| public void binderDied() { | |
| service.mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| service.mConnections.remove(callbacks.asBinder()); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * Completion handler for asynchronous callback methods in {@link MediaBrowserService}. | |
| * <p> | |
| * Each of the methods that takes one of these to send the result must call | |
| * {@link #sendResult} to respond to the caller with the given results. If those | |
| * functions return without calling {@link #sendResult}, they must instead call | |
| * {@link #detach} before returning, and then may call {@link #sendResult} when | |
| * they are done. If more than one of those methods is called, an exception will | |
| * be thrown. | |
| * | |
| * @see #onLoadChildren | |
| * @see #onLoadItem | |
| */ | |
| public class Result<T> { | |
| private Object mDebug; | |
| private boolean mDetachCalled; | |
| private boolean mSendResultCalled; | |
| @UnsupportedAppUsage | |
| private int mFlags; | |
| Result(Object debug) { | |
| mDebug = debug; | |
| } | |
| /** | |
| * Send the result back to the caller. | |
| */ | |
| public void sendResult(T result) { | |
| if (mSendResultCalled) { | |
| throw new IllegalStateException("sendResult() called twice for: " + mDebug); | |
| } | |
| mSendResultCalled = true; | |
| onResultSent(result, mFlags); | |
| } | |
| /** | |
| * Detach this message from the current thread and allow the {@link #sendResult} | |
| * call to happen later. | |
| */ | |
| public void detach() { | |
| if (mDetachCalled) { | |
| throw new IllegalStateException("detach() called when detach() had already" | |
| + " been called for: " + mDebug); | |
| } | |
| if (mSendResultCalled) { | |
| throw new IllegalStateException("detach() called when sendResult() had already" | |
| + " been called for: " + mDebug); | |
| } | |
| mDetachCalled = true; | |
| } | |
| boolean isDone() { | |
| return mDetachCalled || mSendResultCalled; | |
| } | |
| void setFlags(@ResultFlags int flags) { | |
| mFlags = flags; | |
| } | |
| /** | |
| * Called when the result is sent, after assertions about not being called twice | |
| * have happened. | |
| */ | |
| void onResultSent(T result, @ResultFlags int flags) { | |
| } | |
| } | |
| private static class ServiceBinder extends IMediaBrowserService.Stub { | |
| private WeakReference<MediaBrowserService> mService; | |
| private ServiceBinder(MediaBrowserService service) { | |
| mService = new WeakReference(service); | |
| } | |
| @Override | |
| public void connect(final String pkg, final Bundle rootHints, | |
| final IMediaBrowserServiceCallbacks callbacks) { | |
| MediaBrowserService service = mService.get(); | |
| if (service == null) { | |
| return; | |
| } | |
| final int pid = Binder.getCallingPid(); | |
| final int uid = Binder.getCallingUid(); | |
| if (!service.isValidPackage(pkg, uid)) { | |
| throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid | |
| + " package=" + pkg); | |
| } | |
| service.mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| final IBinder b = callbacks.asBinder(); | |
| // Clear out the old subscriptions. We are getting new ones. | |
| service.mConnections.remove(b); | |
| // Temporarily sets a placeholder ConnectionRecord to make | |
| // getCurrentBrowserInfo() work in onGetRoot(). | |
| service.mCurConnection = | |
| new ConnectionRecord( | |
| service, pkg, pid, uid, rootHints, callbacks, null); | |
| BrowserRoot root = service.onGetRoot(pkg, uid, rootHints); | |
| service.mCurConnection = null; | |
| // If they didn't return something, don't allow this client. | |
| if (root == null) { | |
| Log.i(TAG, "No root for client " + pkg + " from service " | |
| + getClass().getName()); | |
| try { | |
| callbacks.onConnectFailed(); | |
| } catch (RemoteException ex) { | |
| Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " | |
| + "pkg=" + pkg); | |
| } | |
| } else { | |
| try { | |
| ConnectionRecord connection = | |
| new ConnectionRecord( | |
| service, pkg, pid, uid, rootHints, callbacks, root); | |
| service.mConnections.put(b, connection); | |
| b.linkToDeath(connection, 0); | |
| if (service.mSession != null) { | |
| callbacks.onConnect(connection.root.getRootId(), | |
| service.mSession, connection.root.getExtras()); | |
| } | |
| } catch (RemoteException ex) { | |
| Log.w(TAG, "Calling onConnect() failed. Dropping client. " | |
| + "pkg=" + pkg); | |
| service.mConnections.remove(b); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| @Override | |
| public void disconnect(final IMediaBrowserServiceCallbacks callbacks) { | |
| MediaBrowserService service = mService.get(); | |
| if (service == null) { | |
| return; | |
| } | |
| service.mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| final IBinder b = callbacks.asBinder(); | |
| // Clear out the old subscriptions. We are getting new ones. | |
| final ConnectionRecord old = service.mConnections.remove(b); | |
| if (old != null) { | |
| // TODO | |
| old.callbacks.asBinder().unlinkToDeath(old, 0); | |
| } | |
| } | |
| }); | |
| } | |
| @Override | |
| public void addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) { | |
| // do-nothing | |
| } | |
| @Override | |
| public void addSubscription(final String id, final IBinder token, final Bundle options, | |
| final IMediaBrowserServiceCallbacks callbacks) { | |
| MediaBrowserService service = mService.get(); | |
| if (service == null) { | |
| return; | |
| } | |
| service.mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| final IBinder b = callbacks.asBinder(); | |
| // Get the record for the connection | |
| final ConnectionRecord connection = service.mConnections.get(b); | |
| if (connection == null) { | |
| Log.w(TAG, "addSubscription for callback that isn't registered id=" | |
| + id); | |
| return; | |
| } | |
| service.addSubscription(id, connection, token, options); | |
| } | |
| }); | |
| } | |
| @Override | |
| public void removeSubscriptionDeprecated( | |
| String id, IMediaBrowserServiceCallbacks callbacks) { | |
| // do-nothing | |
| } | |
| @Override | |
| public void removeSubscription(final String id, final IBinder token, | |
| final IMediaBrowserServiceCallbacks callbacks) { | |
| MediaBrowserService service = mService.get(); | |
| if (service == null) { | |
| return; | |
| } | |
| service.mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| final IBinder b = callbacks.asBinder(); | |
| ConnectionRecord connection = service.mConnections.get(b); | |
| if (connection == null) { | |
| Log.w(TAG, "removeSubscription for callback that isn't registered id=" | |
| + id); | |
| return; | |
| } | |
| if (!service.removeSubscription(id, connection, token)) { | |
| Log.w(TAG, "removeSubscription called for " + id | |
| + " which is not subscribed"); | |
| } | |
| } | |
| }); | |
| } | |
| @Override | |
| public void getMediaItem(final String mediaId, final ResultReceiver receiver, | |
| final IMediaBrowserServiceCallbacks callbacks) { | |
| MediaBrowserService service = mService.get(); | |
| if (service == null) { | |
| return; | |
| } | |
| service.mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| final IBinder b = callbacks.asBinder(); | |
| ConnectionRecord connection = service.mConnections.get(b); | |
| if (connection == null) { | |
| Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); | |
| return; | |
| } | |
| service.performLoadItem(mediaId, connection, receiver); | |
| } | |
| }); | |
| } | |
| } | |
| @Override | |
| public void onCreate() { | |
| super.onCreate(); | |
| mBinder = new ServiceBinder(this); | |
| } | |
| @Override | |
| public IBinder onBind(Intent intent) { | |
| if (SERVICE_INTERFACE.equals(intent.getAction())) { | |
| return mBinder; | |
| } | |
| return null; | |
| } | |
| @Override | |
| public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { | |
| } | |
| /** | |
| * Called to get the root information for browsing by a particular client. | |
| * <p> | |
| * The implementation should verify that the client package has permission | |
| * to access browse media information before returning the root id; it | |
| * should return null if the client is not allowed to access this | |
| * information. | |
| * </p> | |
| * | |
| * @param clientPackageName The package name of the application which is | |
| * requesting access to browse media. | |
| * @param clientUid The uid of the application which is requesting access to | |
| * browse media. | |
| * @param rootHints An optional bundle of service-specific arguments to send | |
| * to the media browser service when connecting and retrieving the | |
| * root id for browsing, or null if none. The contents of this | |
| * bundle may affect the information returned when browsing. | |
| * @return The {@link BrowserRoot} for accessing this app's content or null. | |
| * @see BrowserRoot#EXTRA_RECENT | |
| * @see BrowserRoot#EXTRA_OFFLINE | |
| * @see BrowserRoot#EXTRA_SUGGESTED | |
| */ | |
| public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, | |
| int clientUid, @Nullable Bundle rootHints); | |
| /** | |
| * Called to get information about the children of a media item. | |
| * <p> | |
| * Implementations must call {@link Result#sendResult result.sendResult} | |
| * with the list of children. If loading the children will be an expensive | |
| * operation that should be performed on another thread, | |
| * {@link Result#detach result.detach} may be called before returning from | |
| * this function, and then {@link Result#sendResult result.sendResult} | |
| * called when the loading is complete. | |
| * </p><p> | |
| * In case the media item does not have any children, call {@link Result#sendResult} | |
| * with an empty list. When the given {@code parentId} is invalid, implementations must | |
| * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke | |
| * {@link MediaBrowser.SubscriptionCallback#onError}. | |
| * </p> | |
| * | |
| * @param parentId The id of the parent media item whose children are to be | |
| * queried. | |
| * @param result The Result to send the list of children to. | |
| */ | |
| public abstract void onLoadChildren(@NonNull String parentId, | |
| @NonNull Result<List<MediaBrowser.MediaItem>> result); | |
| /** | |
| * Called to get information about the children of a media item. | |
| * <p> | |
| * Implementations must call {@link Result#sendResult result.sendResult} | |
| * with the list of children. If loading the children will be an expensive | |
| * operation that should be performed on another thread, | |
| * {@link Result#detach result.detach} may be called before returning from | |
| * this function, and then {@link Result#sendResult result.sendResult} | |
| * called when the loading is complete. | |
| * </p><p> | |
| * In case the media item does not have any children, call {@link Result#sendResult} | |
| * with an empty list. When the given {@code parentId} is invalid, implementations must | |
| * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke | |
| * {@link MediaBrowser.SubscriptionCallback#onError}. | |
| * </p> | |
| * | |
| * @param parentId The id of the parent media item whose children are to be | |
| * queried. | |
| * @param result The Result to send the list of children to. | |
| * @param options The bundle of service-specific arguments sent from the media | |
| * browser. The information returned through the result should be | |
| * affected by the contents of this bundle. | |
| */ | |
| public void onLoadChildren(@NonNull String parentId, | |
| @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) { | |
| // To support backward compatibility, when the implementation of MediaBrowserService doesn't | |
| // override onLoadChildren() with options, onLoadChildren() without options will be used | |
| // instead, and the options will be applied in the implementation of result.onResultSent(). | |
| result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); | |
| onLoadChildren(parentId, result); | |
| } | |
| /** | |
| * Called to get information about a specific media item. | |
| * <p> | |
| * Implementations must call {@link Result#sendResult result.sendResult}. If | |
| * loading the item will be an expensive operation {@link Result#detach | |
| * result.detach} may be called before returning from this function, and | |
| * then {@link Result#sendResult result.sendResult} called when the item has | |
| * been loaded. | |
| * </p><p> | |
| * When the given {@code itemId} is invalid, implementations must call | |
| * {@link Result#sendResult result.sendResult} with {@code null}. | |
| * </p><p> | |
| * The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}. | |
| * </p> | |
| * | |
| * @param itemId The id for the specific | |
| * {@link android.media.browse.MediaBrowser.MediaItem}. | |
| * @param result The Result to send the item to. | |
| */ | |
| public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { | |
| result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED); | |
| result.sendResult(null); | |
| } | |
| /** | |
| * Call to set the media session. | |
| * <p> | |
| * This should be called as soon as possible during the service's startup. | |
| * It may only be called once. | |
| * | |
| * @param token The token for the service's {@link MediaSession}. | |
| */ | |
| public void setSessionToken(final MediaSession.Token token) { | |
| if (token == null) { | |
| throw new IllegalArgumentException("Session token may not be null."); | |
| } | |
| if (mSession != null) { | |
| throw new IllegalStateException("The session token has already been set."); | |
| } | |
| mSession = token; | |
| mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| Iterator<ConnectionRecord> iter = mConnections.values().iterator(); | |
| while (iter.hasNext()) { | |
| ConnectionRecord connection = iter.next(); | |
| try { | |
| connection.callbacks.onConnect(connection.root.getRootId(), token, | |
| connection.root.getExtras()); | |
| } catch (RemoteException e) { | |
| Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); | |
| iter.remove(); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Gets the session token, or null if it has not yet been created | |
| * or if it has been destroyed. | |
| */ | |
| public @Nullable MediaSession.Token getSessionToken() { | |
| return mSession; | |
| } | |
| /** | |
| * Gets the root hints sent from the currently connected {@link MediaBrowser}. | |
| * The root hints are service-specific arguments included in an optional bundle sent to the | |
| * media browser service when connecting and retrieving the root id for browsing, or null if | |
| * none. The contents of this bundle may affect the information returned when browsing. | |
| * | |
| * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or | |
| * {@link #onLoadChildren} or {@link #onLoadItem}. | |
| * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT | |
| * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE | |
| * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED | |
| */ | |
| public final Bundle getBrowserRootHints() { | |
| if (mCurConnection == null) { | |
| throw new IllegalStateException("This should be called inside of onGetRoot or" | |
| + " onLoadChildren or onLoadItem methods"); | |
| } | |
| return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); | |
| } | |
| /** | |
| * Gets the browser information who sent the current request. | |
| * | |
| * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or | |
| * {@link #onLoadChildren} or {@link #onLoadItem}. | |
| * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) | |
| */ | |
| public final RemoteUserInfo getCurrentBrowserInfo() { | |
| if (mCurConnection == null) { | |
| throw new IllegalStateException("This should be called inside of onGetRoot or" | |
| + " onLoadChildren or onLoadItem methods"); | |
| } | |
| return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid); | |
| } | |
| /** | |
| * Notifies all connected media browsers that the children of | |
| * the specified parent id have changed in some way. | |
| * This will cause browsers to fetch subscribed content again. | |
| * | |
| * @param parentId The id of the parent media item whose | |
| * children changed. | |
| */ | |
| public void notifyChildrenChanged(@NonNull String parentId) { | |
| notifyChildrenChangedInternal(parentId, null); | |
| } | |
| /** | |
| * Notifies all connected media browsers that the children of | |
| * the specified parent id have changed in some way. | |
| * This will cause browsers to fetch subscribed content again. | |
| * | |
| * @param parentId The id of the parent media item whose | |
| * children changed. | |
| * @param options The bundle of service-specific arguments to send | |
| * to the media browser. The contents of this bundle may | |
| * contain the information about the change. | |
| */ | |
| public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { | |
| if (options == null) { | |
| throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); | |
| } | |
| notifyChildrenChangedInternal(parentId, options); | |
| } | |
| private void notifyChildrenChangedInternal(final String parentId, final Bundle options) { | |
| if (parentId == null) { | |
| throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); | |
| } | |
| mHandler.post(new Runnable() { | |
| @Override | |
| public void run() { | |
| for (IBinder binder : mConnections.keySet()) { | |
| ConnectionRecord connection = mConnections.get(binder); | |
| List<Pair<IBinder, Bundle>> callbackList = | |
| connection.subscriptions.get(parentId); | |
| if (callbackList != null) { | |
| for (Pair<IBinder, Bundle> callback : callbackList) { | |
| if (MediaBrowserUtils.hasDuplicatedItems(options, callback.second)) { | |
| performLoadChildren(parentId, connection, callback.second); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Return whether the given package is one of the ones that is owned by the uid. | |
| */ | |
| private boolean isValidPackage(String pkg, int uid) { | |
| if (pkg == null) { | |
| return false; | |
| } | |
| final PackageManager pm = getPackageManager(); | |
| final String[] packages = pm.getPackagesForUid(uid); | |
| final int N = packages.length; | |
| for (int i = 0; i < N; i++) { | |
| if (packages[i].equals(pkg)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Save the subscription and if it is a new subscription send the results. | |
| */ | |
| private void addSubscription(String id, ConnectionRecord connection, IBinder token, | |
| Bundle options) { | |
| // Save the subscription | |
| List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); | |
| if (callbackList == null) { | |
| callbackList = new ArrayList<>(); | |
| } | |
| for (Pair<IBinder, Bundle> callback : callbackList) { | |
| if (token == callback.first | |
| && MediaBrowserUtils.areSameOptions(options, callback.second)) { | |
| return; | |
| } | |
| } | |
| callbackList.add(new Pair<>(token, options)); | |
| connection.subscriptions.put(id, callbackList); | |
| // send the results | |
| performLoadChildren(id, connection, options); | |
| } | |
| /** | |
| * Remove the subscription. | |
| */ | |
| private boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) { | |
| if (token == null) { | |
| return connection.subscriptions.remove(id) != null; | |
| } | |
| boolean removed = false; | |
| List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); | |
| if (callbackList != null) { | |
| Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator(); | |
| while (iter.hasNext()) { | |
| if (token == iter.next().first) { | |
| removed = true; | |
| iter.remove(); | |
| } | |
| } | |
| if (callbackList.size() == 0) { | |
| connection.subscriptions.remove(id); | |
| } | |
| } | |
| return removed; | |
| } | |
| /** | |
| * Call onLoadChildren and then send the results back to the connection. | |
| * <p> | |
| * Callers must make sure that this connection is still connected. | |
| */ | |
| private void performLoadChildren(final String parentId, final ConnectionRecord connection, | |
| final Bundle options) { | |
| final Result<List<MediaBrowser.MediaItem>> result = | |
| new Result<List<MediaBrowser.MediaItem>>(parentId) { | |
| @Override | |
| void onResultSent(List<MediaBrowser.MediaItem> list, @ResultFlags int flag) { | |
| if (mConnections.get(connection.callbacks.asBinder()) != connection) { | |
| if (DBG) { | |
| Log.d(TAG, "Not sending onLoadChildren result for connection that has" | |
| + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); | |
| } | |
| return; | |
| } | |
| List<MediaBrowser.MediaItem> filteredList = | |
| (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 | |
| ? applyOptions(list, options) : list; | |
| final ParceledListSlice<MediaBrowser.MediaItem> pls; | |
| if (filteredList == null) { | |
| pls = null; | |
| } else { | |
| pls = new ParceledListSlice<>(filteredList); | |
| // Limit the size of initial Parcel to prevent binder buffer overflow | |
| // as onLoadChildren is an async binder call. | |
| pls.setInlineCountLimit(1); | |
| } | |
| try { | |
| connection.callbacks.onLoadChildren(parentId, pls, options); | |
| } catch (RemoteException ex) { | |
| // The other side is in the process of crashing. | |
| Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId | |
| + " package=" + connection.pkg); | |
| } | |
| } | |
| }; | |
| mCurConnection = connection; | |
| if (options == null) { | |
| onLoadChildren(parentId, result); | |
| } else { | |
| onLoadChildren(parentId, result, options); | |
| } | |
| mCurConnection = null; | |
| if (!result.isDone()) { | |
| throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" | |
| + " before returning for package=" + connection.pkg + " id=" + parentId); | |
| } | |
| } | |
| private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list, | |
| final Bundle options) { | |
| if (list == null) { | |
| return null; | |
| } | |
| int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1); | |
| int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); | |
| if (page == -1 && pageSize == -1) { | |
| return list; | |
| } | |
| int fromIndex = pageSize * page; | |
| int toIndex = fromIndex + pageSize; | |
| if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { | |
| return Collections.EMPTY_LIST; | |
| } | |
| if (toIndex > list.size()) { | |
| toIndex = list.size(); | |
| } | |
| return list.subList(fromIndex, toIndex); | |
| } | |
| private void performLoadItem(String itemId, final ConnectionRecord connection, | |
| final ResultReceiver receiver) { | |
| final Result<MediaBrowser.MediaItem> result = | |
| new Result<MediaBrowser.MediaItem>(itemId) { | |
| @Override | |
| void onResultSent(MediaBrowser.MediaItem item, @ResultFlags int flag) { | |
| if (mConnections.get(connection.callbacks.asBinder()) != connection) { | |
| if (DBG) { | |
| Log.d(TAG, "Not sending onLoadItem result for connection that has" | |
| + " been disconnected. pkg=" + connection.pkg + " id=" + itemId); | |
| } | |
| return; | |
| } | |
| if ((flag & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) { | |
| receiver.send(RESULT_ERROR, null); | |
| return; | |
| } | |
| Bundle bundle = new Bundle(); | |
| bundle.putParcelable(KEY_MEDIA_ITEM, item); | |
| receiver.send(RESULT_OK, bundle); | |
| } | |
| }; | |
| mCurConnection = connection; | |
| onLoadItem(itemId, result); | |
| mCurConnection = null; | |
| if (!result.isDone()) { | |
| throw new IllegalStateException("onLoadItem must call detach() or sendResult()" | |
| + " before returning for id=" + itemId); | |
| } | |
| } | |
| /** | |
| * Contains information that the browser service needs to send to the client | |
| * when first connected. | |
| */ | |
| public static final class BrowserRoot { | |
| /** | |
| * The lookup key for a boolean that indicates whether the browser service should return a | |
| * browser root for recently played media items. | |
| * | |
| * <p>When creating a media browser for a given media browser service, this key can be | |
| * supplied as a root hint for retrieving media items that are recently played. | |
| * If the media browser service can provide such media items, the implementation must return | |
| * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. | |
| * | |
| * <p>The root hint may contain multiple keys. | |
| * | |
| * @see #EXTRA_OFFLINE | |
| * @see #EXTRA_SUGGESTED | |
| */ | |
| public static final String EXTRA_RECENT = "android.service.media.extra.RECENT"; | |
| /** | |
| * The lookup key for a boolean that indicates whether the browser service should return a | |
| * browser root for offline media items. | |
| * | |
| * <p>When creating a media browser for a given media browser service, this key can be | |
| * supplied as a root hint for retrieving media items that are can be played without an | |
| * internet connection. | |
| * If the media browser service can provide such media items, the implementation must return | |
| * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. | |
| * | |
| * <p>The root hint may contain multiple keys. | |
| * | |
| * @see #EXTRA_RECENT | |
| * @see #EXTRA_SUGGESTED | |
| */ | |
| public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE"; | |
| /** | |
| * The lookup key for a boolean that indicates whether the browser service should return a | |
| * browser root for suggested media items. | |
| * | |
| * <p>When creating a media browser for a given media browser service, this key can be | |
| * supplied as a root hint for retrieving the media items suggested by the media browser | |
| * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)} | |
| * is considered ordered by relevance, first being the top suggestion. | |
| * If the media browser service can provide such media items, the implementation must return | |
| * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. | |
| * | |
| * <p>The root hint may contain multiple keys. | |
| * | |
| * @see #EXTRA_RECENT | |
| * @see #EXTRA_OFFLINE | |
| */ | |
| public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED"; | |
| private final String mRootId; | |
| private final Bundle mExtras; | |
| /** | |
| * Constructs a browser root. | |
| * @param rootId The root id for browsing. | |
| * @param extras Any extras about the browser service. | |
| */ | |
| public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { | |
| if (rootId == null) { | |
| throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " | |
| + "Use null for BrowserRoot instead."); | |
| } | |
| mRootId = rootId; | |
| mExtras = extras; | |
| } | |
| /** | |
| * Gets the root id for browsing. | |
| */ | |
| public String getRootId() { | |
| return mRootId; | |
| } | |
| /** | |
| * Gets any extras about the browser service. | |
| */ | |
| public Bundle getExtras() { | |
| return mExtras; | |
| } | |
| } | |
| } |