/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.Service; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.media.browse.MediaBrowser; import android.media.browse.MediaBrowserUtils; import android.media.session.MediaSession; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.service.media.IMediaBrowserService; import android.service.media.IMediaBrowserServiceCallbacks; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; /** * Base class for media browse services. *
* 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 MediaSession}. *
* * 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: ** <service android:name=".MyMediaBrowserService" * android:label="@string/service_name" > * <intent-filter> * <action android:name="android.media.browse.MediaBrowserService" /> * </intent-filter> * </service> ** */ public abstract class MediaBrowserService extends Service { private static final String TAG = "MediaBrowserService"; private static final boolean DBG = false; /** * The {@link Intent} that must be declared as handled by the service. */ @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; /** * A key for passing the MediaItem to the ResultReceiver in getItem. * * @hide */ public static final String KEY_MEDIA_ITEM = "media_item"; private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED }) private @interface ResultFlags { } private final ArrayMap
* Each of the methods that takes one of these to send the result must call
* {@link #sendResult} to respond to the caller with the given results. If those
* functions return without calling {@link #sendResult}, they must instead call
* {@link #detach} before returning, and then may call {@link #sendResult} when
* they are done. If more than one of those methods is called, an exception will
* be thrown.
*
* @see #onLoadChildren
* @see #onLoadItem
*/
public class Result
* 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.
*
* 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.
*
* In case the media item does not have any children, call {@link Result#sendResult}
* with an empty list. When the given {@code parentId} is invalid, implementations must
* call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
* {@link MediaBrowser.SubscriptionCallback#onError}.
*
* 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.
*
* In case the media item does not have any children, call {@link Result#sendResult}
* with an empty list. When the given {@code parentId} is invalid, implementations must
* call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
* {@link MediaBrowser.SubscriptionCallback#onError}.
*
* 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.
*
* When the given {@code itemId} is invalid, implementations must call
* {@link Result#sendResult result.sendResult} with {@code null}, which will
* invoke {@link MediaBrowser.ItemCallback#onError}.
*
* The default implementation calls {@link Result#sendResult result.sendResult}
* with {@code null}.
*
* This should be called as soon as possible during the service's startup.
* It may only be called once.
*
* @param token The token for the service's {@link MediaSession}.
*/
public void setSessionToken(final MediaSession.Token token) {
if (token == null) {
throw new IllegalArgumentException("Session token may not be null.");
}
if (mSession != null) {
throw new IllegalStateException("The session token has already been set.");
}
mSession = token;
mHandler.post(new Runnable() {
@Override
public void run() {
for (IBinder key : mConnections.keySet()) {
ConnectionRecord connection = mConnections.get(key);
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.");
mConnections.remove(key);
}
}
}
});
}
/**
* Gets the session token, or null if it has not yet been created
* or if it has been destroyed.
*/
public @Nullable MediaSession.Token getSessionToken() {
return mSession;
}
/**
* Gets the root hints sent from the currently connected {@link MediaBrowser}.
* The root hints are service-specific arguments included in an optional bundle sent to the
* media browser service when connecting and retrieving the root id for browsing, or null if
* none. The contents of this bundle may affect the information returned when browsing.
*
* @throws IllegalStateException If this method is called outside of {@link #onLoadChildren}
* or {@link #onLoadItem}
* @see MediaBrowserService.BrowserRoot#EXTRA_RECENT
* @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
* @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
*/
public final Bundle getBrowserRootHints() {
if (mCurConnection == null) {
throw new IllegalStateException("This should be called inside of onLoadChildren or"
+ " onLoadItem methods");
}
return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
}
/**
* Notifies all connected media browsers that the children of
* the specified parent id have changed in some way.
* This will cause browsers to fetch subscribed content again.
*
* @param parentId The id of the parent media item whose
* children changed.
*/
public void notifyChildrenChanged(@NonNull String parentId) {
notifyChildrenChangedInternal(parentId, null);
}
/**
* Notifies all connected media browsers that the children of
* the specified parent id have changed in some way.
* This will cause browsers to fetch subscribed content again.
*
* @param parentId The id of the parent media item whose
* children changed.
* @param options 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 (options == null) {
throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
}
notifyChildrenChangedInternal(parentId, options);
}
private void notifyChildrenChangedInternal(final String parentId, final Bundle options) {
if (parentId == null) {
throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
}
mHandler.post(new Runnable() {
@Override
public void run() {
for (IBinder binder : mConnections.keySet()) {
ConnectionRecord connection = mConnections.get(binder);
List
* Callers must make sure that this connection is still connected.
*/
private void performLoadChildren(final String parentId, final ConnectionRecord connection,
final Bundle options) {
final Result 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.
*
* 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.
*
* 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.
*
* 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.
*
* When creating a media browser for a given media browser service, this key can be
* supplied as a root hint for retrieving the media items suggested by the media browser
* service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
* is considered ordered by relevance, first being the top suggestion.
* If the media browser service can provide such media items, the implementation must return
* the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
*
* The root hint may contain multiple keys.
*
* @see #EXTRA_RECENT
* @see #EXTRA_OFFLINE
*/
public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
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;
}
}
}
> result);
/**
* Called to get information about the children of a media item.
*
> result, @NonNull Bundle options) {
// To support backward compatibility, when the implementation of MediaBrowserService doesn't
// override onLoadChildren() with options, onLoadChildren() without options will be used
// instead, and the options will be applied in the implementation of result.onResultSent().
result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
onLoadChildren(parentId, result);
}
/**
* Called to get information about a specific media item.
*
> result
= new Result
>(parentId) {
@Override
void onResultSent(List