From f364f944962c4ec66f5e5b33dafe8480f38f6db6 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Tue, 22 Jul 2014 09:39:06 -0700 Subject: [PATCH] Added Action to PlaybackState Added the ability to discover MediaBrowsers Change-Id: I925c8738ac73afd0bee3dada2ee7ff2d5047e63f --- api/current.txt | 22 ++ .../android/media/RemoteControlClient.java | 11 +- .../java/android/media/RemoteController.java | 6 +- .../media/session/ISessionCallback.aidl | 3 +- .../media/session/ISessionController.aidl | 3 +- .../media/session/MediaController.java | 41 ++- .../android/media/session/MediaSession.java | 45 ++- .../android/media/session/PlaybackState.java | 282 ++++++++++++++++-- .../server/media/MediaSessionRecord.java | 22 +- 9 files changed, 377 insertions(+), 58 deletions(-) diff --git a/api/current.txt b/api/current.txt index 6c61d5580c4e7..4ef6f89d6bce8 100644 --- a/api/current.txt +++ b/api/current.txt @@ -16595,6 +16595,8 @@ package android.media.session { method public void playUri(android.net.Uri, android.os.Bundle); method public void rewind(); method public void seekTo(long); + method public void sendCustomAction(android.media.session.PlaybackState.CustomAction, android.os.Bundle); + method public void sendCustomAction(java.lang.String, android.os.Bundle); method public void setRating(android.media.Rating); method public void skipToNext(); method public void skipToPrevious(); @@ -16670,6 +16672,7 @@ package android.media.session { public static abstract class MediaSession.TransportControlsCallback { ctor public MediaSession.TransportControlsCallback(); + method public void onCustomAction(java.lang.String, android.os.Bundle); method public void onFastForward(); method public void onPause(); method public void onPlay(); @@ -16698,6 +16701,7 @@ package android.media.session { method public int describeContents(); method public long getActions(); method public long getBufferPosition(); + method public java.util.List getCustomActions(); method public java.lang.CharSequence getErrorMessage(); method public long getLastPositionUpdateTime(); method public float getPlaybackSpeed(); @@ -16735,6 +16739,8 @@ package android.media.session { public static final class PlaybackState.Builder { ctor public PlaybackState.Builder(); ctor public PlaybackState.Builder(android.media.session.PlaybackState); + method public android.media.session.PlaybackState.Builder addCustomAction(java.lang.String, java.lang.String, int); + method public android.media.session.PlaybackState.Builder addCustomAction(android.media.session.PlaybackState.CustomAction); method public android.media.session.PlaybackState build(); method public android.media.session.PlaybackState.Builder setActions(long); method public android.media.session.PlaybackState.Builder setActiveTrack(long); @@ -16744,6 +16750,22 @@ package android.media.session { method public android.media.session.PlaybackState.Builder setState(int, long, float); } + public static final class PlaybackState.CustomAction implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getAction(); + method public android.os.Bundle getExtras(); + method public int getIcon(); + method public java.lang.CharSequence getName(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class PlaybackState.CustomAction.Builder { + ctor public PlaybackState.CustomAction.Builder(java.lang.String, java.lang.CharSequence, int); + method public android.media.session.PlaybackState.CustomAction build(); + method public android.media.session.PlaybackState.CustomAction.Builder setExtras(android.os.Bundle); + } + } package android.media.tv { diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index 023587162c6c6..db6315a5d3a06 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -18,28 +18,20 @@ package android.media; import android.app.PendingIntent; import android.content.ComponentName; -import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; import android.media.session.MediaSessionLegacyHelper; import android.media.session.PlaybackState; import android.media.session.MediaSession; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; import java.lang.IllegalArgumentException; -import java.util.ArrayList; -import java.util.Iterator; /** * RemoteControlClient enables exposing information meant to be consumed by remote controls @@ -754,7 +746,8 @@ import java.util.Iterator; // USE_SESSIONS if (mSession != null) { PlaybackState.Builder bob = new PlaybackState.Builder(mSessionPlaybackState); - bob.setActions(PlaybackState.getActionsFromRccControlFlags(transportControlFlags)); + bob.setActions( + PlaybackState.getActionsFromRccControlFlags(transportControlFlags)); mSessionPlaybackState = bob.build(); mSession.setPlaybackState(mSessionPlaybackState); } diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java index 966fbb0825b3a..3c09782b046d6 100644 --- a/media/java/android/media/RemoteController.java +++ b/media/java/android/media/RemoteController.java @@ -23,8 +23,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.media.IRemoteControlDisplay; -import android.media.MediaMetadataEditor; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionLegacyHelper; @@ -1025,8 +1023,8 @@ import java.util.List; state.getPosition(), state.getPlaybackSpeed()); } if (state != null) { - l.onClientTransportControlUpdate(PlaybackState.getRccControlFlagsFromActions(state - .getActions())); + l.onClientTransportControlUpdate( + PlaybackState.getRccControlFlagsFromActions(state.getActions())); } } } diff --git a/media/java/android/media/session/ISessionCallback.aidl b/media/java/android/media/session/ISessionCallback.aidl index 1c0dfa3c93950..99111293efa5e 100644 --- a/media/java/android/media/session/ISessionCallback.aidl +++ b/media/java/android/media/session/ISessionCallback.aidl @@ -25,7 +25,7 @@ import android.os.ResultReceiver; * @hide */ oneway interface ISessionCallback { - void onCommand(String command, in Bundle extras, in ResultReceiver cb); + void onCommand(String command, in Bundle args, in ResultReceiver cb); void onMediaButton(in Intent mediaButtonIntent, int sequenceNumber, in ResultReceiver cb); // These callbacks are for the TransportPerformer @@ -41,6 +41,7 @@ oneway interface ISessionCallback { void onRewind(); void onSeekTo(long pos); void onRate(in Rating rating); + void onCustomAction(String action, in Bundle args); // These callbacks are for volume handling void onAdjustVolume(int direction); diff --git a/media/java/android/media/session/ISessionController.aidl b/media/java/android/media/session/ISessionController.aidl index a55380207be48..d20b0ad505eec 100644 --- a/media/java/android/media/session/ISessionController.aidl +++ b/media/java/android/media/session/ISessionController.aidl @@ -38,7 +38,7 @@ import java.util.List; * @hide */ interface ISessionController { - void sendCommand(String command, in Bundle extras, in ResultReceiver cb); + void sendCommand(String command, in Bundle args, in ResultReceiver cb); boolean sendMediaButton(in KeyEvent mediaButton); void registerCallbackListener(in ISessionControllerCallback cb); void unregisterCallbackListener(in ISessionControllerCallback cb); @@ -64,6 +64,7 @@ interface ISessionController { void rewind(); void seekTo(long pos); void rate(in Rating rating); + void sendCustomAction(String action, in Bundle args); MediaMetadata getMetadata(); PlaybackState getPlaybackState(); ParceledListSlice getQueue(); diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index b699d8b2c22e0..050db211411a4 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -329,16 +329,16 @@ public final class MediaController { * commands should only be sent to sessions that the controller owns. * * @param command The command to send - * @param params Any parameters to include with the command + * @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 params, + 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(command, params, cb); + mSessionBinder.sendCommand(command, args, cb); } catch (RemoteException e) { Log.d(TAG, "Dead object in sendCommand.", e); } @@ -663,6 +663,41 @@ public final class MediaController { Log.wtf(TAG, "Error calling rate.", 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(action, args); + } catch (RemoteException e) { + Log.d(TAG, "Dead object in sendCustomAction.", e); + } + } } /** diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 72384c29ed8d5..be2d479c02316 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -503,8 +503,8 @@ public final class MediaSession { } /** - * Set some extras that can be associated with the {@link MediaSession}. No assumptions should be made - * as to how a {@link MediaController} will handle these extras. + * Set some extras that can be associated with the {@link MediaSession}. No assumptions should + * be made as to how a {@link MediaController} will handle these extras. * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. * * @param extras The extras associated with the {@link MediaSession}. @@ -583,6 +583,10 @@ public final class MediaSession { postToTransportCallbacks(TransportMessageHandler.MSG_RATE, rating); } + private void dispatchCustomAction(String action, Bundle args) { + postToTransportCallbacks(TransportMessageHandler.MSG_CUSTOM_ACTION, action, args); + } + private TransportMessageHandler getTransportControlsHandlerForCallbackLocked( TransportControlsCallback callback) { for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) { @@ -651,8 +655,8 @@ public final class MediaSession { return false; } - private void postCommand(String command, Bundle extras, ResultReceiver resultCb) { - Command cmd = new Command(command, extras, resultCb); + private void postCommand(String command, Bundle args, ResultReceiver resultCb) { + Command cmd = new Command(command, args, resultCb); synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { mCallbacks.get(i).post(CallbackMessageHandler.MSG_COMMAND, cmd); @@ -755,15 +759,15 @@ public final class MediaSession { } /** - * Called when a controller has sent a custom command to this session. + * Called when a controller has sent a command to this session. * The owner of the session may handle custom commands but is not * required to. * * @param command The command name. - * @param extras Optional parameters for the command, may be null. + * @param args Optional parameters for the command, may be null. * @param cb A result receiver to which a result may be sent by the command, may be null. */ - public void onCommand(@NonNull String command, @Nullable Bundle extras, + public void onCommand(@NonNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb) { } } @@ -849,6 +853,17 @@ public final class MediaSession { */ public void onSetRating(@NonNull Rating rating) { } + + /** + * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be + * performed. + * + * @param action The action that was originally sent in the + * {@link PlaybackState.CustomAction}. + * @param extras Optional extras specified by the {@link MediaController}. + */ + public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { + } } /** @@ -862,10 +877,10 @@ public final class MediaSession { } @Override - public void onCommand(String command, Bundle extras, ResultReceiver cb) { + public void onCommand(String command, Bundle args, ResultReceiver cb) { MediaSession session = mMediaSession.get(); if (session != null) { - session.postCommand(command, extras, cb); + session.postCommand(command, args, cb); } } @@ -980,6 +995,14 @@ public final class MediaSession { } } + @Override + public void onCustomAction(String action, Bundle args) { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.dispatchCustomAction(action, args); + } + } + @Override public void onAdjustVolume(int direction) { MediaSession session = mMediaSession.get(); @@ -1216,6 +1239,7 @@ public final class MediaSession { private static final int MSG_REWIND = 10; private static final int MSG_SEEK_TO = 11; private static final int MSG_RATE = 12; + private static final int MSG_CUSTOM_ACTION = 13; private TransportControlsCallback mCallback; @@ -1276,6 +1300,9 @@ public final class MediaSession { case MSG_RATE: mCallback.onSetRating((Rating) msg.obj); break; + case MSG_CUSTOM_ACTION: + mCallback.onCustomAction((String) msg.obj, msg.getData()); + break; } } } diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java index 3ce92cfd42601..65bd677040c50 100644 --- a/media/java/android/media/session/PlaybackState.java +++ b/media/java/android/media/session/PlaybackState.java @@ -15,12 +15,18 @@ */ package android.media.session; +import android.annotation.DrawableRes; import android.media.RemoteControlClient; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; +import android.text.TextUtils; import android.util.Log; +import java.util.ArrayList; +import java.util.List; + /** * Playback state for a {@link MediaSession}. This includes a state like * {@link PlaybackState#STATE_PLAYING}, the current playback position, @@ -32,70 +38,70 @@ public final class PlaybackState implements Parcelable { /** * Indicates this performer supports the stop command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_STOP = 1 << 0; /** * Indicates this performer supports the pause command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_PAUSE = 1 << 1; /** * Indicates this performer supports the play command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_PLAY = 1 << 2; /** * Indicates this performer supports the rewind command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_REWIND = 1 << 3; /** * Indicates this performer supports the previous command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; /** * Indicates this performer supports the next command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_SKIP_TO_NEXT = 1 << 5; /** * Indicates this performer supports the fast forward command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_FAST_FORWARD = 1 << 6; /** * Indicates this performer supports the set rating command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_SET_RATING = 1 << 7; /** * Indicates this performer supports the seek to command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_SEEK_TO = 1 << 8; /** * Indicates this performer supports the play/pause toggle command. * - * @see #setActions + * @see Builder#setActions(long) */ public static final long ACTION_PLAY_PAUSE = 1 << 9; @@ -124,42 +130,43 @@ public final class PlaybackState implements Parcelable { * This is the default playback state and indicates that no media has been * added yet, or the performer has been reset and has no content to play. * - * @see #setState + * @see Builder#setState(int, long, float) + * @see Builder#setState(int, long, float, long) */ public final static int STATE_NONE = 0; /** * State indicating this item is currently stopped. * - * @see #setState + * @see Builder#setState */ public final static int STATE_STOPPED = 1; /** * State indicating this item is currently paused. * - * @see #setState + * @see Builder#setState */ public final static int STATE_PAUSED = 2; /** * State indicating this item is currently playing. * - * @see #setState + * @see Builder#setState */ public final static int STATE_PLAYING = 3; /** * State indicating this item is currently fast forwarding. * - * @see #setState + * @see Builder#setState */ public final static int STATE_FAST_FORWARDING = 4; /** * State indicating this item is currently rewinding. * - * @see #setState + * @see Builder#setState */ public final static int STATE_REWINDING = 5; @@ -167,7 +174,7 @@ public final class PlaybackState implements Parcelable { * State indicating this item is currently buffering and will begin playing * when enough data has buffered. * - * @see #setState + * @see Builder#setState */ public final static int STATE_BUFFERING = 6; @@ -175,7 +182,7 @@ public final class PlaybackState implements Parcelable { * State indicating this item is currently in an error state. The error * message should also be set when entering this state. * - * @see #setState + * @see Builder#setState */ public final static int STATE_ERROR = 7; @@ -185,21 +192,21 @@ public final class PlaybackState implements Parcelable { * state when the connection finishes or enter {@link #STATE_NONE}. * If the connection failed {@link #STATE_ERROR} should be used. * - * @see #setState + * @see Builder#setState */ public final static int STATE_CONNECTING = 8; /** * State indicating the player is currently skipping to the previous item. * - * @see #setState + * @see Builder#setState */ public final static int STATE_SKIPPING_TO_PREVIOUS = 9; /** * State indicating the player is currently skipping to the next item. * - * @see #setState + * @see Builder#setState */ public final static int STATE_SKIPPING_TO_NEXT = 10; @@ -213,18 +220,22 @@ public final class PlaybackState implements Parcelable { private final long mBufferPosition; private final float mSpeed; private final long mActions; + private List mCustomActions; private final CharSequence mErrorMessage; private final long mUpdateTime; private final long mActiveTrackId; private PlaybackState(int state, long position, long updateTime, float speed, - long bufferPosition, long actions, long activeTrackId, CharSequence error) { + long bufferPosition, long transportControls, + List customActions, long activeTrackId, + CharSequence error) { mState = state; mPosition = position; mSpeed = speed; mUpdateTime = updateTime; mBufferPosition = bufferPosition; - mActions = actions; + mActions = transportControls; + mCustomActions = new ArrayList<>(customActions); mActiveTrackId = activeTrackId; mErrorMessage = error; } @@ -236,6 +247,7 @@ public final class PlaybackState implements Parcelable { mUpdateTime = in.readLong(); mBufferPosition = in.readLong(); mActions = in.readLong(); + mCustomActions = in.createTypedArrayList(CustomAction.CREATOR); mActiveTrackId = in.readLong(); mErrorMessage = in.readCharSequence(); @@ -250,6 +262,7 @@ public final class PlaybackState implements Parcelable { bob.append(", speed=").append(mSpeed); bob.append(", updated=").append(mUpdateTime); bob.append(", actions=").append(mActions); + bob.append(", custom actions=").append(mCustomActions); bob.append(", active track id=").append(mActiveTrackId); bob.append(", error=").append(mErrorMessage); bob.append("}"); @@ -269,6 +282,7 @@ public final class PlaybackState implements Parcelable { dest.writeLong(mUpdateTime); dest.writeLong(mBufferPosition); dest.writeLong(mActions); + dest.writeTypedList(mCustomActions); dest.writeLong(mActiveTrackId); dest.writeCharSequence(mErrorMessage); } @@ -334,6 +348,13 @@ public final class PlaybackState implements Parcelable { return mActions; } + /** + * Get the list of custom actions. + */ + public List getCustomActions() { + return mCustomActions; + } + /** * Get a user readable error message. This should be set when the state is * {@link PlaybackState#STATE_ERROR}. @@ -520,10 +541,174 @@ public final class PlaybackState implements Parcelable { } }; + /** + * {@link PlaybackState.CustomAction CustomActions} can be used to extend the capabilities of + * the standard transport controls by exposing app specific actions to + * {@link MediaController MediaControllers}. + */ + public static final class CustomAction implements Parcelable { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + private final Bundle mExtras; + + /** + * Use {@link PlaybackState.CustomAction.Builder#build()}. + */ + private CustomAction(String action, CharSequence name, int icon, Bundle extras) { + mAction = action; + mName = name; + mIcon = icon; + mExtras = extras; + } + + private CustomAction(Parcel in) { + mAction = in.readString(); + mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readInt(); + mExtras = in.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mAction); + TextUtils.writeToParcel(mName, dest, flags); + dest.writeInt(mIcon); + dest.writeBundle(mExtras); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + @Override + public PlaybackState.CustomAction createFromParcel(Parcel p) { + return new PlaybackState.CustomAction(p); + } + + @Override + public PlaybackState.CustomAction[] newArray(int size) { + return new PlaybackState.CustomAction[size]; + } + }; + + /** + * Returns the action of the {@link CustomAction}. + * + * @return The action of the {@link CustomAction}. + */ + public String getAction() { + return mAction; + } + + /** + * Returns the display name of this action. e.g. "Favorite" + * + * @return The display name of this {@link CustomAction}. + */ + public CharSequence getName() { + return mName; + } + + /** + * Returns the resource id of the icon in the {@link MediaSession MediaSession's} package. + * + * @return The resource id of the icon in the {@link MediaSession MediaSession's} package. + */ + public int getIcon() { + return mIcon; + } + + /** + * Returns extras which provide additional application-specific information about the + * action, or null if none. These arguments are meant to be consumed by a + * {@link MediaController} if it knows how to handle them. + * + * @return Optional arguments for the {@link CustomAction}. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public String toString() { + return "Action:" + + "mName='" + mName + + ", mIcon=" + mIcon + + ", mExtras=" + mExtras; + } + + /** + * Builder for {@link CustomAction} objects. + */ + public static final class Builder { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + private Bundle mExtras; + + /** + * Creates a {@link CustomAction} builder with the id, name, and icon set. + * + * @param action The action of the {@link CustomAction}. + * @param name The display name of the {@link CustomAction}. This name will be displayed + * along side the action if the UI supports it. + * @param icon The icon resource id of the {@link CustomAction}. This resource id + * must be in the same package as the {@link MediaSession}. It will be + * displayed with the custom action if the UI supports it. + */ + public Builder(String action, CharSequence name, @DrawableRes int icon) { + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException( + "You must specify an action to build a CustomAction."); + } + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException( + "You must specify a name to build a CustomAction."); + } + if (icon == 0) { + throw new IllegalArgumentException( + "You must specify an icon resource id to build a CustomAction."); + } + mAction = action; + mName = name; + mIcon = icon; + } + + /** + * Set optional extras for the {@link CustomAction}. These extras are meant to be + * consumed by a {@link MediaController} if it knows how to handle them. + * Keys should be fully qualified (e.g. "com.example.MY_ARG") to avoid collisions. + * + * @param extras Optional extras for the {@link CustomAction}. + * @return this. + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Build and return the {@link CustomAction} instance with the specified values. + * + * @return A new {@link CustomAction} instance. + */ + public CustomAction build() { + return new CustomAction(mAction, mName, mIcon, mExtras); + } + } + } + /** * Builder for {@link PlaybackState} objects. */ public static final class Builder { + private final List mCustomActions = new ArrayList<>(); + private int mState; private long mPosition; private long mBufferPosition; @@ -554,6 +739,9 @@ public final class PlaybackState implements Parcelable { mBufferPosition = from.mBufferPosition; mSpeed = from.mSpeed; mActions = from.mActions; + if (from.mCustomActions != null) { + mCustomActions.addAll(from.mCustomActions); + } mErrorMessage = from.mErrorMessage; mUpdateTime = from.mUpdateTime; mActiveTrackId = from.mActiveTrackId; @@ -656,13 +844,53 @@ public final class PlaybackState implements Parcelable { return this; } + /** + * Add a custom action to the playback state. Actions can be used to expose additional + * functionality to {@link MediaController MediaControllers} beyond what is offered by the + * standard transport controls. + *

+ * e.g. start a radio station based on the current item or skip ahead by 30 seconds. + * + * @param action An identifier for this action. It will be sent back to the + * {@link MediaSession} through + * {@link + * MediaSession.TransportControlsCallback#onCustomAction(String, Bundle)}. + * @param name The display name for the action. If text is shown with the action or used + * for accessibility, this is what should be used. + * @param icon The resource action of the icon that should be displayed for the action. The + * resource should be in the package of the {@link MediaSession}. + * @return this + */ + public Builder addCustomAction(String action, String name, int icon) { + return addCustomAction(new PlaybackState.CustomAction(action, name, icon, null)); + } + + /** + * Add a custom action to the playback state. Actions can be used to expose additional + * functionality to {@link MediaController MediaControllers} beyond what is offered by the + * standard transport controls. + *

+ * An example of an action would be to start a radio station based on the current item + * or to skip ahead by 30 seconds. + * + * @param customAction The custom action to add to the {@link PlaybackState}. + * @return this + */ + public Builder addCustomAction(PlaybackState.CustomAction customAction) { + if (customAction == null) { + throw new IllegalArgumentException( + "You may not add a null CustomAction to PlaybackState."); + } + mCustomActions.add(customAction); + return this; + } + /** * Set the current buffer position in ms. This is the farthest playback * point that can be reached from the current position using only * buffered content. * - * @param bufferPosition The position in ms that playback is buffered - * to. + * @param bufferPosition The position in ms that playback is buffered to. * @return this */ public Builder setBufferPosition(long bufferPosition) { @@ -695,13 +923,13 @@ public final class PlaybackState implements Parcelable { } /** - * Build and return the PlaybackState instance with these values. + * Build and return the {@link PlaybackState} instance with these values. * * @return A new state instance. */ public PlaybackState build() { return new PlaybackState(mState, mPosition, mUpdateTime, mSpeed, mBufferPosition, - mActions, mActiveTrackId, mErrorMessage); + mActions, mCustomActions, mActiveTrackId, mErrorMessage); } } } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 9364fa901916e..e9df507540a6a 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -748,14 +748,22 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { return false; } - public void sendCommand(String command, Bundle extras, ResultReceiver cb) { + public void sendCommand(String command, Bundle args, ResultReceiver cb) { try { - mCb.onCommand(command, extras, cb); + mCb.onCommand(command, args, cb); } catch (RemoteException e) { Slog.e(TAG, "Remote failure in sendCommand.", e); } } + public void sendCustomAction(String action, Bundle args) { + try { + mCb.onCustomAction(action, args); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in sendCustomAction.", e); + } + } + public void play() { try { mCb.onPlay(); @@ -871,9 +879,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { class ControllerStub extends ISessionController.Stub { @Override - public void sendCommand(String command, Bundle extras, ResultReceiver cb) + public void sendCommand(String command, Bundle args, ResultReceiver cb) throws RemoteException { - mSessionCb.sendCommand(command, extras, cb); + mSessionCb.sendCommand(command, args, cb); } @Override @@ -1019,6 +1027,12 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { mSessionCb.rate(rating); } + @Override + public void sendCustomAction(String action, Bundle args) + throws RemoteException { + mSessionCb.sendCustomAction(action, args); + } + @Override public MediaMetadata getMetadata() {