/* * 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.compat.annotation.UnsupportedAppUsage; 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.media.session.MediaSessionManager; import android.media.session.MediaSessionManager.RemoteUserInfo; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; import com.android.media.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** * Base class for media browser services. *
* Media browser 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 */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static final String KEY_MEDIA_ITEM = "media_item"; private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0; private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1; private static final int RESULT_ERROR = -1; private static final int RESULT_OK = 0; private final ServiceBinder mBinder; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { RESULT_FLAG_OPTION_NOT_HANDLED, RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED }) private @interface ResultFlags { } private final Handler mHandler = new Handler(); private final AtomicReference
* 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}.
*
* The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}.
* 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}.
*/
// TODO: b/185136506 - Update the javadoc to reflect API changes when
// enableNullSessionInMediaBrowserService makes it to nextfood.
public void setSessionToken(final MediaSession.Token token) {
ServiceState serviceState = mServiceState.get();
if (token == null) {
if (!Flags.enableNullSessionInMediaBrowserService()) {
throw new IllegalArgumentException("Session token may not be null.");
} else if (serviceState.mSession != null) {
ServiceState newServiceState = new ServiceState();
mBinder.setServiceState(newServiceState);
mServiceState.set(newServiceState);
serviceState.release();
} else {
// Nothing to do. The session is already null.
}
} else if (serviceState.mSession != null) {
throw new IllegalStateException("The session token has already been set.");
} else {
serviceState.mSession = token;
mHandler.post(() -> serviceState.notifySessionTokenInitializedOnHandler(token));
}
}
/**
* 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 mServiceState.get().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 #onGetRoot} or
* {@link #onLoadChildren} or {@link #onLoadItem}.
* @see MediaBrowserService.BrowserRoot#EXTRA_RECENT
* @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
* @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
*/
public final Bundle getBrowserRootHints() {
ConnectionRecord currentConnection = mCurrentConnectionOnHandler;
if (currentConnection == null) {
throw new IllegalStateException("This should be called inside of onGetRoot or"
+ " onLoadChildren or onLoadItem methods");
}
return currentConnection.rootHints == null ? null : new Bundle(currentConnection.rootHints);
}
/**
* Gets the browser information who sent the current request.
*
* @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
* {@link #onLoadChildren} or {@link #onLoadItem}.
* @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
*/
public final RemoteUserInfo getCurrentBrowserInfo() {
ConnectionRecord currentConnection = mCurrentConnectionOnHandler;
if (currentConnection == null) {
throw new IllegalStateException("This should be called inside of onGetRoot or"
+ " onLoadChildren or onLoadItem methods");
}
return new RemoteUserInfo(
currentConnection.pkg, currentConnection.pid, currentConnection.uid);
}
/**
* 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) {
notifyChildrenChanged(parentId, Bundle.EMPTY);
}
/**
* 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 The bundle of service-specific arguments to send
* to the media browser. 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");
}
if (parentId == null) {
throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
}
mHandler.post(() -> mServiceState.get().notifyChildrenChangeOnHandler(parentId, options));
}
/**
* Contains information that the browser service needs to send to the client
* when first connected.
*/
public static final class BrowserRoot {
/**
* The lookup key for a boolean that indicates whether the browser service should return a
* browser root for recently played 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 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";
private final String mRootId;
private final 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;
}
}
/**
* Holds all state associated with {@link #mSession}.
*
* This class decouples the state associated with the session from the lifecycle of the
* service. This allows us to put the service in a valid state once the session is released
* (which is an irrecoverable invalid state). More details about this in b/185136506.
*/
private class ServiceState {
// Fields accessed from any caller thread.
@Nullable private MediaSession.Token mSession;
// Fields accessed from mHandler only.
@NonNull private final ArrayMap Callers must make sure that this connection is still connected.
*/
public void performLoadChildrenOnHandler(
String parentId, ConnectionRecord connection, Bundle options) {
Result> 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