/* * 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.GROUP_ID; 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_UNREGISTER_CALLBACK_MESSENGER; import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID; 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.EXTRA_CLIENT_VERSION; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; 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 static android.support.v4.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT; import android.app.Service; import android.content.Intent; import android.content.pm.PackageManager; 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.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.MediaSessionCompat; import android.support.v4.os.BuildCompat; import android.support.v4.os.ResultReceiver; import android.support.v4.util.ArrayMap; import android.support.v4.util.Pair; import android.text.TextUtils; import android.util.Log; 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. *
* 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}. *
* * 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=".MyMediaBrowserServiceCompat" * android:label="@string/service_name" > * <intent-filter> * <action android:name="android.media.browse.MediaBrowserService" /> * </intent-filter> * </service> **/ public abstract class MediaBrowserServiceCompat extends Service { static final String TAG = "MBServiceCompat"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 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. * * @hide */ @RestrictTo(GROUP_ID) public static final String KEY_MEDIA_ITEM = "media_item"; static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001; static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 0x00000002; /** @hide */ @RestrictTo(GROUP_ID) @Retention(RetentionPolicy.SOURCE) @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED, RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED }) private @interface ResultFlags { } 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 MediaBrowserServiceCompat#onLoadChildren
* @see MediaBrowserServiceCompat#onLoadItem
*/
public static 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.
*
* @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, or null if the
* id is invalid.
*/
public abstract void onLoadChildren(@NonNull String parentId,
@NonNull Result
* 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.
*
* @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, or null if the
* id is invalid.
* @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
* 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}.
*
* 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, Result
* 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.
*/
public @Nullable 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.
*
* 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}
* or {@link #onLoadItem}
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
*/
public final Bundle getBrowserRootHints() {
return mImpl.getBrowserRootHints();
}
/**
* 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);
}
/**
* 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
* Callers must make sure that this connection is still connected.
*/
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
* @see #EXTRA_SUGGESTION_KEYWORDS
*/
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
* @see #EXTRA_SUGGESTION_KEYWORDS
*/
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.support.v4.media.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.
*
* The root hint may contain multiple keys.
*
* @see #EXTRA_RECENT
* @see #EXTRA_OFFLINE
* @see #EXTRA_SUGGESTION_KEYWORDS
*/
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.
*
* 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.
*
* The root hint may contain multiple keys.
*
* @see #EXTRA_RECENT
* @see #EXTRA_OFFLINE
* @see #EXTRA_SUGGESTED
*/
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;
}
}
}
> 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