/* * 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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; import static android.support.v4.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT; import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION; import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; import static android.support.v4.media.MediaBrowserProtocol.DATA_OPTIONS; import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME; import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER; import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS; import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER; import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.BadParcelableException; 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.RequiresApi; import android.support.annotation.RestrictTo; import android.support.v4.app.BundleCompat; import android.support.v4.media.session.IMediaSession; import android.support.v4.media.session.MediaControllerCompat.TransportControls; 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; /** * 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. *
* All callback methods will be called from the thread on which the browser was constructed. *
* *For information about building your media application, read the * Media Apps developer guide.
** 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); } /** * Searches {@link MediaItem media items} from the connected service. Not all services may * support this, and {@link SearchCallback#onError} will be called if not implemented. * * @param query The search query that contains keywords separated by space. Should not be an * empty string. * @param extras The bundle of service-specific arguments to send to the media browser service. * The contents of this bundle may affect the search result. * @param callback The callback to receive the search result. Must be non-null. * @throws IllegalStateException if the browser is not connected to the media browser service. */ public void search(@NonNull final String query, final Bundle extras, @NonNull SearchCallback callback) { if (TextUtils.isEmpty(query)) { throw new IllegalArgumentException("query cannot be empty"); } if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } mImpl.search(query, extras, callback); } /** * Sends a custom action to the connected service. If the service doesn't support the given * action, {@link CustomActionCallback#onError} will be called. * * @param action The custom action that will be sent to the connected service. Should not be an * empty string. * @param extras The bundle of service-specific arguments to send to the media browser service. * @param callback The callback to receive the result of the custom action. * @see #CUSTOM_ACTION_DOWNLOAD * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE */ public void sendCustomAction(@NonNull String action, Bundle extras, @Nullable CustomActionCallback callback) { if (TextUtils.isEmpty(action)) { throw new IllegalArgumentException("action cannot be empty"); } mImpl.sendCustomAction(action, extras, callback); } /** * A class with information on a single media item for use in browsing/searching media. * MediaItems are application dependent so we cannot guarantee that they contain the * right values. */ public static class MediaItem implements Parcelable { private final int mFlags; private final MediaDescriptionCompat mDescription; /** @hide */ @RestrictTo(LIBRARY_GROUP) @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 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);
}
Bundle copiedOptions = options == null ? null : new Bundle(options);
sub.putCallback(copiedOptions, 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 (isConnected()) {
try {
mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
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 (isConnected()) {
mServiceBinderWrapper.removeSubscription(parentId, null,
mCallbacksMessenger);
}
} else {
final List