/* * Copyright (C) 2015 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.support.v4.media; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; 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.Parcelable; import android.os.RemoteException; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.app.BundleCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.os.BuildCompat; import android.support.v4.os.ResultReceiver; import android.support.v4.util.ArrayMap; import android.text.TextUtils; import android.util.Log; 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.List; import java.util.Map; import static android.support.annotation.RestrictTo.Scope.GROUP_ID; import static android.support.v4.media.MediaBrowserProtocol.*; /** * Browses media content offered by a {@link MediaBrowserServiceCompat}. *
* This object is not thread-safe. All calls should happen on the thread on which the browser * was constructed. *
*/ public final class MediaBrowserCompat { static final String TAG = "MediaBrowserCompat"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** * Used as an int extra field to denote the page number to subscribe. * The value of {@code EXTRA_PAGE} should be greater than or equal to 1. * * @see android.service.media.MediaBrowserService.BrowserRoot * @see #EXTRA_PAGE_SIZE */ public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE"; /** * Used as an int extra field to denote the number of media items in a page. * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1. * * @see android.service.media.MediaBrowserService.BrowserRoot * @see #EXTRA_PAGE */ public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; private final MediaBrowserImpl mImpl; /** * Creates a media browser for the specified media browse service. * * @param context The context. * @param serviceComponent The component name of the media browse service. * @param callback The connection callback. * @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. * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED */ public MediaBrowserCompat(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints) { if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) { mImpl = new MediaBrowserImplApi24(context, serviceComponent, callback, rootHints); } else if (Build.VERSION.SDK_INT >= 23) { mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints); } else if (Build.VERSION.SDK_INT >= 21) { mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints); } else { mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints); } } /** * Connects to the media browse service. ** The connection callback specified in the constructor will be invoked * when the connection completes or fails. *
*/ public void connect() { mImpl.connect(); } /** * Disconnects from the media browse service. * After this, no more callbacks will be received. */ public void disconnect() { mImpl.disconnect(); } /** * Returns whether the browser is connected to the service. */ public boolean isConnected() { return mImpl.isConnected(); } /** * Gets the service component that the media browser is connected to. */ public @NonNull ComponentName getServiceComponent() { return mImpl.getServiceComponent(); } /** * Gets the root id. ** Note that the root id may become invalid or change when when the * browser is disconnected. *
* * @throws IllegalStateException if not connected. */ public @NonNull String getRoot() { return mImpl.getRoot(); } /** * Gets any extras for the media service. * * @throws IllegalStateException if not connected. */ public @Nullable Bundle getExtras() { return mImpl.getExtras(); } /** * Gets the media session token associated with the media browser. ** Note that the session token may become invalid or change when when the * browser is disconnected. *
* * @return The session token for the browser, never null. * * @throws IllegalStateException if not connected. */ public @NonNull MediaSessionCompat.Token getSessionToken() { return mImpl.getSessionToken(); } /** * Queries for information about the media items that are contained within * the specified id and subscribes to receive updates when they change. ** The list of subscriptions is maintained even when not connected and is * restored after the reconnection. It is ok to subscribe while not connected * but the results will not be returned until the connection completes. *
** If the id is already subscribed with a different callback then the new * callback will replace the previous one and the child data will be * reloaded. *
* * @param parentId The id of the parent media item whose list of children * will be subscribed. * @param callback The callback to receive the list of children. */ public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { // Check arguments. if (TextUtils.isEmpty(parentId)) { throw new IllegalArgumentException("parentId is empty"); } if (callback == null) { throw new IllegalArgumentException("callback is null"); } mImpl.subscribe(parentId, null, callback); } /** * Queries with service-specific arguments for information about the media items * that are contained within the specified id and subscribes to receive updates * when they change. ** The list of subscriptions is maintained even when not connected and is * restored after the reconnection. It is ok to subscribe while not connected * but the results will not be returned until the connection completes. *
** If the id is already subscribed with a different callback then the new * callback will replace the previous one and the child data will be * reloaded. *
* * @param parentId The id of the parent media item whose list of children * will be subscribed. * @param options A bundle of service-specific arguments to send to the media * browse service. The contents of this bundle may affect the * information returned when browsing. * @param callback The callback to receive the list of children. */ public void subscribe(@NonNull String parentId, @NonNull Bundle options, @NonNull SubscriptionCallback callback) { // Check arguments. if (TextUtils.isEmpty(parentId)) { throw new IllegalArgumentException("parentId is empty"); } if (callback == null) { throw new IllegalArgumentException("callback is null"); } if (options == null) { throw new IllegalArgumentException("options are null"); } mImpl.subscribe(parentId, options, callback); } /** * Unsubscribes for changes to the children of the specified media id. ** The query callback will no longer be invoked for results associated with * this id once this method returns. *
* * @param parentId The id of the parent media item whose list of children * will be unsubscribed. */ public void unsubscribe(@NonNull String parentId) { // Check arguments. if (TextUtils.isEmpty(parentId)) { throw new IllegalArgumentException("parentId is empty"); } mImpl.unsubscribe(parentId, null); } /** * Unsubscribes for changes to the children of the specified media id. ** The query callback will no longer be invoked for results associated with * this id once this method returns. *
* * @param parentId The id of the parent media item whose list of children * will be unsubscribed. * @param callback A callback sent to the media browse service to subscribe. */ public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { // Check arguments. if (TextUtils.isEmpty(parentId)) { throw new IllegalArgumentException("parentId is empty"); } if (callback == null) { throw new IllegalArgumentException("callback is null"); } mImpl.unsubscribe(parentId, callback); } /** * Retrieves a specific {@link MediaItem} from the connected service. Not * all services may support this, so falling back to subscribing to the * parent's id should be used when unavailable. * * @param mediaId The id of the item to retrieve. * @param cb The callback to receive the result on. */ public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) { mImpl.getItem(mediaId, cb); } /** * A class with information on a single media item for use in browsing media. */ public static class MediaItem implements Parcelable { private final int mFlags; private final MediaDescriptionCompat mDescription; /** @hide */ @RestrictTo(GROUP_ID) @Retention(RetentionPolicy.SOURCE) @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE }) public @interface Flags { } /** * Flag: Indicates that the item has children of its own. */ public static final int FLAG_BROWSABLE = 1 << 0; /** * Flag: Indicates that the item is playable. ** The id of this item may be passed to * {@link MediaControllerCompat.TransportControls#playFromMediaId(String, Bundle)} * to start playing it. *
*/ public static final int FLAG_PLAYABLE = 1 << 1; /** * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem} * object. ** This method is only supported on API 21+. On API 20 and below, it returns null. *
* * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object. * @return An equivalent {@link MediaItem} object, or null if none. */ public static MediaItem fromMediaItem(Object itemObj) { if (itemObj == null || Build.VERSION.SDK_INT < 21) { return null; } int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj); MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription( MediaBrowserCompatApi21.MediaItem.getDescription(itemObj)); return new MediaItem(description, flags); } /** * Creates a list of {@link MediaItem} objects from a framework * {@link android.media.browse.MediaBrowser.MediaItem} object list. ** This method is only supported on API 21+. On API 20 and below, it returns null. *
* * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects. * @return An equivalent list of {@link MediaItem} objects, or null if none. */ public static List* If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} * called, because some errors may heal themselves. *
* * @param parentId The media id of the parent media item whose children could not be loaded. */ public void onError(@NonNull String parentId) { } /** * Called when the id doesn't exist or other errors in subscribing. ** If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} * called, because some errors may heal themselves. *
* * @param parentId The media id of the parent media item whose children could * not be loaded. * @param options A bundle of service-specific arguments sent to the media * browse service. */ public void onError(@NonNull String parentId, @NonNull Bundle options) { } private void setSubscription(Subscription subscription) { mSubscriptionRef = new WeakReference(subscription); } private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback { StubApi21() { } @Override public void onChildrenLoaded(@NonNull String parentId, List> children) { Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get(); if (sub == null) { SubscriptionCallback.this.onChildrenLoaded( parentId, MediaItem.fromMediaItemList(children)); } else { List
* Everywhere that calls this EXCEPT for disconnect() should follow it with
* a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
* for a clean shutdown, but everywhere else is a dirty shutdown and should
* notify the app.
*/
void forceCloseConnection() {
if (mServiceConnection != null) {
mContext.unbindService(mServiceConnection);
}
mState = CONNECT_STATE_DISCONNECTED;
mServiceConnection = null;
mServiceBinderWrapper = null;
mCallbacksMessenger = null;
mHandler.setCallbacksMessenger(null);
mRootId = null;
mMediaSessionToken = null;
}
@Override
public boolean isConnected() {
return mState == CONNECT_STATE_CONNECTED;
}
@Override
public @NonNull ComponentName getServiceComponent() {
if (!isConnected()) {
throw new IllegalStateException("getServiceComponent() called while not connected" +
" (state=" + mState + ")");
}
return mServiceComponent;
}
@Override
public @NonNull String getRoot() {
if (!isConnected()) {
throw new IllegalStateException("getRoot() called while not connected"
+ "(state=" + getStateLabel(mState) + ")");
}
return mRootId;
}
@Override
public @Nullable Bundle getExtras() {
if (!isConnected()) {
throw new IllegalStateException("getExtras() called while not connected (state="
+ getStateLabel(mState) + ")");
}
return mExtras;
}
@Override
public @NonNull MediaSessionCompat.Token getSessionToken() {
if (!isConnected()) {
throw new IllegalStateException("getSessionToken() called while not connected"
+ "(state=" + mState + ")");
}
return mMediaSessionToken;
}
@Override
public void subscribe(@NonNull String parentId, Bundle options,
@NonNull SubscriptionCallback callback) {
// Update or create the subscription.
Subscription sub = mSubscriptions.get(parentId);
if (sub == null) {
sub = new Subscription();
mSubscriptions.put(parentId, sub);
}
sub.putCallback(options, callback);
// If we are connected, tell the service that we are watching. If we aren't
// connected, the service will be told when we connect.
if (mState == CONNECT_STATE_CONNECTED) {
try {
mServiceBinderWrapper.addSubscription(parentId, callback.mToken, options,
mCallbacksMessenger);
} catch (RemoteException e) {
// Process is crashing. We will disconnect, and upon reconnect we will
// automatically reregister. So nothing to do here.
Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
}
}
}
@Override
public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
Subscription sub = mSubscriptions.get(parentId);
if (sub == null) {
return;
}
// Tell the service if necessary.
try {
if (callback == null) {
if (mState == CONNECT_STATE_CONNECTED) {
mServiceBinderWrapper.removeSubscription(parentId, null,
mCallbacksMessenger);
}
} else {
final List