| /* |
| * 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.media.session; |
| |
| import android.annotation.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SystemApi; |
| import android.app.PendingIntent; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.Context; |
| import android.content.pm.ParceledListSlice; |
| import android.media.AudioAttributes; |
| import android.media.AudioManager; |
| import android.media.MediaMetadata; |
| import android.media.Rating; |
| import android.media.VolumeProvider; |
| import android.media.VolumeProvider.ControlType; |
| import android.media.session.MediaSession.QueueItem; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Allows an app to interact with an ongoing media session. Media buttons and |
| * other commands can be sent to the session. A callback may be registered to |
| * receive updates from the session, such as metadata and play state changes. |
| * <p> |
| * A MediaController can be created through {@link MediaSessionManager} if you |
| * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an |
| * enabled notification listener or by getting a {@link MediaSession.Token} |
| * directly from the session owner. |
| * <p> |
| * MediaController objects are thread-safe. |
| */ |
| public final class MediaController { |
| private static final String TAG = "MediaController"; |
| |
| private static final int MSG_EVENT = 1; |
| private static final int MSG_UPDATE_PLAYBACK_STATE = 2; |
| private static final int MSG_UPDATE_METADATA = 3; |
| private static final int MSG_UPDATE_VOLUME = 4; |
| private static final int MSG_UPDATE_QUEUE = 5; |
| private static final int MSG_UPDATE_QUEUE_TITLE = 6; |
| private static final int MSG_UPDATE_EXTRAS = 7; |
| private static final int MSG_DESTROYED = 8; |
| |
| private final ISessionController mSessionBinder; |
| |
| private final MediaSession.Token mToken; |
| private final Context mContext; |
| private final CallbackStub mCbStub = new CallbackStub(this); |
| private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>(); |
| private final Object mLock = new Object(); |
| |
| private boolean mCbRegistered = false; |
| private String mPackageName; |
| private String mTag; |
| private Bundle mSessionInfo; |
| |
| private final TransportControls mTransportControls; |
| |
| /** |
| * Create a new MediaController from a session's token. |
| * |
| * @param context The caller's context. |
| * @param token The token for the session. |
| */ |
| public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) { |
| if (context == null) { |
| throw new IllegalArgumentException("context shouldn't be null"); |
| } |
| if (token == null) { |
| throw new IllegalArgumentException("token shouldn't be null"); |
| } |
| if (token.getBinder() == null) { |
| throw new IllegalArgumentException("token.getBinder() shouldn't be null"); |
| } |
| mSessionBinder = token.getBinder(); |
| mTransportControls = new TransportControls(); |
| mToken = token; |
| mContext = context; |
| } |
| |
| /** |
| * Get a {@link TransportControls} instance to send transport actions to |
| * the associated session. |
| * |
| * @return A transport controls instance. |
| */ |
| public @NonNull TransportControls getTransportControls() { |
| return mTransportControls; |
| } |
| |
| /** |
| * Send the specified media button event to the session. Only media keys can |
| * be sent by this method, other keys will be ignored. |
| * |
| * @param keyEvent The media button event to dispatch. |
| * @return true if the event was sent to the session, false otherwise. |
| */ |
| public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) { |
| if (keyEvent == null) { |
| throw new IllegalArgumentException("KeyEvent may not be null"); |
| } |
| if (!KeyEvent.isMediaSessionKey(keyEvent.getKeyCode())) { |
| return false; |
| } |
| try { |
| return mSessionBinder.sendMediaButton(mContext.getPackageName(), keyEvent); |
| } catch (RemoteException e) { |
| // System is dead. =( |
| } |
| return false; |
| } |
| |
| /** |
| * Get the current playback state for this session. |
| * |
| * @return The current PlaybackState or null |
| */ |
| public @Nullable PlaybackState getPlaybackState() { |
| try { |
| return mSessionBinder.getPlaybackState(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getPlaybackState.", e); |
| return null; |
| } |
| } |
| |
| /** |
| * Get the current metadata for this session. |
| * |
| * @return The current MediaMetadata or null. |
| */ |
| public @Nullable MediaMetadata getMetadata() { |
| try { |
| return mSessionBinder.getMetadata(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getMetadata.", e); |
| return null; |
| } |
| } |
| |
| /** |
| * Get the current play queue for this session if one is set. If you only |
| * care about the current item {@link #getMetadata()} should be used. |
| * |
| * @return The current play queue or null. |
| */ |
| public @Nullable List<MediaSession.QueueItem> getQueue() { |
| try { |
| ParceledListSlice list = mSessionBinder.getQueue(); |
| return list == null ? null : list.getList(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getQueue.", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Get the queue title for this session. |
| */ |
| public @Nullable CharSequence getQueueTitle() { |
| try { |
| return mSessionBinder.getQueueTitle(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getQueueTitle", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Get the extras for this session. |
| */ |
| public @Nullable Bundle getExtras() { |
| try { |
| return mSessionBinder.getExtras(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getExtras", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Get the rating type supported by the session. One of: |
| * <ul> |
| * <li>{@link Rating#RATING_NONE}</li> |
| * <li>{@link Rating#RATING_HEART}</li> |
| * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li> |
| * <li>{@link Rating#RATING_3_STARS}</li> |
| * <li>{@link Rating#RATING_4_STARS}</li> |
| * <li>{@link Rating#RATING_5_STARS}</li> |
| * <li>{@link Rating#RATING_PERCENTAGE}</li> |
| * </ul> |
| * |
| * @return The supported rating type |
| */ |
| public int getRatingType() { |
| try { |
| return mSessionBinder.getRatingType(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getRatingType.", e); |
| return Rating.RATING_NONE; |
| } |
| } |
| |
| /** |
| * Get the flags for this session. Flags are defined in {@link MediaSession}. |
| * |
| * @return The current set of flags for the session. |
| */ |
| public long getFlags() { |
| try { |
| return mSessionBinder.getFlags(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getFlags.", e); |
| } |
| return 0; |
| } |
| |
| /** |
| * Get the current playback info for this session. |
| * |
| * @return The current playback info or null. |
| */ |
| public @Nullable PlaybackInfo getPlaybackInfo() { |
| try { |
| return mSessionBinder.getVolumeAttributes(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getAudioInfo.", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Get an intent for launching UI associated with this session if one |
| * exists. |
| * |
| * @return A {@link PendingIntent} to launch UI or null. |
| */ |
| public @Nullable PendingIntent getSessionActivity() { |
| try { |
| return mSessionBinder.getLaunchPendingIntent(); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling getPendingIntent.", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Get the token for the session this is connected to. |
| * |
| * @return The token for the connected session. |
| */ |
| public @NonNull MediaSession.Token getSessionToken() { |
| return mToken; |
| } |
| |
| /** |
| * Set the volume of the output this session is playing on. The command will |
| * be ignored if it does not support |
| * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in |
| * {@link AudioManager} may be used to affect the handling. |
| * |
| * @see #getPlaybackInfo() |
| * @param value The value to set it to, between 0 and the reported max. |
| * @param flags Flags from {@link AudioManager} to include with the volume |
| * request. |
| */ |
| public void setVolumeTo(int value, int flags) { |
| try { |
| // Note: Need both package name and OP package name. Package name is used for |
| // RemoteUserInfo, and OP package name is used for AudioService's internal |
| // AppOpsManager usages. |
| mSessionBinder.setVolumeTo(mContext.getPackageName(), mContext.getOpPackageName(), |
| value, flags); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling setVolumeTo.", e); |
| } |
| } |
| |
| /** |
| * Adjust the volume of the output this session is playing on. The direction |
| * must be one of {@link AudioManager#ADJUST_LOWER}, |
| * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}. |
| * The command will be ignored if the session does not support |
| * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or |
| * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in |
| * {@link AudioManager} may be used to affect the handling. |
| * |
| * @see #getPlaybackInfo() |
| * @param direction The direction to adjust the volume in. |
| * @param flags Any flags to pass with the command. |
| */ |
| public void adjustVolume(int direction, int flags) { |
| try { |
| // Note: Need both package name and OP package name. Package name is used for |
| // RemoteUserInfo, and OP package name is used for AudioService's internal |
| // AppOpsManager usages. |
| mSessionBinder.adjustVolume(mContext.getPackageName(), mContext.getOpPackageName(), |
| direction, flags); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling adjustVolumeBy.", e); |
| } |
| } |
| |
| /** |
| * Registers a callback to receive updates from the Session. Updates will be |
| * posted on the caller's thread. |
| * |
| * @param callback The callback object, must not be null. |
| */ |
| public void registerCallback(@NonNull Callback callback) { |
| registerCallback(callback, null); |
| } |
| |
| /** |
| * Registers a callback to receive updates from the session. Updates will be |
| * posted on the specified handler's thread. |
| * |
| * @param callback The callback object, must not be null. |
| * @param handler The handler to post updates on. If null the callers thread |
| * will be used. |
| */ |
| public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) { |
| if (callback == null) { |
| throw new IllegalArgumentException("callback must not be null"); |
| } |
| if (handler == null) { |
| handler = new Handler(); |
| } |
| synchronized (mLock) { |
| addCallbackLocked(callback, handler); |
| } |
| } |
| |
| /** |
| * Unregisters the specified callback. If an update has already been posted |
| * you may still receive it after calling this method. |
| * |
| * @param callback The callback to remove. |
| */ |
| public void unregisterCallback(@NonNull Callback callback) { |
| if (callback == null) { |
| throw new IllegalArgumentException("callback must not be null"); |
| } |
| synchronized (mLock) { |
| removeCallbackLocked(callback); |
| } |
| } |
| |
| /** |
| * Sends a generic command to the session. It is up to the session creator |
| * to decide what commands and parameters they will support. As such, |
| * commands should only be sent to sessions that the controller owns. |
| * |
| * @param command The command to send |
| * @param args Any parameters to include with the command |
| * @param cb The callback to receive the result on |
| */ |
| public void sendCommand(@NonNull String command, @Nullable Bundle args, |
| @Nullable ResultReceiver cb) { |
| if (TextUtils.isEmpty(command)) { |
| throw new IllegalArgumentException("command cannot be null or empty"); |
| } |
| try { |
| mSessionBinder.sendCommand(mContext.getPackageName(), command, args, cb); |
| } catch (RemoteException e) { |
| Log.d(TAG, "Dead object in sendCommand.", e); |
| } |
| } |
| |
| /** |
| * Get the session owner's package name. |
| * |
| * @return The package name of the session owner. |
| */ |
| public String getPackageName() { |
| if (mPackageName == null) { |
| try { |
| mPackageName = mSessionBinder.getPackageName(); |
| } catch (RemoteException e) { |
| Log.d(TAG, "Dead object in getPackageName.", e); |
| } |
| } |
| return mPackageName; |
| } |
| |
| /** |
| * Gets the additional session information which was set when the session was created. |
| * |
| * @return The additional session information, or an empty {@link Bundle} if not set. |
| */ |
| @NonNull |
| public Bundle getSessionInfo() { |
| if (mSessionInfo != null) { |
| return new Bundle(mSessionInfo); |
| } |
| |
| // Get info from the connected session. |
| try { |
| mSessionInfo = mSessionBinder.getSessionInfo(); |
| } catch (RemoteException e) { |
| Log.d(TAG, "Dead object in getSessionInfo.", e); |
| } |
| |
| if (mSessionInfo == null) { |
| Log.d(TAG, "sessionInfo is not set."); |
| mSessionInfo = Bundle.EMPTY; |
| } else if (MediaSession.hasCustomParcelable(mSessionInfo)) { |
| Log.w(TAG, "sessionInfo contains custom parcelable. Ignoring."); |
| mSessionInfo = Bundle.EMPTY; |
| } |
| return new Bundle(mSessionInfo); |
| } |
| |
| /** |
| * Get the session's tag for debugging purposes. |
| * |
| * @return The session's tag. |
| */ |
| @NonNull |
| public String getTag() { |
| if (mTag == null) { |
| try { |
| mTag = mSessionBinder.getTag(); |
| } catch (RemoteException e) { |
| Log.d(TAG, "Dead object in getTag.", e); |
| } |
| } |
| return mTag; |
| } |
| |
| /** |
| * @hide |
| * Returns whether this and {@code other} media controller controls the same session. |
| */ |
| @UnsupportedAppUsage(publicAlternatives = "Check equality of {@link #getSessionToken() tokens} " |
| + "instead.", maxTargetSdk = Build.VERSION_CODES.R) |
| public boolean controlsSameSession(@Nullable MediaController other) { |
| if (other == null) return false; |
| return mToken.equals(other.mToken); |
| } |
| |
| private void addCallbackLocked(Callback cb, Handler handler) { |
| if (getHandlerForCallbackLocked(cb) != null) { |
| Log.w(TAG, "Callback is already added, ignoring"); |
| return; |
| } |
| MessageHandler holder = new MessageHandler(handler.getLooper(), cb); |
| mCallbacks.add(holder); |
| holder.mRegistered = true; |
| |
| if (!mCbRegistered) { |
| try { |
| mSessionBinder.registerCallback(mContext.getPackageName(), mCbStub); |
| mCbRegistered = true; |
| } catch (RemoteException e) { |
| Log.e(TAG, "Dead object in registerCallback", e); |
| } |
| } |
| } |
| |
| private boolean removeCallbackLocked(Callback cb) { |
| boolean success = false; |
| for (int i = mCallbacks.size() - 1; i >= 0; i--) { |
| MessageHandler handler = mCallbacks.get(i); |
| if (cb == handler.mCallback) { |
| mCallbacks.remove(i); |
| success = true; |
| handler.mRegistered = false; |
| } |
| } |
| if (mCbRegistered && mCallbacks.size() == 0) { |
| try { |
| mSessionBinder.unregisterCallback(mCbStub); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Dead object in removeCallbackLocked"); |
| } |
| mCbRegistered = false; |
| } |
| return success; |
| } |
| |
| /** |
| * Gets associated handler for the given callback. |
| * @hide |
| */ |
| @VisibleForTesting |
| public Handler getHandlerForCallback(Callback cb) { |
| synchronized (mLock) { |
| return getHandlerForCallbackLocked(cb); |
| } |
| } |
| |
| private MessageHandler getHandlerForCallbackLocked(Callback cb) { |
| if (cb == null) { |
| throw new IllegalArgumentException("Callback cannot be null"); |
| } |
| for (int i = mCallbacks.size() - 1; i >= 0; i--) { |
| MessageHandler handler = mCallbacks.get(i); |
| if (cb == handler.mCallback) { |
| return handler; |
| } |
| } |
| return null; |
| } |
| |
| private void postMessage(int what, Object obj, Bundle data) { |
| synchronized (mLock) { |
| for (int i = mCallbacks.size() - 1; i >= 0; i--) { |
| mCallbacks.get(i).post(what, obj, data); |
| } |
| } |
| } |
| |
| /** |
| * Callback for receiving updates from the session. A Callback can be |
| * registered using {@link #registerCallback}. |
| */ |
| public abstract static class Callback { |
| /** |
| * Override to handle the session being destroyed. The session is no |
| * longer valid after this call and calls to it will be ignored. |
| */ |
| public void onSessionDestroyed() { |
| } |
| |
| /** |
| * Override to handle custom events sent by the session owner without a |
| * specified interface. Controllers should only handle these for |
| * sessions they own. |
| * |
| * @param event The event from the session. |
| * @param extras Optional parameters for the event, may be null. |
| */ |
| public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) { |
| } |
| |
| /** |
| * Override to handle changes in playback state. |
| * |
| * @param state The new playback state of the session |
| */ |
| public void onPlaybackStateChanged(@Nullable PlaybackState state) { |
| } |
| |
| /** |
| * Override to handle changes to the current metadata. |
| * |
| * @param metadata The current metadata for the session or null if none. |
| * @see MediaMetadata |
| */ |
| public void onMetadataChanged(@Nullable MediaMetadata metadata) { |
| } |
| |
| /** |
| * Override to handle changes to items in the queue. |
| * |
| * @param queue A list of items in the current play queue. It should |
| * include the currently playing item as well as previous and |
| * upcoming items if applicable. |
| * @see MediaSession.QueueItem |
| */ |
| public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) { |
| } |
| |
| /** |
| * Override to handle changes to the queue title. |
| * |
| * @param title The title that should be displayed along with the play queue such as |
| * "Now Playing". May be null if there is no such title. |
| */ |
| public void onQueueTitleChanged(@Nullable CharSequence title) { |
| } |
| |
| /** |
| * Override to handle changes to the {@link MediaSession} extras. |
| * |
| * @param extras The extras that can include other information associated with the |
| * {@link MediaSession}. |
| */ |
| public void onExtrasChanged(@Nullable Bundle extras) { |
| } |
| |
| /** |
| * Override to handle changes to the audio info. |
| * |
| * @param info The current audio info for this session. |
| */ |
| public void onAudioInfoChanged(PlaybackInfo info) { |
| } |
| } |
| |
| /** |
| * Interface for controlling media playback on a session. This allows an app |
| * to send media transport commands to the session. |
| */ |
| public final class TransportControls { |
| private static final String TAG = "TransportController"; |
| |
| private TransportControls() { |
| } |
| |
| /** |
| * Request that the player prepare its playback. In other words, other sessions can continue |
| * to play during the preparation of this session. This method can be used to speed up the |
| * start of the playback. Once the preparation is done, the session will change its playback |
| * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to |
| * start playback. |
| */ |
| public void prepare() { |
| try { |
| mSessionBinder.prepare(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling prepare.", e); |
| } |
| } |
| |
| /** |
| * Request that the player prepare playback for a specific media id. In other words, other |
| * sessions can continue to play during the preparation of this session. This method can be |
| * used to speed up the start of the playback. Once the preparation is done, the session |
| * will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, |
| * {@link #play} can be called to start playback. If the preparation is not needed, |
| * {@link #playFromMediaId} can be directly called without this method. |
| * |
| * @param mediaId The id of the requested media. |
| * @param extras Optional extras that can include extra information about the media item |
| * to be prepared. |
| */ |
| public void prepareFromMediaId(String mediaId, Bundle extras) { |
| if (TextUtils.isEmpty(mediaId)) { |
| throw new IllegalArgumentException( |
| "You must specify a non-empty String for prepareFromMediaId."); |
| } |
| try { |
| mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mediaId, extras); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e); |
| } |
| } |
| |
| /** |
| * Request that the player prepare playback for a specific search query. An empty or null |
| * query should be treated as a request to prepare any music. In other words, other sessions |
| * can continue to play during the preparation of this session. This method can be used to |
| * speed up the start of the playback. Once the preparation is done, the session will |
| * change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, |
| * {@link #play} can be called to start playback. If the preparation is not needed, |
| * {@link #playFromSearch} can be directly called without this method. |
| * |
| * @param query The search query. |
| * @param extras Optional extras that can include extra information |
| * about the query. |
| */ |
| public void prepareFromSearch(String query, Bundle extras) { |
| if (query == null) { |
| // This is to remain compatible with |
| // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH |
| query = ""; |
| } |
| try { |
| mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling prepare(" + query + ").", e); |
| } |
| } |
| |
| /** |
| * Request that the player prepare playback for a specific {@link Uri}. In other words, |
| * other sessions can continue to play during the preparation of this session. This method |
| * can be used to speed up the start of the playback. Once the preparation is done, the |
| * session will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards, |
| * {@link #play} can be called to start playback. If the preparation is not needed, |
| * {@link #playFromUri} can be directly called without this method. |
| * |
| * @param uri The URI of the requested media. |
| * @param extras Optional extras that can include extra information about the media item |
| * to be prepared. |
| */ |
| public void prepareFromUri(Uri uri, Bundle extras) { |
| if (uri == null || Uri.EMPTY.equals(uri)) { |
| throw new IllegalArgumentException( |
| "You must specify a non-empty Uri for prepareFromUri."); |
| } |
| try { |
| mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling prepare(" + uri + ").", e); |
| } |
| } |
| |
| /** |
| * Request that the player start its playback at its current position. |
| */ |
| public void play() { |
| try { |
| mSessionBinder.play(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling play.", e); |
| } |
| } |
| |
| /** |
| * Request that the player start playback for a specific media id. |
| * |
| * @param mediaId The id of the requested media. |
| * @param extras Optional extras that can include extra information about the media item |
| * to be played. |
| */ |
| public void playFromMediaId(String mediaId, Bundle extras) { |
| if (TextUtils.isEmpty(mediaId)) { |
| throw new IllegalArgumentException( |
| "You must specify a non-empty String for playFromMediaId."); |
| } |
| try { |
| mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling play(" + mediaId + ").", e); |
| } |
| } |
| |
| /** |
| * Request that the player start playback for a specific search query. |
| * An empty or null query should be treated as a request to play any |
| * music. |
| * |
| * @param query The search query. |
| * @param extras Optional extras that can include extra information |
| * about the query. |
| */ |
| public void playFromSearch(String query, Bundle extras) { |
| if (query == null) { |
| // This is to remain compatible with |
| // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH |
| query = ""; |
| } |
| try { |
| mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling play(" + query + ").", e); |
| } |
| } |
| |
| /** |
| * Request that the player start playback for a specific {@link Uri}. |
| * |
| * @param uri The URI of the requested media. |
| * @param extras Optional extras that can include extra information about the media item |
| * to be played. |
| */ |
| public void playFromUri(Uri uri, Bundle extras) { |
| if (uri == null || Uri.EMPTY.equals(uri)) { |
| throw new IllegalArgumentException( |
| "You must specify a non-empty Uri for playFromUri."); |
| } |
| try { |
| mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling play(" + uri + ").", e); |
| } |
| } |
| |
| /** |
| * Play an item with a specific id in the play queue. If you specify an |
| * id that is not in the play queue, the behavior is undefined. |
| */ |
| public void skipToQueueItem(long id) { |
| try { |
| mSessionBinder.skipToQueueItem(mContext.getPackageName(), id); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e); |
| } |
| } |
| |
| /** |
| * Request that the player pause its playback and stay at its current |
| * position. |
| */ |
| public void pause() { |
| try { |
| mSessionBinder.pause(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling pause.", e); |
| } |
| } |
| |
| /** |
| * Request that the player stop its playback; it may clear its state in |
| * whatever way is appropriate. |
| */ |
| public void stop() { |
| try { |
| mSessionBinder.stop(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling stop.", e); |
| } |
| } |
| |
| /** |
| * Move to a new location in the media stream. |
| * |
| * @param pos Position to move to, in milliseconds. |
| */ |
| public void seekTo(long pos) { |
| try { |
| mSessionBinder.seekTo(mContext.getPackageName(), pos); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling seekTo.", e); |
| } |
| } |
| |
| /** |
| * Start fast forwarding. If playback is already fast forwarding this |
| * may increase the rate. |
| */ |
| public void fastForward() { |
| try { |
| mSessionBinder.fastForward(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling fastForward.", e); |
| } |
| } |
| |
| /** |
| * Skip to the next item. |
| */ |
| public void skipToNext() { |
| try { |
| mSessionBinder.next(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling next.", e); |
| } |
| } |
| |
| /** |
| * Start rewinding. If playback is already rewinding this may increase |
| * the rate. |
| */ |
| public void rewind() { |
| try { |
| mSessionBinder.rewind(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling rewind.", e); |
| } |
| } |
| |
| /** |
| * Skip to the previous item. |
| */ |
| public void skipToPrevious() { |
| try { |
| mSessionBinder.previous(mContext.getPackageName()); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling previous.", e); |
| } |
| } |
| |
| /** |
| * Rate the current content. This will cause the rating to be set for |
| * the current user. The Rating type must match the type returned by |
| * {@link #getRatingType()}. |
| * |
| * @param rating The rating to set for the current content |
| */ |
| public void setRating(Rating rating) { |
| try { |
| mSessionBinder.rate(mContext.getPackageName(), rating); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling rate.", e); |
| } |
| } |
| |
| /** |
| * Sets the playback speed. A value of {@code 1.0f} is the default playback value, |
| * and a negative value indicates reverse playback. {@code 0.0f} is not allowed. |
| * |
| * @param speed The playback speed |
| * @throws IllegalArgumentException if the {@code speed} is equal to zero. |
| */ |
| public void setPlaybackSpeed(float speed) { |
| if (speed == 0.0f) { |
| throw new IllegalArgumentException("speed must not be zero"); |
| } |
| try { |
| mSessionBinder.setPlaybackSpeed(mContext.getPackageName(), speed); |
| } catch (RemoteException e) { |
| Log.wtf(TAG, "Error calling setPlaybackSpeed.", e); |
| } |
| } |
| |
| /** |
| * Send a custom action back for the {@link MediaSession} to perform. |
| * |
| * @param customAction The action to perform. |
| * @param args Optional arguments to supply to the {@link MediaSession} for this |
| * custom action. |
| */ |
| public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction, |
| @Nullable Bundle args) { |
| if (customAction == null) { |
| throw new IllegalArgumentException("CustomAction cannot be null."); |
| } |
| sendCustomAction(customAction.getAction(), args); |
| } |
| |
| /** |
| * Send the id and args from a custom action back for the {@link MediaSession} to perform. |
| * |
| * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args) |
| * @param action The action identifier of the {@link PlaybackState.CustomAction} as |
| * specified by the {@link MediaSession}. |
| * @param args Optional arguments to supply to the {@link MediaSession} for this |
| * custom action. |
| */ |
| public void sendCustomAction(@NonNull String action, @Nullable Bundle args) { |
| if (TextUtils.isEmpty(action)) { |
| throw new IllegalArgumentException("CustomAction cannot be null."); |
| } |
| try { |
| mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args); |
| } catch (RemoteException e) { |
| Log.d(TAG, "Dead object in sendCustomAction.", e); |
| } |
| } |
| } |
| |
| /** |
| * Holds information about the current playback and how audio is handled for |
| * this session. |
| */ |
| public static final class PlaybackInfo implements Parcelable { |
| |
| /** |
| * @hide |
| */ |
| @IntDef({PLAYBACK_TYPE_LOCAL, PLAYBACK_TYPE_REMOTE}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface PlaybackType {} |
| |
| /** |
| * The session uses local playback. |
| */ |
| public static final int PLAYBACK_TYPE_LOCAL = 1; |
| /** |
| * The session uses remote playback. |
| */ |
| public static final int PLAYBACK_TYPE_REMOTE = 2; |
| |
| private final int mPlaybackType; |
| private final int mVolumeControl; |
| private final int mMaxVolume; |
| private final int mCurrentVolume; |
| private final AudioAttributes mAudioAttrs; |
| private final String mVolumeControlId; |
| |
| /** |
| * Creates a new playback info. |
| * |
| * @param playbackType The playback type. Should be {@link #PLAYBACK_TYPE_LOCAL} or {@link |
| * #PLAYBACK_TYPE_REMOTE} |
| * @param volumeControl See {@link #getVolumeControl()}. |
| * @param maxVolume The max volume. Should be equal or greater than zero. |
| * @param currentVolume The current volume. Should be in the interval [0, maxVolume]. |
| * @param audioAttrs The audio attributes for this playback. Should not be null. |
| * @param volumeControlId See {@link #getVolumeControlId()}. |
| * @hide |
| */ |
| @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) |
| public PlaybackInfo( |
| @PlaybackType int playbackType, |
| @ControlType int volumeControl, |
| @IntRange(from = 0) int maxVolume, |
| @IntRange(from = 0) int currentVolume, |
| @NonNull AudioAttributes audioAttrs, |
| @Nullable String volumeControlId) { |
| mPlaybackType = playbackType; |
| mVolumeControl = volumeControl; |
| mMaxVolume = maxVolume; |
| mCurrentVolume = currentVolume; |
| mAudioAttrs = audioAttrs; |
| mVolumeControlId = volumeControlId; |
| } |
| |
| PlaybackInfo(Parcel in) { |
| mPlaybackType = in.readInt(); |
| mVolumeControl = in.readInt(); |
| mMaxVolume = in.readInt(); |
| mCurrentVolume = in.readInt(); |
| mAudioAttrs = in.readParcelable(null, android.media.AudioAttributes.class); |
| mVolumeControlId = in.readString(); |
| } |
| |
| /** |
| * Get the type of playback which affects volume handling. One of: |
| * <ul> |
| * <li>{@link #PLAYBACK_TYPE_LOCAL}</li> |
| * <li>{@link #PLAYBACK_TYPE_REMOTE}</li> |
| * </ul> |
| * |
| * @return The type of playback this session is using. |
| */ |
| public int getPlaybackType() { |
| return mPlaybackType; |
| } |
| |
| /** |
| * Get the volume control type associated to the session, as indicated by {@link |
| * VolumeProvider#getVolumeControl()}. |
| */ |
| public int getVolumeControl() { |
| return mVolumeControl; |
| } |
| |
| /** |
| * Get the maximum volume that may be set for this session. |
| * |
| * @return The maximum allowed volume where this session is playing. |
| */ |
| public int getMaxVolume() { |
| return mMaxVolume; |
| } |
| |
| /** |
| * Get the current volume for this session. |
| * |
| * @return The current volume where this session is playing. |
| */ |
| public int getCurrentVolume() { |
| return mCurrentVolume; |
| } |
| |
| /** |
| * Get the audio attributes for this session. The attributes will affect volume handling for |
| * the session. When the playback type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these |
| * may be ignored by the remote volume handler. |
| * |
| * @return The attributes for this session. |
| */ |
| public AudioAttributes getAudioAttributes() { |
| return mAudioAttrs; |
| } |
| |
| /** |
| * Get the routing controller ID for this session, as indicated by {@link |
| * VolumeProvider#getVolumeControlId()}. Returns null if unset, or if {@link |
| * #getPlaybackType()} is {@link #PLAYBACK_TYPE_LOCAL}. |
| */ |
| @Nullable |
| public String getVolumeControlId() { |
| return mVolumeControlId; |
| } |
| |
| @Override |
| public String toString() { |
| return "playbackType=" + mPlaybackType + ", volumeControlType=" + mVolumeControl |
| + ", maxVolume=" + mMaxVolume + ", currentVolume=" + mCurrentVolume |
| + ", audioAttrs=" + mAudioAttrs + ", volumeControlId=" + mVolumeControlId; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeInt(mPlaybackType); |
| dest.writeInt(mVolumeControl); |
| dest.writeInt(mMaxVolume); |
| dest.writeInt(mCurrentVolume); |
| dest.writeParcelable(mAudioAttrs, flags); |
| dest.writeString(mVolumeControlId); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<PlaybackInfo> CREATOR = |
| new Parcelable.Creator<PlaybackInfo>() { |
| @Override |
| public PlaybackInfo createFromParcel(Parcel in) { |
| return new PlaybackInfo(in); |
| } |
| |
| @Override |
| public PlaybackInfo[] newArray(int size) { |
| return new PlaybackInfo[size]; |
| } |
| }; |
| } |
| |
| private static final class CallbackStub extends ISessionControllerCallback.Stub { |
| private final WeakReference<MediaController> mController; |
| |
| CallbackStub(MediaController controller) { |
| mController = new WeakReference<MediaController>(controller); |
| } |
| |
| @Override |
| public void onSessionDestroyed() { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_DESTROYED, null, null); |
| } |
| } |
| |
| @Override |
| public void onEvent(String event, Bundle extras) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_EVENT, event, extras); |
| } |
| } |
| |
| @Override |
| public void onPlaybackStateChanged(PlaybackState state) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null); |
| } |
| } |
| |
| @Override |
| public void onMetadataChanged(MediaMetadata metadata) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_UPDATE_METADATA, metadata, null); |
| } |
| } |
| |
| @Override |
| public void onQueueChanged(ParceledListSlice queue) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_UPDATE_QUEUE, queue, null); |
| } |
| } |
| |
| @Override |
| public void onQueueTitleChanged(CharSequence title) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null); |
| } |
| } |
| |
| @Override |
| public void onExtrasChanged(Bundle extras) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_UPDATE_EXTRAS, extras, null); |
| } |
| } |
| |
| @Override |
| public void onVolumeInfoChanged(PlaybackInfo info) { |
| MediaController controller = mController.get(); |
| if (controller != null) { |
| controller.postMessage(MSG_UPDATE_VOLUME, info, null); |
| } |
| } |
| } |
| |
| private static final class MessageHandler extends Handler { |
| private final MediaController.Callback mCallback; |
| private boolean mRegistered = false; |
| |
| MessageHandler(Looper looper, MediaController.Callback cb) { |
| super(looper); |
| mCallback = cb; |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| if (!mRegistered) { |
| return; |
| } |
| switch (msg.what) { |
| case MSG_EVENT: |
| mCallback.onSessionEvent((String) msg.obj, msg.getData()); |
| break; |
| case MSG_UPDATE_PLAYBACK_STATE: |
| mCallback.onPlaybackStateChanged((PlaybackState) msg.obj); |
| break; |
| case MSG_UPDATE_METADATA: |
| mCallback.onMetadataChanged((MediaMetadata) msg.obj); |
| break; |
| case MSG_UPDATE_QUEUE: |
| mCallback.onQueueChanged(msg.obj == null ? null : |
| (List<QueueItem>) ((ParceledListSlice) msg.obj).getList()); |
| break; |
| case MSG_UPDATE_QUEUE_TITLE: |
| mCallback.onQueueTitleChanged((CharSequence) msg.obj); |
| break; |
| case MSG_UPDATE_EXTRAS: |
| mCallback.onExtrasChanged((Bundle) msg.obj); |
| break; |
| case MSG_UPDATE_VOLUME: |
| mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj); |
| break; |
| case MSG_DESTROYED: |
| mCallback.onSessionDestroyed(); |
| break; |
| } |
| } |
| |
| public void post(int what, Object obj, Bundle data) { |
| Message msg = obtainMessage(what, obj); |
| msg.setAsynchronous(true); |
| msg.setData(data); |
| msg.sendToTarget(); |
| } |
| } |
| |
| } |