/* * 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.DATA_CALLBACK_TOKEN; import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID; 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_SERVICE_VERSION; 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 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.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.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> ** *
For information about building your media application, read the * Media Apps developer guide.
** 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. *
* 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. *
* * @see MediaBrowserServiceCompat#onLoadChildren * @see MediaBrowserServiceCompat#onLoadItem * @see MediaBrowserServiceCompat#onSearch * @see MediaBrowserServiceCompat#onCustomAction */ 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. *
* * @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. ** 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 MediaBrowserCompat.SubscriptionCallback#onError}. *
* * @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* 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 MediaBrowserCompat.SubscriptionCallback#onError}. *
* * @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* 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, @NonNull Result
* 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.
*
* 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}.
*
* The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}.
*
* 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.
*
* If the requested custom action is not supported by this service, call
* {@link Result#sendError}. The default implementation will invoke {@link Result#sendError}.
*
* 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},
* {@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();
}
/**
* 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
*/
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.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
*/
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
* @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;
}
}
}
> result) {
result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED);
result.sendResult(null);
}
/**
* Called to request a custom action to this service.
*
> result
= new Result
>(parentId) {
@Override
void onResultSent(List
> result =
new Result
>(query) {
@Override
void onResultSent(List