| /* |
| * Copyright 2018 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 androidx.media; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY; |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; |
| import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; |
| import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; |
| import static androidx.media.MediaBrowserProtocol.DATA_CALLING_PID; |
| import static androidx.media.MediaBrowserProtocol.DATA_CALLING_UID; |
| import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION; |
| import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; |
| import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; |
| import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; |
| import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; |
| import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; |
| import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS; |
| import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME; |
| import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER; |
| import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS; |
| import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; |
| import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY; |
| import static androidx.media.MediaBrowserProtocol.EXTRA_CALLING_PID; |
| import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; |
| import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; |
| import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; |
| import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER; |
| import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; |
| import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; |
| import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; |
| import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT; |
| import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; |
| import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_PID; |
| import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_UID; |
| |
| import android.app.Service; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.media.browse.MediaBrowser; |
| import android.media.session.MediaSession; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.Messenger; |
| import android.os.Parcel; |
| import android.os.RemoteException; |
| import android.service.media.MediaBrowserService; |
| import android.support.v4.media.MediaBrowserCompat; |
| import android.support.v4.media.session.IMediaSession; |
| import android.support.v4.media.session.MediaSessionCompat; |
| import android.support.v4.os.ResultReceiver; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.annotation.CallSuper; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.MainThread; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.RestrictTo; |
| import androidx.collection.ArrayMap; |
| import androidx.core.app.BundleCompat; |
| import androidx.core.util.Pair; |
| import androidx.media.MediaSessionManager.RemoteUserInfo; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * Base class for media browse services. |
| * <p> |
| * Media browse 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 MediaSessionCompat}. |
| * </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=".MyMediaBrowserServiceCompat" |
| * android:label="@string/service_name" > |
| * <intent-filter> |
| * <action android:name="android.media.browse.MediaBrowserService" /> |
| * </intent-filter> |
| * </service> |
| * </pre> |
| * |
| * <div class="special reference"> |
| * <h3>Developer Guides</h3> |
| * <p>For information about building your media application, read the |
| * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p> |
| * </div> |
| */ |
| public abstract class MediaBrowserServiceCompat extends Service { |
| static final String TAG = "MBServiceCompat"; |
| static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| private static final float EPSILON = 0.00001f; |
| |
| private MediaBrowserServiceImpl mImpl; |
| |
| /** |
| * The {@link Intent} that must be declared as handled by the service. |
| */ |
| public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; |
| |
| /** |
| * A key for passing the MediaItem to the ResultReceiver in getItem. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String KEY_MEDIA_ITEM = "media_item"; |
| |
| /** |
| * A key for passing the list of MediaItems to the ResultReceiver in search. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String KEY_SEARCH_RESULTS = "search_results"; |
| |
| static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0; |
| static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1; |
| static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2; |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public static final int RESULT_ERROR = -1; |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public static final int RESULT_OK = 0; |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public static final int RESULT_PROGRESS_UPDATE = 1; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef(flag = true, value = {RESULT_FLAG_OPTION_NOT_HANDLED, |
| RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED}) |
| private @interface ResultFlags { |
| } |
| |
| private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl(); |
| final ConnectionRecord mConnectionFromFwk = new ConnectionRecord( |
| LEGACY_CONTROLLER, UNKNOWN_PID, UNKNOWN_UID, null, null); |
| final ArrayList<ConnectionRecord> mPendingConnections = new ArrayList<>(); |
| final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); |
| ConnectionRecord mCurConnection; |
| final ServiceHandler mHandler = new ServiceHandler(/* service= */ this); |
| MediaSessionCompat.Token mSession; |
| |
| interface MediaBrowserServiceImpl { |
| void onCreate(); |
| IBinder onBind(Intent intent); |
| void setSessionToken(MediaSessionCompat.Token token); |
| void notifyChildrenChanged(String parentId, Bundle options); |
| void notifyChildrenChanged(RemoteUserInfo remoteUserInfo, String parentId, Bundle options); |
| Bundle getBrowserRootHints(); |
| RemoteUserInfo getCurrentBrowserInfo(); |
| } |
| |
| class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { |
| private Messenger mMessenger; |
| |
| @Override |
| public void onCreate() { |
| mMessenger = new Messenger(mHandler); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| if (SERVICE_INTERFACE.equals(intent.getAction())) { |
| return mMessenger.getBinder(); |
| } |
| return null; |
| } |
| |
| @Override |
| public void setSessionToken(final MediaSessionCompat.Token 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(); |
| } |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void notifyChildrenChanged(@NonNull final String parentId, final Bundle options) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| for (IBinder binder : mConnections.keySet()) { |
| ConnectionRecord connection = mConnections.get(binder); |
| notifyChildrenChangedOnHandler(connection, parentId, options); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void notifyChildrenChanged(@NonNull final RemoteUserInfo remoteUserInfo, |
| @NonNull final String parentId, final Bundle options) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| for (int i = 0; i < mConnections.size(); i++) { |
| ConnectionRecord connection = mConnections.valueAt(i); |
| if (connection.browserInfo.equals(remoteUserInfo)) { |
| notifyChildrenChangedOnHandler(connection, parentId, options); |
| break; |
| } |
| } |
| } |
| }); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void notifyChildrenChangedOnHandler(final ConnectionRecord connection, |
| final String parentId, final Bundle options) { |
| List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(parentId); |
| if (callbackList != null) { |
| for (Pair<IBinder, Bundle> callback : callbackList) { |
| if (MediaBrowserCompatUtils.hasDuplicatedItems(options, callback.second)) { |
| performLoadChildren(parentId, connection, callback.second, options); |
| } |
| } |
| } |
| // Don't break, because multiple remoteUserInfo may match. |
| } |
| |
| @Override |
| public Bundle getBrowserRootHints() { |
| if (mCurConnection == null) { |
| throw new IllegalStateException("This should be called inside of onLoadChildren," |
| + " onLoadItem, onSearch, or onCustomAction methods"); |
| } |
| return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); |
| } |
| |
| @Override |
| public RemoteUserInfo getCurrentBrowserInfo() { |
| if (mCurConnection == null) { |
| throw new IllegalStateException("This should be called inside of onLoadChildren," |
| + " onLoadItem, onSearch, or onCustomAction methods"); |
| } |
| return mCurConnection.browserInfo; |
| } |
| } |
| |
| @RequiresApi(21) |
| class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl { |
| final List<Bundle> mRootExtrasList = new ArrayList<>(); |
| MediaBrowserService mServiceFwk; |
| Messenger mMessenger; |
| |
| @Override |
| public void onCreate() { |
| mServiceFwk = new MediaBrowserServiceApi21(MediaBrowserServiceCompat.this); |
| mServiceFwk.onCreate(); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mServiceFwk.onBind(intent); |
| } |
| |
| @Override |
| public void setSessionToken(final MediaSessionCompat.Token token) { |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| setSessionTokenOnHandler(token); |
| } |
| }); |
| } |
| |
| void setSessionTokenOnHandler(MediaSessionCompat.Token token) { |
| if (!mRootExtrasList.isEmpty()) { |
| IMediaSession extraBinder = token.getExtraBinder(); |
| if (extraBinder != null) { |
| for (Bundle rootExtras : mRootExtrasList) { |
| BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER, |
| extraBinder.asBinder()); |
| } |
| } |
| mRootExtrasList.clear(); |
| } |
| mServiceFwk.setSessionToken((MediaSession.Token) token.getToken()); |
| } |
| |
| @Override |
| public void notifyChildrenChanged(final String parentId, final Bundle options) { |
| notifyChildrenChangedForFramework(parentId, options); |
| notifyChildrenChangedForCompat(parentId, options); |
| } |
| |
| @Override |
| public void notifyChildrenChanged(final RemoteUserInfo remoteUserInfo, |
| final String parentId, final Bundle options) { |
| // TODO(Post-P): Need a way to notify to a specific browser in framework. |
| notifyChildrenChangedForCompat(remoteUserInfo, parentId, options); |
| } |
| |
| public BrowserRoot onGetRoot( |
| String clientPackageName, int clientUid, Bundle rootHints) { |
| Bundle rootExtras = null; |
| int clientPid = UNKNOWN_PID; |
| if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) { |
| rootHints.remove(EXTRA_CLIENT_VERSION); |
| mMessenger = new Messenger(mHandler); |
| rootExtras = new Bundle(); |
| rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); |
| BundleCompat.putBinder(rootExtras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder()); |
| if (mSession != null) { |
| IMediaSession extraBinder = mSession.getExtraBinder(); |
| BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER, |
| extraBinder == null ? null : extraBinder.asBinder()); |
| } else { |
| mRootExtrasList.add(rootExtras); |
| } |
| clientPid = rootHints.getInt(EXTRA_CALLING_PID, UNKNOWN_PID); |
| rootHints.remove(EXTRA_CALLING_PID); |
| } |
| ConnectionRecord connection = new ConnectionRecord( |
| clientPackageName, clientPid, clientUid, rootHints, null); |
| // We aren't sure whether this connection request would be accepted. |
| // Temporarily set mCurConnection just to make getCurrentBrowserInfo() working. |
| mCurConnection = connection; |
| BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot( |
| clientPackageName, clientUid, rootHints); |
| mCurConnection = null; |
| if (root == null) { |
| return null; |
| } |
| if (mMessenger != null) { |
| // Keeps the connection request from the MediaBrowserCompat to reuse the package |
| // name here. |
| // Note: Connection will be completed after it gets extra binder call with |
| // CLIENT_MSG_REGISTER_CALLBACK_MESSENGER. |
| mPendingConnections.add(connection); |
| } |
| if (rootExtras == null) { |
| rootExtras = root.getExtras(); |
| } else if (root.getExtras() != null) { |
| rootExtras.putAll(root.getExtras()); |
| } |
| return new BrowserRoot(root.getRootId(), rootExtras); |
| } |
| |
| public void onLoadChildren(String parentId, |
| final ResultWrapper<List<Parcel>> resultWrapper) { |
| final Result<List<MediaBrowserCompat.MediaItem>> result = |
| new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { |
| @Override |
| void onResultSent(@Nullable List<MediaBrowserCompat.MediaItem> list) { |
| List<Parcel> parcelList = null; |
| if (list != null) { |
| parcelList = new ArrayList<>(list.size()); |
| for (MediaBrowserCompat.MediaItem item : list) { |
| Parcel parcel = Parcel.obtain(); |
| item.writeToParcel(parcel, 0); |
| parcelList.add(parcel); |
| } |
| } |
| resultWrapper.sendResult(parcelList); |
| } |
| |
| @Override |
| public void detach() { |
| resultWrapper.detach(); |
| } |
| }; |
| mCurConnection = mConnectionFromFwk; |
| MediaBrowserServiceCompat.this.onLoadChildren(parentId, result); |
| mCurConnection = null; |
| } |
| |
| void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { |
| mServiceFwk.notifyChildrenChanged(parentId); |
| } |
| |
| void notifyChildrenChangedForCompat(final String parentId, final Bundle options) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| for (IBinder binder : mConnections.keySet()) { |
| ConnectionRecord connection = mConnections.get(binder); |
| notifyChildrenChangedForCompatOnHandler(connection, parentId, options); |
| } |
| } |
| }); |
| } |
| |
| void notifyChildrenChangedForCompat(final RemoteUserInfo remoteUserInfo, |
| final String parentId, final Bundle options) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| for (int i = 0; i < mConnections.size(); i++) { |
| ConnectionRecord connection = mConnections.valueAt(i); |
| if (connection.browserInfo.equals(remoteUserInfo)) { |
| notifyChildrenChangedForCompatOnHandler(connection, parentId, options); |
| } |
| } |
| } |
| }); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void notifyChildrenChangedForCompatOnHandler(final ConnectionRecord connection, |
| final String parentId, final Bundle options) { |
| List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(parentId); |
| if (callbackList != null) { |
| for (Pair<IBinder, Bundle> callback : callbackList) { |
| if (MediaBrowserCompatUtils.hasDuplicatedItems(options, callback.second)) { |
| performLoadChildren(parentId, connection, callback.second, options); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public Bundle getBrowserRootHints() { |
| if (mMessenger == null) { |
| // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser. |
| return null; |
| } |
| if (mCurConnection == null) { |
| throw new IllegalStateException("This should be called inside of onGetRoot," |
| + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); |
| } |
| return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); |
| } |
| |
| @Override |
| public RemoteUserInfo getCurrentBrowserInfo() { |
| if (mCurConnection == null) { |
| throw new IllegalStateException("This should be called inside of onGetRoot," |
| + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); |
| } |
| return mCurConnection.browserInfo; |
| } |
| |
| @RequiresApi(21) |
| class MediaBrowserServiceApi21 extends MediaBrowserService { |
| MediaBrowserServiceApi21(Context context) { |
| attachBaseContext(context); |
| } |
| |
| @Override |
| public MediaBrowserService.BrowserRoot onGetRoot(String clientPackageName, |
| int clientUid, Bundle rootHints) { |
| MediaSessionCompat.ensureClassLoader(rootHints); |
| MediaBrowserServiceCompat.BrowserRoot browserRootCompat = |
| MediaBrowserServiceImplApi21.this.onGetRoot(clientPackageName, clientUid, |
| rootHints == null ? null : new Bundle(rootHints)); |
| return browserRootCompat == null ? null : new MediaBrowserService.BrowserRoot( |
| browserRootCompat.mRootId, browserRootCompat.mExtras); |
| } |
| |
| @Override |
| public void onLoadChildren(String parentId, |
| Result<List<MediaBrowser.MediaItem>> result) { |
| MediaBrowserServiceImplApi21.this.onLoadChildren(parentId, |
| new ResultWrapper<List<Parcel>>(result)); |
| } |
| } |
| } |
| |
| @RequiresApi(23) |
| class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 { |
| @Override |
| public void onCreate() { |
| mServiceFwk = new MediaBrowserServiceApi23(MediaBrowserServiceCompat.this); |
| mServiceFwk.onCreate(); |
| } |
| |
| public void onLoadItem(String itemId, final ResultWrapper<Parcel> resultWrapper) { |
| final Result<MediaBrowserCompat.MediaItem> result = |
| new Result<MediaBrowserCompat.MediaItem>(itemId) { |
| @Override |
| void onResultSent(@Nullable MediaBrowserCompat.MediaItem item) { |
| if (item == null) { |
| resultWrapper.sendResult(null); |
| } else { |
| Parcel parcelItem = Parcel.obtain(); |
| item.writeToParcel(parcelItem, 0); |
| resultWrapper.sendResult(parcelItem); |
| } |
| } |
| |
| @Override |
| public void detach() { |
| resultWrapper.detach(); |
| } |
| }; |
| mCurConnection = mConnectionFromFwk; |
| MediaBrowserServiceCompat.this.onLoadItem(itemId, result); |
| mCurConnection = null; |
| } |
| |
| class MediaBrowserServiceApi23 extends MediaBrowserServiceApi21 { |
| MediaBrowserServiceApi23(Context context) { |
| super(context); |
| } |
| |
| @Override |
| public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { |
| MediaBrowserServiceImplApi23.this.onLoadItem(itemId, |
| new ResultWrapper<Parcel>(result)); |
| } |
| } |
| } |
| |
| @RequiresApi(26) |
| class MediaBrowserServiceImplApi26 extends MediaBrowserServiceImplApi23 { |
| @Override |
| public void onCreate() { |
| mServiceFwk = new MediaBrowserServiceApi26(MediaBrowserServiceCompat.this); |
| mServiceFwk.onCreate(); |
| } |
| |
| public void onLoadChildren(String parentId, |
| final ResultWrapper<List<Parcel>> resultWrapper, |
| final Bundle options) { |
| final Result<List<MediaBrowserCompat.MediaItem>> result = |
| new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { |
| @Override |
| void onResultSent(@Nullable List<MediaBrowserCompat.MediaItem> list) { |
| if (list == null) { |
| resultWrapper.sendResult(null); |
| return; |
| } |
| if ((getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0) { |
| // If onLoadChildren(options) is not overridden, the list we get |
| // here is not paginated. Therefore, we need to manually cut |
| // the list. In other words, we need to apply options here. |
| list = applyOptions(list, options); |
| } |
| List<Parcel> parcelList = new ArrayList<>(list.size()); |
| for (MediaBrowserCompat.MediaItem item : list) { |
| Parcel parcel = Parcel.obtain(); |
| item.writeToParcel(parcel, 0); |
| parcelList.add(parcel); |
| } |
| resultWrapper.sendResult(parcelList); |
| } |
| |
| @Override |
| public void detach() { |
| resultWrapper.detach(); |
| } |
| }; |
| mCurConnection = mConnectionFromFwk; |
| MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options); |
| mCurConnection = null; |
| } |
| |
| @Override |
| public Bundle getBrowserRootHints() { |
| if (mCurConnection == null) { |
| throw new IllegalStateException("This should be called inside of onGetRoot," |
| + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); |
| } |
| if (mCurConnection == mConnectionFromFwk) { |
| return mServiceFwk.getBrowserRootHints(); |
| } |
| return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); |
| } |
| |
| @Override |
| void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { |
| if (options != null) { |
| mServiceFwk.notifyChildrenChanged(parentId, options); |
| } else { |
| super.notifyChildrenChangedForFramework(parentId, options); |
| } |
| } |
| |
| class MediaBrowserServiceApi26 extends MediaBrowserServiceApi23 { |
| MediaBrowserServiceApi26(Context context) { |
| super(context); |
| } |
| |
| @Override |
| public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result, |
| Bundle options) { |
| MediaSessionCompat.ensureClassLoader(options); |
| mCurConnection = mConnectionFromFwk; |
| MediaBrowserServiceImplApi26.this.onLoadChildren(parentId, |
| new ResultWrapper<List<Parcel>>(result), options); |
| mCurConnection = null; |
| } |
| } |
| } |
| |
| @RequiresApi(28) |
| class MediaBrowserServiceImplApi28 extends MediaBrowserServiceImplApi26 { |
| @Override |
| public RemoteUserInfo getCurrentBrowserInfo() { |
| if (mCurConnection == null) { |
| throw new IllegalStateException("This should be called inside of onGetRoot," |
| + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); |
| } |
| if (mCurConnection == mConnectionFromFwk) { |
| return new RemoteUserInfo(mServiceFwk.getCurrentBrowserInfo()); |
| } |
| return mCurConnection.browserInfo; |
| } |
| } |
| |
| private static final class ServiceHandler extends Handler { |
| |
| // Must only be accessed on the main thread. |
| @Nullable private MediaBrowserServiceCompat mService; |
| |
| @MainThread |
| ServiceHandler(@NonNull MediaBrowserServiceCompat service) { |
| mService = service; |
| } |
| |
| @Override |
| @MainThread |
| public void handleMessage(@NonNull Message msg) { |
| if (mService != null) { |
| mService.handleMessageInternal(msg); |
| } else { |
| removeCallbacksAndMessages(/* token= */ null); |
| } |
| } |
| |
| @MainThread |
| public void release() { |
| mService = null; |
| } |
| |
| @Override |
| public boolean sendMessageAtTime(Message msg, long uptimeMillis) { |
| // Binder.getCallingUid() in handleMessage will return the uid of this process. |
| // In order to get the right calling uid, Binder.getCallingUid() should be called here. |
| Bundle data = msg.getData(); |
| data.setClassLoader(MediaBrowserCompat.class.getClassLoader()); |
| data.putInt(DATA_CALLING_UID, Binder.getCallingUid()); |
| int pid = Binder.getCallingPid(); |
| if (pid > 0) { |
| data.putInt(DATA_CALLING_PID, pid); |
| } else if (!data.containsKey(DATA_CALLING_PID)) { |
| // If the MediaBrowserCompat didn't send its PID, then put UNKNOWN_PID. |
| data.putInt(DATA_CALLING_PID, UNKNOWN_PID); |
| } |
| return super.sendMessageAtTime(msg, uptimeMillis); |
| } |
| |
| public void postOrRun(Runnable r) { |
| if (Thread.currentThread() == getLooper().getThread()) { |
| r.run(); |
| } else { |
| post(r); |
| } |
| } |
| } |
| |
| /** |
| * All the info about a connection. |
| */ |
| private class ConnectionRecord implements IBinder.DeathRecipient { |
| public final String pkg; |
| public final int pid; |
| public final int uid; |
| public final RemoteUserInfo browserInfo; |
| public final Bundle rootHints; |
| public final ServiceCallbacks callbacks; |
| public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>(); |
| public BrowserRoot root; |
| |
| ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints, |
| ServiceCallbacks callback) { |
| this.pkg = pkg; |
| this.pid = pid; |
| this.uid = uid; |
| this.browserInfo = new RemoteUserInfo(pkg, pid, uid); |
| this.rootHints = rootHints; |
| this.callbacks = callback; |
| } |
| |
| @Override |
| public void binderDied() { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| mConnections.remove(callbacks.asBinder()); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. |
| * <p> |
| * Each of the methods that takes one of these to send the result must call either |
| * {@link #sendResult} or {@link #sendError} to respond to the caller with the given results or |
| * errors. If those functions return without calling {@link #sendResult} or {@link #sendError}, |
| * they must instead call {@link #detach} before returning, and then may call |
| * {@link #sendResult} or {@link #sendError} when they are done. If {@link #sendResult}, |
| * {@link #sendError}, or {@link #detach} is called twice, an exception will be thrown. |
| * </p><p> |
| * Those functions might also want to call {@link #sendProgressUpdate} to send interim updates |
| * to the caller. If it is called after calling {@link #sendResult} or {@link #sendError}, an |
| * exception will be thrown. |
| * </p> |
| * |
| * @see MediaBrowserServiceCompat#onLoadChildren |
| * @see MediaBrowserServiceCompat#onLoadItem |
| * @see MediaBrowserServiceCompat#onSearch |
| * @see MediaBrowserServiceCompat#onCustomAction |
| */ |
| public static class Result<T> { |
| private final Object mDebug; |
| private boolean mDetachCalled; |
| private boolean mSendResultCalled; |
| private boolean mSendErrorCalled; |
| private int mFlags; |
| |
| Result(Object debug) { |
| mDebug = debug; |
| } |
| |
| /** |
| * Send the result back to the caller. |
| */ |
| public void sendResult(@Nullable T result) { |
| if (mSendResultCalled || mSendErrorCalled) { |
| throw new IllegalStateException("sendResult() called when either sendResult() or " |
| + "sendError() had already been called for: " + mDebug); |
| } |
| mSendResultCalled = true; |
| onResultSent(result); |
| } |
| |
| /** |
| * Send an interim update to the caller. This method is supported only when it is used in |
| * {@link #onCustomAction}. |
| * |
| * @param extras A bundle that contains extra data. |
| */ |
| public void sendProgressUpdate(@Nullable Bundle extras) { |
| if (mSendResultCalled || mSendErrorCalled) { |
| throw new IllegalStateException("sendProgressUpdate() called when either " |
| + "sendResult() or sendError() had already been called for: " + mDebug); |
| } |
| checkExtraFields(extras); |
| onProgressUpdateSent(extras); |
| } |
| |
| /** |
| * Notify the caller of a failure. This is supported only when it is used in |
| * {@link #onCustomAction}. |
| * |
| * @param extras A bundle that contains extra data. |
| */ |
| public void sendError(@Nullable Bundle extras) { |
| if (mSendResultCalled || mSendErrorCalled) { |
| throw new IllegalStateException("sendError() called when either sendResult() or " |
| + "sendError() had already been called for: " + mDebug); |
| } |
| mSendErrorCalled = true; |
| onErrorSent(extras); |
| } |
| |
| /** |
| * 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); |
| } |
| if (mSendErrorCalled) { |
| throw new IllegalStateException("detach() called when sendError() had already" |
| + " been called for: " + mDebug); |
| } |
| mDetachCalled = true; |
| } |
| |
| boolean isDone() { |
| return mDetachCalled || mSendResultCalled || mSendErrorCalled; |
| } |
| |
| void setFlags(@ResultFlags int flags) { |
| mFlags = flags; |
| } |
| |
| int getFlags() { |
| return mFlags; |
| } |
| |
| /** |
| * Called when the result is sent, after assertions about not being called twice have |
| * happened. |
| */ |
| void onResultSent(@Nullable T result) { |
| } |
| |
| /** |
| * Called when an interim update is sent. |
| */ |
| void onProgressUpdateSent(@Nullable Bundle extras) { |
| throw new UnsupportedOperationException("It is not supported to send an interim update " |
| + "for " + mDebug); |
| } |
| |
| /** |
| * Called when an error is sent, after assertions about not being called twice have |
| * happened. |
| */ |
| void onErrorSent(@Nullable Bundle extras) { |
| throw new UnsupportedOperationException("It is not supported to send an error for " |
| + mDebug); |
| } |
| |
| private void checkExtraFields(@Nullable Bundle extras) { |
| if (extras == null) { |
| return; |
| } |
| if (extras.containsKey(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS)) { |
| float value = extras.getFloat(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS); |
| if (value < -EPSILON || value > 1.0f + EPSILON) { |
| throw new IllegalArgumentException("The value of the EXTRA_DOWNLOAD_PROGRESS " |
| + "field must be a float number within [0.0, 1.0]"); |
| } |
| } |
| } |
| } |
| |
| private class ServiceBinderImpl { |
| ServiceBinderImpl() { |
| } |
| |
| public void connect(final String pkg, final int pid, final int uid, final Bundle rootHints, |
| final ServiceCallbacks callbacks) { |
| |
| if (!isValidPackage(pkg, uid)) { |
| throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid |
| + " package=" + pkg); |
| } |
| |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| // Clear out the old subscriptions. We are getting new ones. |
| mConnections.remove(b); |
| |
| final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid, |
| rootHints, callbacks); |
| mCurConnection = connection; |
| connection.root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); |
| mCurConnection = null; |
| |
| // If they didn't return something, don't allow this client. |
| if (connection.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 { |
| mConnections.put(b, connection); |
| b.linkToDeath(connection, 0); |
| if (mSession != null) { |
| callbacks.onConnect(connection.root.getRootId(), |
| mSession, connection.root.getExtras()); |
| } |
| } catch (RemoteException ex) { |
| Log.w(TAG, "Calling onConnect() failed. Dropping client. " |
| + "pkg=" + pkg); |
| mConnections.remove(b); |
| } |
| } |
| } |
| }); |
| } |
| |
| public void disconnect(final ServiceCallbacks callbacks) { |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| // Clear out the old subscriptions. We are getting new ones. |
| final ConnectionRecord old = mConnections.remove(b); |
| if (old != null) { |
| // TODO |
| old.callbacks.asBinder().unlinkToDeath(old, 0); |
| } |
| } |
| }); |
| } |
| |
| public void addSubscription(final String id, final IBinder token, final Bundle options, |
| final ServiceCallbacks callbacks) { |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| // Get the record for the connection |
| final ConnectionRecord connection = mConnections.get(b); |
| if (connection == null) { |
| Log.w(TAG, "addSubscription for callback that isn't registered id=" |
| + id); |
| return; |
| } |
| |
| MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options); |
| } |
| }); |
| } |
| |
| public void removeSubscription(final String id, final IBinder token, |
| final ServiceCallbacks callbacks) { |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| ConnectionRecord connection = mConnections.get(b); |
| if (connection == null) { |
| Log.w(TAG, "removeSubscription for callback that isn't registered id=" |
| + id); |
| return; |
| } |
| if (!MediaBrowserServiceCompat.this.removeSubscription( |
| id, connection, token)) { |
| Log.w(TAG, "removeSubscription called for " + id |
| + " which is not subscribed"); |
| } |
| } |
| }); |
| } |
| |
| public void getMediaItem(final String mediaId, final ResultReceiver receiver, |
| final ServiceCallbacks callbacks) { |
| if (TextUtils.isEmpty(mediaId) || receiver == null) { |
| return; |
| } |
| |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| ConnectionRecord connection = mConnections.get(b); |
| if (connection == null) { |
| Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); |
| return; |
| } |
| performLoadItem(mediaId, connection, receiver); |
| } |
| }); |
| } |
| |
| // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. |
| public void registerCallbacks(final ServiceCallbacks callbacks, final String pkg, |
| final int pid, final int uid, final Bundle rootHints) { |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| // Clear out the old subscriptions. We are getting new ones. |
| mConnections.remove(b); |
| |
| ConnectionRecord connection = null; |
| Iterator<ConnectionRecord> iter = mPendingConnections.iterator(); |
| while (iter.hasNext()) { |
| ConnectionRecord pendingConnection = iter.next(); |
| // Note: We cannot use Map/Set for mPendingConnections but List because |
| // multiple MediaBrowserCompats with the same UID can request connect. |
| if (pendingConnection.uid == uid) { |
| // If caller hasn't set pkg and pid, do the best effort to get it. |
| if (TextUtils.isEmpty(pkg) || pid <= 0) { |
| // Note: Do not assign pendingConnection directly because it doesn't |
| // have callback information. |
| connection = new ConnectionRecord(pendingConnection.pkg, |
| pendingConnection.pid, pendingConnection.uid, |
| rootHints, callbacks); |
| } |
| iter.remove(); |
| break; |
| } |
| } |
| if (connection == null) { |
| connection = new ConnectionRecord(pkg, pid, uid, rootHints, callbacks); |
| } |
| mConnections.put(b, connection); |
| try { |
| b.linkToDeath(connection, 0); |
| } catch (RemoteException e) { |
| Log.w(TAG, "IBinder is already dead."); |
| } |
| } |
| }); |
| } |
| |
| // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. |
| public void unregisterCallbacks(final ServiceCallbacks callbacks) { |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| ConnectionRecord old = mConnections.remove(b); |
| if (old != null) { |
| b.unlinkToDeath(old, 0); |
| } |
| } |
| }); |
| } |
| |
| public void search(final String query, final Bundle extras, final ResultReceiver receiver, |
| final ServiceCallbacks callbacks) { |
| if (TextUtils.isEmpty(query) || receiver == null) { |
| return; |
| } |
| |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| ConnectionRecord connection = mConnections.get(b); |
| if (connection == null) { |
| Log.w(TAG, "search for callback that isn't registered query=" + query); |
| return; |
| } |
| performSearch(query, extras, connection, receiver); |
| } |
| }); |
| } |
| |
| public void sendCustomAction(final String action, final Bundle extras, |
| final ResultReceiver receiver, final ServiceCallbacks callbacks) { |
| if (TextUtils.isEmpty(action) || receiver == null) { |
| return; |
| } |
| |
| mHandler.postOrRun(new Runnable() { |
| @Override |
| public void run() { |
| final IBinder b = callbacks.asBinder(); |
| |
| ConnectionRecord connection = mConnections.get(b); |
| if (connection == null) { |
| Log.w(TAG, "sendCustomAction for callback that isn't registered action=" |
| + action + ", extras=" + extras); |
| return; |
| } |
| performCustomAction(action, extras, connection, receiver); |
| } |
| }); |
| } |
| } |
| |
| private interface ServiceCallbacks { |
| IBinder asBinder(); |
| void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) |
| throws RemoteException; |
| void onConnectFailed() throws RemoteException; |
| void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options, |
| Bundle notifyChildrenChangedOptions) throws RemoteException; |
| } |
| |
| private static class ServiceCallbacksCompat implements ServiceCallbacks { |
| final Messenger mCallbacks; |
| |
| ServiceCallbacksCompat(Messenger callbacks) { |
| mCallbacks = callbacks; |
| } |
| |
| @Override |
| public IBinder asBinder() { |
| return mCallbacks.getBinder(); |
| } |
| |
| @Override |
| public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras) |
| throws RemoteException { |
| if (extras == null) { |
| extras = new Bundle(); |
| } |
| extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); |
| Bundle data = new Bundle(); |
| data.putString(DATA_MEDIA_ITEM_ID, root); |
| data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session); |
| data.putBundle(DATA_ROOT_HINTS, extras); |
| sendRequest(SERVICE_MSG_ON_CONNECT, data); |
| } |
| |
| @Override |
| public void onConnectFailed() throws RemoteException { |
| sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); |
| } |
| |
| @SuppressWarnings({"rawtypes", "unchecked"}) |
| @Override |
| public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, |
| Bundle options, Bundle notifyChildrenChangedOptions) throws RemoteException { |
| Bundle data = new Bundle(); |
| data.putString(DATA_MEDIA_ITEM_ID, mediaId); |
| data.putBundle(DATA_OPTIONS, options); |
| data.putBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS, notifyChildrenChangedOptions); |
| if (list != null) { |
| data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST, |
| list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list)); |
| } |
| sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data); |
| } |
| |
| private void sendRequest(int what, Bundle data) throws RemoteException { |
| Message msg = Message.obtain(); |
| msg.what = what; |
| msg.arg1 = SERVICE_VERSION_CURRENT; |
| msg.setData(data); |
| mCallbacks.send(msg); |
| } |
| } |
| |
| @RequiresApi(21) |
| @SuppressWarnings({"rawtypes", "unchecked"}) |
| static class ResultWrapper<T> { |
| MediaBrowserService.Result mResultFwk; |
| |
| ResultWrapper(MediaBrowserService.Result result) { |
| mResultFwk = result; |
| } |
| |
| public void sendResult(T result) { |
| if (result instanceof List) { |
| mResultFwk.sendResult(parcelListToItemList((List<Parcel>) result)); |
| } else if (result instanceof Parcel) { |
| Parcel parcel = (Parcel) result; |
| parcel.setDataPosition(0); |
| mResultFwk.sendResult(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel)); |
| parcel.recycle(); |
| } else { |
| // The result is null or an invalid instance. |
| mResultFwk.sendResult(null); |
| } |
| } |
| |
| public void detach() { |
| mResultFwk.detach(); |
| } |
| |
| List<MediaBrowser.MediaItem> parcelListToItemList(List<Parcel> parcelList) { |
| if (parcelList == null) { |
| return null; |
| } |
| List<MediaBrowser.MediaItem> items = new ArrayList<>(parcelList.size()); |
| for (Parcel parcel : parcelList) { |
| parcel.setDataPosition(0); |
| items.add(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel)); |
| parcel.recycle(); |
| } |
| return items; |
| } |
| } |
| |
| /** |
| * Attaches to the base context. This method is added to change the visibility of |
| * {@link Service#attachBaseContext(Context)}. |
| * <p> |
| * Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it |
| * because lint checks considers the overriden method as the new public API that needs update |
| * of current.txt. |
| * |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public void attachToBaseContext(Context base) { |
| attachBaseContext(base); |
| } |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| if (Build.VERSION.SDK_INT >= 28) { |
| mImpl = new MediaBrowserServiceImplApi28(); |
| } else if (Build.VERSION.SDK_INT >= 26) { |
| mImpl = new MediaBrowserServiceImplApi26(); |
| } else if (Build.VERSION.SDK_INT >= 23) { |
| mImpl = new MediaBrowserServiceImplApi23(); |
| } else if (Build.VERSION.SDK_INT >= 21) { |
| mImpl = new MediaBrowserServiceImplApi21(); |
| } else { |
| mImpl = new MediaBrowserServiceImplBase(); |
| } |
| mImpl.onCreate(); |
| } |
| |
| @CallSuper |
| @MainThread |
| @Override |
| public void onDestroy() { |
| mHandler.release(); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mImpl.onBind(intent); |
| } |
| |
| @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 browse 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 MediaBrowserCompat.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<MediaBrowserCompat.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 MediaBrowserCompat.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 A bundle of service-specific arguments sent from the media |
| * browse. The information returned through the result should be |
| * affected by the contents of this bundle. |
| */ |
| public void onLoadChildren(@NonNull String parentId, |
| @NonNull Result<List<MediaBrowserCompat.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 when a {@link MediaBrowserCompat#subscribe} is called. |
| * |
| * @param id id |
| * @param option option |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void onSubscribe(String id, Bundle option) { |
| } |
| |
| /** |
| * Called when a {@link MediaBrowserCompat#unsubscribe} is called. |
| * |
| * @param id |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void onUnsubscribe(String id) { |
| } |
| |
| /** |
| * 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 MediaBrowserCompat.ItemCallback#onError}. |
| * |
| * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}. |
| * @param result The Result to send the item to, or null if the id is |
| * invalid. |
| */ |
| public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) { |
| result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED); |
| result.sendResult(null); |
| } |
| |
| /** |
| * Called to get the search result. |
| * <p> |
| * Implementations must call {@link Result#sendResult result.sendResult}. If the search 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 |
| * search has been completed. |
| * </p><p> |
| * In case there are no search results, call {@link Result#sendResult result.sendResult} with an |
| * empty list. In case there are some errors happened, call {@link Result#sendResult |
| * result.sendResult} with {@code null}, which will invoke {@link |
| * MediaBrowserCompat.SearchCallback#onError}. |
| * </p><p> |
| * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}. |
| * </p> |
| * |
| * @param query The search query sent from the media browser. It contains keywords separated |
| * by space. |
| * @param extras The bundle of service-specific arguments sent from the media browser. |
| * @param result The {@link Result} to send the search result. |
| */ |
| public void onSearch(@NonNull String query, Bundle extras, |
| @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) { |
| result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED); |
| result.sendResult(null); |
| } |
| |
| /** |
| * Called to request a custom action to this service. |
| * <p> |
| * Implementations must call either {@link Result#sendResult} or {@link Result#sendError}. If |
| * the requested custom action will be an expensive operation {@link Result#detach} may be |
| * called before returning from this function, and then the service can send the result later |
| * when the custom action is completed. Implementation can also call |
| * {@link Result#sendProgressUpdate} to send an interim update to the requester. |
| * </p><p> |
| * If the requested custom action is not supported by this service, call |
| * {@link Result#sendError}. The default implementation will invoke {@link Result#sendError}. |
| * </p> |
| * |
| * @param action The custom action sent from the media browser. |
| * @param extras The bundle of service-specific arguments sent from the media browser. |
| * @param result The {@link Result} to send the result of the requested custom action. |
| * @see MediaBrowserCompat#CUSTOM_ACTION_DOWNLOAD |
| * @see MediaBrowserCompat#CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE |
| */ |
| public void onCustomAction(@NonNull String action, Bundle extras, |
| @NonNull Result<Bundle> result) { |
| result.sendError(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 MediaSessionCompat}. |
| */ |
| public void setSessionToken(MediaSessionCompat.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; |
| mImpl.setSessionToken(token); |
| } |
| |
| /** |
| * Gets the session token, or null if it has not yet been created |
| * or if it has been destroyed. |
| */ |
| @Nullable |
| public MediaSessionCompat.Token getSessionToken() { |
| return mSession; |
| } |
| |
| /** |
| * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}. |
| * 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. |
| * <p> |
| * Note that this will return null when connected to {@link android.media.browse.MediaBrowser} |
| * and running on API 23 or lower. |
| * |
| * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren}, |
| * {@link #onLoadItem} or {@link #onSearch}. |
| * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT |
| * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE |
| * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED |
| */ |
| public final Bundle getBrowserRootHints() { |
| return mImpl.getBrowserRootHints(); |
| } |
| |
| /** |
| * 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) |
| */ |
| @NonNull |
| public final RemoteUserInfo getCurrentBrowserInfo() { |
| return mImpl.getCurrentBrowserInfo(); |
| } |
| |
| /** |
| * 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) { |
| if (parentId == null) { |
| throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); |
| } |
| mImpl.notifyChildrenChanged(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 A bundle of service-specific arguments to send |
| * to the media browse. The contents of this bundle may |
| * contain the information about the change. |
| */ |
| public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { |
| if (parentId == null) { |
| throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); |
| } |
| if (options == null) { |
| throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); |
| } |
| mImpl.notifyChildrenChanged(parentId, options); |
| } |
| |
| /** |
| * Notifies a 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 remoteUserInfo to receive this event. |
| * @param parentId The id of the parent media item whose |
| * children changed. |
| * @param options A bundle of service-specific arguments to send |
| * to the media browse. The contents of this bundle may |
| * contain the information about the change. |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public void notifyChildrenChanged(@NonNull RemoteUserInfo remoteUserInfo, |
| @NonNull String parentId, @NonNull Bundle options) { |
| if (remoteUserInfo == null) { |
| throw new IllegalArgumentException("remoteUserInfo cannot be null in" |
| + " notifyChildrenChanged"); |
| } |
| if (parentId == null) { |
| throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); |
| } |
| if (options == null) { |
| throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); |
| } |
| mImpl.notifyChildrenChanged(remoteUserInfo, parentId, options); |
| } |
| |
| // Package visibility to avoid synthetic accessor. |
| /* package */ void handleMessageInternal(Message msg) { |
| Bundle data = msg.getData(); |
| switch (msg.what) { |
| case CLIENT_MSG_CONNECT: { |
| Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); |
| MediaSessionCompat.ensureClassLoader(rootHints); |
| |
| mServiceBinderImpl.connect( |
| data.getString(DATA_PACKAGE_NAME), |
| data.getInt(DATA_CALLING_PID), |
| data.getInt(DATA_CALLING_UID), |
| rootHints, |
| new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| } |
| case CLIENT_MSG_DISCONNECT: |
| mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| case CLIENT_MSG_ADD_SUBSCRIPTION: { |
| Bundle options = data.getBundle(DATA_OPTIONS); |
| MediaSessionCompat.ensureClassLoader(options); |
| |
| mServiceBinderImpl.addSubscription( |
| data.getString(DATA_MEDIA_ITEM_ID), |
| BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN), |
| options, |
| new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| } |
| case CLIENT_MSG_REMOVE_SUBSCRIPTION: |
| mServiceBinderImpl.removeSubscription( |
| data.getString(DATA_MEDIA_ITEM_ID), |
| BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN), |
| new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| case CLIENT_MSG_GET_MEDIA_ITEM: |
| mServiceBinderImpl.getMediaItem( |
| data.getString(DATA_MEDIA_ITEM_ID), |
| (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), |
| new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER: { |
| Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); |
| MediaSessionCompat.ensureClassLoader(rootHints); |
| |
| mServiceBinderImpl.registerCallbacks( |
| new ServiceCallbacksCompat(msg.replyTo), |
| data.getString(DATA_PACKAGE_NAME), |
| data.getInt(DATA_CALLING_PID), |
| data.getInt(DATA_CALLING_UID), |
| rootHints); |
| break; |
| } |
| case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER: |
| mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| case CLIENT_MSG_SEARCH: { |
| Bundle searchExtras = data.getBundle(DATA_SEARCH_EXTRAS); |
| MediaSessionCompat.ensureClassLoader(searchExtras); |
| |
| mServiceBinderImpl.search( |
| data.getString(DATA_SEARCH_QUERY), |
| searchExtras, |
| (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), |
| new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| } |
| case CLIENT_MSG_SEND_CUSTOM_ACTION: { |
| Bundle customActionExtras = data.getBundle(DATA_CUSTOM_ACTION_EXTRAS); |
| MediaSessionCompat.ensureClassLoader(customActionExtras); |
| |
| mServiceBinderImpl.sendCustomAction( |
| data.getString(DATA_CUSTOM_ACTION), |
| customActionExtras, |
| (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER), |
| new ServiceCallbacksCompat(msg.replyTo)); |
| break; |
| } |
| default: |
| Log.w( |
| TAG, |
| "Unhandled message: " |
| + msg |
| + "\n Service version: " |
| + SERVICE_VERSION_CURRENT |
| + "\n Client version: " |
| + msg.arg1); |
| } |
| } |
| |
| /** |
| * Return whether the given package is one of the ones that is owned by the uid. |
| */ |
| 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. |
| */ |
| 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 |
| && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) { |
| return; |
| } |
| } |
| callbackList.add(new Pair<>(token, options)); |
| connection.subscriptions.put(id, callbackList); |
| // send the results |
| performLoadChildren(id, connection, options, null); |
| |
| mCurConnection = connection; |
| onSubscribe(id, options); |
| mCurConnection = null; |
| } |
| |
| /** |
| * Remove the subscription. |
| */ |
| boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) { |
| try { |
| 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; |
| } finally { |
| mCurConnection = connection; |
| onUnsubscribe(id); |
| mCurConnection = null; |
| } |
| } |
| |
| /** |
| * Call onLoadChildren and then send the results back to the connection. |
| * <p> |
| * Callers must make sure that this connection is still connected. |
| */ |
| void performLoadChildren(final String parentId, final ConnectionRecord connection, |
| final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions) { |
| final Result<List<MediaBrowserCompat.MediaItem>> result |
| = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) { |
| @Override |
| void onResultSent(@Nullable List<MediaBrowserCompat.MediaItem> list) { |
| if (mConnections.get(connection.callbacks.asBinder()) != connection) { |
| if (DEBUG) { |
| Log.d(TAG, "Not sending onLoadChildren result for connection that has" |
| + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); |
| } |
| return; |
| } |
| |
| List<MediaBrowserCompat.MediaItem> filteredList = |
| (getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 |
| ? applyOptions(list, subscribeOptions) : list; |
| try { |
| connection.callbacks.onLoadChildren(parentId, filteredList, subscribeOptions, |
| notifyChildrenChangedOptions); |
| } 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 (subscribeOptions == null) { |
| onLoadChildren(parentId, result); |
| } else { |
| onLoadChildren(parentId, result, subscribeOptions); |
| } |
| mCurConnection = null; |
| |
| if (!result.isDone()) { |
| throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" |
| + " before returning for package=" + connection.pkg + " id=" + parentId); |
| } |
| } |
| |
| List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list, |
| final Bundle options) { |
| if (list == null) { |
| return null; |
| } |
| int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); |
| int pageSize = options.getInt(MediaBrowserCompat.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.emptyList(); |
| } |
| if (toIndex > list.size()) { |
| toIndex = list.size(); |
| } |
| return list.subList(fromIndex, toIndex); |
| } |
| |
| void performLoadItem(String itemId, ConnectionRecord connection, |
| final ResultReceiver receiver) { |
| final Result<MediaBrowserCompat.MediaItem> result = |
| new Result<MediaBrowserCompat.MediaItem>(itemId) { |
| @Override |
| void onResultSent(@Nullable MediaBrowserCompat.MediaItem item) { |
| if ((getFlags() & 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); |
| } |
| } |
| |
| void performSearch(final String query, Bundle extras, ConnectionRecord connection, |
| final ResultReceiver receiver) { |
| final Result<List<MediaBrowserCompat.MediaItem>> result = |
| new Result<List<MediaBrowserCompat.MediaItem>>(query) { |
| @Override |
| void onResultSent(@Nullable List<MediaBrowserCompat.MediaItem> items) { |
| if ((getFlags() & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0 |
| || items == null) { |
| receiver.send(RESULT_ERROR, null); |
| return; |
| } |
| Bundle bundle = new Bundle(); |
| bundle.putParcelableArray(KEY_SEARCH_RESULTS, |
| items.toArray(new MediaBrowserCompat.MediaItem[0])); |
| receiver.send(RESULT_OK, bundle); |
| } |
| }; |
| |
| mCurConnection = connection; |
| onSearch(query, extras, result); |
| mCurConnection = null; |
| |
| if (!result.isDone()) { |
| throw new IllegalStateException("onSearch must call detach() or sendResult()" |
| + " before returning for query=" + query); |
| } |
| } |
| |
| void performCustomAction(final String action, Bundle extras, ConnectionRecord connection, |
| final ResultReceiver receiver) { |
| final Result<Bundle> result = new Result<Bundle>(action) { |
| @Override |
| void onResultSent(@Nullable Bundle result) { |
| receiver.send(RESULT_OK, result); |
| } |
| |
| @Override |
| void onProgressUpdateSent(@Nullable Bundle data) { |
| receiver.send(RESULT_PROGRESS_UPDATE, data); |
| } |
| |
| @Override |
| void onErrorSent(@Nullable Bundle data) { |
| receiver.send(RESULT_ERROR, data); |
| } |
| }; |
| |
| mCurConnection = connection; |
| onCustomAction(action, extras, result); |
| mCurConnection = null; |
| |
| if (!result.isDone()) { |
| throw new IllegalStateException("onCustomAction must call detach() or sendResult() or " |
| + "sendError() before returning for action=" + action + " extras=" |
| + extras); |
| } |
| } |
| |
| /** |
| * 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 MediaBrowserCompat.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"; |
| |
| /** |
| * The lookup key for a string that indicates specific keywords which will be considered |
| * when the browser service suggests media items. |
| * |
| * <p>When creating a media browser for a given media browser service, this key can be |
| * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested |
| * media items related with the keywords. 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 |
| * @see #EXTRA_SUGGESTED |
| * @deprecated The search functionality is now supported by the methods |
| * {@link MediaBrowserCompat#search} and {@link #onSearch}. Use those methods |
| * instead. |
| */ |
| @Deprecated |
| public static final String EXTRA_SUGGESTION_KEYWORDS |
| = "android.service.media.extra.SUGGESTION_KEYWORDS"; |
| |
| final private String mRootId; |
| final private 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; |
| } |
| } |
| } |