/*
* 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.support.v4.media.session;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.media.AudioManager;
import android.media.session.MediaController;
import android.net.Uri;
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.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.v4.app.BundleCompat;
import android.support.v4.app.SupportActivity;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.VolumeProviderCompat;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
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.
*
* A MediaController can be created if you have a {@link MediaSessionCompat.Token}
* from the session owner.
*
* MediaController objects are thread-safe.
*
* This is a helper for accessing features in {@link android.media.session.MediaSession}
* introduced after API level 4 in a backwards compatible fashion.
*
* If MediaControllerCompat is created with a {@link MediaSessionCompat.Token session token}
* from another process, following methods will not work directly after the creation if the
* {@link MediaSessionCompat.Token session token} is not passed through a
* {@link android.support.v4.media.MediaBrowserCompat}:
*
For information about building your media application, read the
* Media Apps developer guide.
*
*/
public final class MediaControllerCompat {
static final String TAG = "MediaControllerCompat";
static final String COMMAND_GET_EXTRA_BINDER =
"android.support.v4.media.session.command.GET_EXTRA_BINDER";
static final String COMMAND_ADD_QUEUE_ITEM =
"android.support.v4.media.session.command.ADD_QUEUE_ITEM";
static final String COMMAND_ADD_QUEUE_ITEM_AT =
"android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT";
static final String COMMAND_REMOVE_QUEUE_ITEM =
"android.support.v4.media.session.command.REMOVE_QUEUE_ITEM";
static final String COMMAND_REMOVE_QUEUE_ITEM_AT =
"android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT";
static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION =
"android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION";
static final String COMMAND_ARGUMENT_INDEX =
"android.support.v4.media.session.command.ARGUMENT_INDEX";
private static class MediaControllerExtraData extends SupportActivity.ExtraData {
private final MediaControllerCompat mMediaController;
MediaControllerExtraData(MediaControllerCompat mediaController) {
mMediaController = mediaController;
}
MediaControllerCompat getMediaController() {
return mMediaController;
}
}
/**
* Sets a {@link MediaControllerCompat} in the {@code activity} for later retrieval via
* {@link #getMediaController(Activity)}.
*
*
This is compatible with {@link Activity#setMediaController(MediaController)}.
* If {@code activity} inherits {@link android.support.v4.app.FragmentActivity}, the
* {@code mediaController} will be saved in the {@code activity}. In addition to that,
* on API 21 and later, {@link Activity#setMediaController(MediaController)} will be
* called.
*
* @param activity The activity to set the {@code mediaController} in, must not be null.
* @param mediaController The controller for the session which should receive
* media keys and volume changes on API 21 and later.
* @see #getMediaController(Activity)
* @see Activity#setMediaController(android.media.session.MediaController)
*/
public static void setMediaController(@NonNull Activity activity,
MediaControllerCompat mediaController) {
if (activity instanceof SupportActivity) {
((SupportActivity) activity).putExtraData(
new MediaControllerExtraData(mediaController));
}
if (android.os.Build.VERSION.SDK_INT >= 21) {
Object controllerObj = null;
if (mediaController != null) {
Object sessionTokenObj = mediaController.getSessionToken().getToken();
controllerObj = MediaControllerCompatApi21.fromToken(activity, sessionTokenObj);
}
MediaControllerCompatApi21.setMediaController(activity, controllerObj);
}
}
/**
* Retrieves the {@link MediaControllerCompat} set in the activity by
* {@link #setMediaController(Activity, MediaControllerCompat)} for sending media key and volume
* events.
*
*
This is compatible with {@link Activity#getMediaController()}.
*
* @param activity The activity to get the media controller from, must not be null.
* @return The controller which should receive events.
* @see #setMediaController(Activity, MediaControllerCompat)
*/
public static MediaControllerCompat getMediaController(@NonNull Activity activity) {
if (activity instanceof SupportActivity) {
MediaControllerExtraData extraData =
((SupportActivity) activity).getExtraData(MediaControllerExtraData.class);
return extraData != null ? extraData.getMediaController() : null;
} else if (android.os.Build.VERSION.SDK_INT >= 21) {
Object controllerObj = MediaControllerCompatApi21.getMediaController(activity);
if (controllerObj == null) {
return null;
}
Object sessionTokenObj = MediaControllerCompatApi21.getSessionToken(controllerObj);
try {
return new MediaControllerCompat(activity,
MediaSessionCompat.Token.fromToken(sessionTokenObj));
} catch (RemoteException e) {
Log.e(TAG, "Dead object in getMediaController.", e);
}
}
return null;
}
private static void validateCustomAction(String action, Bundle args) {
if (action == null) {
return;
}
switch(action) {
case MediaSessionCompat.ACTION_FOLLOW:
case MediaSessionCompat.ACTION_UNFOLLOW:
if (args == null
|| !args.containsKey(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ATTRIBUTE)) {
throw new IllegalArgumentException("An extra field "
+ MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ATTRIBUTE + " is required "
+ "for this action " + action + ".");
}
break;
}
}
private final MediaControllerImpl mImpl;
private final MediaSessionCompat.Token mToken;
/**
* Creates a media controller from a session.
*
* @param session The session to be controlled.
*/
public MediaControllerCompat(Context context, @NonNull MediaSessionCompat session) {
if (session == null) {
throw new IllegalArgumentException("session must not be null");
}
mToken = session.getSessionToken();
if (android.os.Build.VERSION.SDK_INT >= 24) {
mImpl = new MediaControllerImplApi24(context, session);
} else if (android.os.Build.VERSION.SDK_INT >= 23) {
mImpl = new MediaControllerImplApi23(context, session);
} else if (android.os.Build.VERSION.SDK_INT >= 21) {
mImpl = new MediaControllerImplApi21(context, session);
} else {
mImpl = new MediaControllerImplBase(mToken);
}
}
/**
* Creates a media controller from a session token which may have
* been obtained from another process.
*
* @param sessionToken The token of the session to be controlled.
* @throws RemoteException if the session is not accessible.
*/
public MediaControllerCompat(Context context, @NonNull MediaSessionCompat.Token sessionToken)
throws RemoteException {
if (sessionToken == null) {
throw new IllegalArgumentException("sessionToken must not be null");
}
mToken = sessionToken;
if (android.os.Build.VERSION.SDK_INT >= 24) {
mImpl = new MediaControllerImplApi24(context, sessionToken);
} else if (android.os.Build.VERSION.SDK_INT >= 23) {
mImpl = new MediaControllerImplApi23(context, sessionToken);
} else if (android.os.Build.VERSION.SDK_INT >= 21) {
mImpl = new MediaControllerImplApi21(context, sessionToken);
} else {
mImpl = new MediaControllerImplBase(mToken);
}
}
/**
* Get a {@link TransportControls} instance for this session.
*
* @return A controls instance
*/
public TransportControls getTransportControls() {
return mImpl.getTransportControls();
}
/**
* 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(KeyEvent keyEvent) {
if (keyEvent == null) {
throw new IllegalArgumentException("KeyEvent may not be null");
}
return mImpl.dispatchMediaButtonEvent(keyEvent);
}
/**
* Get the current playback state for this session.
*
* @return The current PlaybackState or null
*/
public PlaybackStateCompat getPlaybackState() {
return mImpl.getPlaybackState();
}
/**
* Get the current metadata for this session.
*
* @return The current MediaMetadata or null.
*/
public MediaMetadataCompat getMetadata() {
return mImpl.getMetadata();
}
/**
* 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 List getQueue() {
return mImpl.getQueue();
}
/**
* Add a queue item from the given {@code description} at the end of the play queue
* of this session. Not all sessions may support this. To know whether the session supports
* this, get the session's flags with {@link #getFlags()} and check that the flag
* {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
*
* @param description The {@link MediaDescriptionCompat} for creating the
* {@link MediaSessionCompat.QueueItem} to be inserted.
* @throws UnsupportedOperationException If this session doesn't support this.
* @see #getFlags()
* @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
*/
public void addQueueItem(MediaDescriptionCompat description) {
mImpl.addQueueItem(description);
}
/**
* Add a queue item from the given {@code description} at the specified position
* in the play queue of this session. Shifts the queue item currently at that position
* (if any) and any subsequent queue items to the right (adds one to their indices).
* Not all sessions may support this. To know whether the session supports this,
* get the session's flags with {@link #getFlags()} and check that the flag
* {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
*
* @param description The {@link MediaDescriptionCompat} for creating the
* {@link MediaSessionCompat.QueueItem} to be inserted.
* @param index The index at which the created {@link MediaSessionCompat.QueueItem}
* is to be inserted.
* @throws UnsupportedOperationException If this session doesn't support this.
* @see #getFlags()
* @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
*/
public void addQueueItem(MediaDescriptionCompat description, int index) {
mImpl.addQueueItem(description, index);
}
/**
* Remove the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
* with the given {@link MediaDescriptionCompat description} in the play queue of the
* associated session. Not all sessions may support this. To know whether the session supports
* this, get the session's flags with {@link #getFlags()} and check that the flag
* {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
*
* @param description The {@link MediaDescriptionCompat} for denoting the
* {@link MediaSessionCompat.QueueItem} to be removed.
* @throws UnsupportedOperationException If this session doesn't support this.
* @see #getFlags()
* @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
*/
public void removeQueueItem(MediaDescriptionCompat description) {
mImpl.removeQueueItem(description);
}
/**
* Remove an queue item at the specified position in the play queue
* of this session. Not all sessions may support this. To know whether the session supports
* this, get the session's flags with {@link #getFlags()} and check that the flag
* {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
*
* @param index The index of the element to be removed.
* @throws UnsupportedOperationException If this session doesn't support this.
* @see #getFlags()
* @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
* @deprecated Use {@link #removeQueueItem(MediaDescriptionCompat)} instead.
*/
@Deprecated
public void removeQueueItemAt(int index) {
List queue = getQueue();
if (queue != null && index >= 0 && index < queue.size()) {
QueueItem item = queue.get(index);
if (item != null) {
removeQueueItem(item.getDescription());
}
}
}
/**
* Get the queue title for this session.
*/
public CharSequence getQueueTitle() {
return mImpl.getQueueTitle();
}
/**
* Get the extras for this session.
*/
public Bundle getExtras() {
return mImpl.getExtras();
}
/**
* Get the rating type supported by the session. One of:
*
*
{@link RatingCompat#RATING_NONE}
*
{@link RatingCompat#RATING_HEART}
*
{@link RatingCompat#RATING_THUMB_UP_DOWN}
*
{@link RatingCompat#RATING_3_STARS}
*
{@link RatingCompat#RATING_4_STARS}
*
{@link RatingCompat#RATING_5_STARS}
*
{@link RatingCompat#RATING_PERCENTAGE}
*
*
* @return The supported rating type
*/
public int getRatingType() {
return mImpl.getRatingType();
}
/**
* Return whether captioning is enabled for this session.
*
* @return {@code true} if captioning is enabled, {@code false} if disabled or not set.
*/
public boolean isCaptioningEnabled() {
return mImpl.isCaptioningEnabled();
}
/**
* Get the repeat mode for this session.
*
* @return The latest repeat mode set to the session, or
* {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set.
*/
public int getRepeatMode() {
return mImpl.getRepeatMode();
}
/**
* Return whether the shuffle mode is enabled for this session.
*
* @return {@code true} if the shuffle mode is enabled, {@code false} if disabled or not set.
* @deprecated Use {@link #getShuffleMode} instead.
*/
@Deprecated
public boolean isShuffleModeEnabled() {
return mImpl.isShuffleModeEnabled();
}
/**
* Get the shuffle mode for this session.
*
* @return The latest shuffle mode set to the session, or
* {@link PlaybackStateCompat#SHUFFLE_MODE_NONE} if not set.
*/
public int getShuffleMode() {
return mImpl.getShuffleMode();
}
/**
* Get the flags for this session. Flags are defined in
* {@link MediaSessionCompat}.
*
* @return The current set of flags for the session.
*/
public long getFlags() {
return mImpl.getFlags();
}
/**
* Get the current playback info for this session.
*
* @return The current playback info or null.
*/
public PlaybackInfo getPlaybackInfo() {
return mImpl.getPlaybackInfo();
}
/**
* Get an intent for launching UI associated with this session if one
* exists.
*
* @return A {@link PendingIntent} to launch UI or null.
*/
public PendingIntent getSessionActivity() {
return mImpl.getSessionActivity();
}
/**
* Get the token for the session this controller is connected to.
*
* @return The session's token.
*/
public MediaSessionCompat.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 VolumeProviderCompat#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) {
mImpl.setVolumeTo(value, flags);
}
/**
* 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 VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
* {@link VolumeProviderCompat#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) {
mImpl.adjustVolume(direction, flags);
}
/**
* Adds 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);
}
/**
* Adds 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, Handler handler) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
if (handler == null) {
handler = new Handler();
}
mImpl.registerCallback(callback, handler);
}
/**
* Stop receiving updates on 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");
}
mImpl.unregisterCallback(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 params Any parameters to include with the command
* @param cb The callback to receive the result on
*/
public void sendCommand(@NonNull String command, Bundle params, ResultReceiver cb) {
if (TextUtils.isEmpty(command)) {
throw new IllegalArgumentException("command must neither be null nor empty");
}
mImpl.sendCommand(command, params, cb);
}
/**
* Get the session owner's package name.
*
* @return The package name of of the session owner.
*/
public String getPackageName() {
return mImpl.getPackageName();
}
/**
* Gets the underlying framework
* {@link android.media.session.MediaController} object.
*
* This method is only supported on API 21+.
*
*
* @return The underlying {@link android.media.session.MediaController}
* object, or null if none.
*/
public Object getMediaController() {
return mImpl.getMediaController();
}
/**
* Callback for receiving updates on from the session. A Callback can be
* registered using {@link #registerCallback}
*/
public static abstract class Callback implements IBinder.DeathRecipient {
private final Object mCallbackObj;
MessageHandler mHandler;
boolean mHasExtraCallback;
boolean mRegistered = false;
public Callback() {
if (android.os.Build.VERSION.SDK_INT >= 21) {
mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21());
} else {
mCallbackObj = new StubCompat();
}
}
/**
* 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.
*/
public void onSessionEvent(String event, Bundle extras) {
}
/**
* Override to handle changes in playback state.
*
* @param state The new playback state of the session
*/
public void onPlaybackStateChanged(PlaybackStateCompat state) {
}
/**
* Override to handle changes to the current metadata.
*
* @param metadata The current metadata for the session or null if none.
* @see MediaMetadataCompat
*/
public void onMetadataChanged(MediaMetadataCompat metadata) {
}
/**
* Override to handle changes to items in the queue.
*
* @see MediaSessionCompat.QueueItem
* @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.
*/
public void onQueueChanged(List 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(CharSequence title) {
}
/**
* Override to handle chagnes to the {@link MediaSessionCompat} extras.
*
* @param extras The extras that can include other information
* associated with the {@link MediaSessionCompat}.
*/
public void onExtrasChanged(Bundle extras) {
}
/**
* Override to handle changes to the audio info.
*
* @param info The current audio info for this session.
*/
public void onAudioInfoChanged(PlaybackInfo info) {
}
/**
* Override to handle changes to the captioning enabled status.
*
* @param enabled {@code true} if captioning is enabled, {@code false} otherwise.
*/
public void onCaptioningEnabledChanged(boolean enabled) {
}
/**
* Override to handle changes to the repeat mode.
*
* @param repeatMode The repeat mode. It should be one of followings:
* {@link PlaybackStateCompat#REPEAT_MODE_NONE},
* {@link PlaybackStateCompat#REPEAT_MODE_ONE},
* {@link PlaybackStateCompat#REPEAT_MODE_ALL},
* {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
*/
public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
}
/**
* Override to handle changes to the shuffle mode.
*
* @param enabled {@code true} if the shuffle mode is enabled, {@code false} otherwise.
* @deprecated Use {@link #onShuffleModeChanged(int)} instead.
*/
@Deprecated
public void onShuffleModeChanged(boolean enabled) {
}
/**
* Override to handle changes to the shuffle mode.
*
* @param shuffleMode The shuffle mode. Must be one of the followings:
* {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
* {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
* {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
*/
public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
}
@Override
public void binderDied() {
onSessionDestroyed();
}
/**
* Set the handler to use for pre 21 callbacks.
*/
private void setHandler(Handler handler) {
mHandler = new MessageHandler(handler.getLooper());
}
private class StubApi21 implements MediaControllerCompatApi21.Callback {
StubApi21() {
}
@Override
public void onSessionDestroyed() {
Callback.this.onSessionDestroyed();
}
@Override
public void onSessionEvent(String event, Bundle extras) {
if (mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) {
// Ignore. ExtraCallback will handle this.
} else {
Callback.this.onSessionEvent(event, extras);
}
}
@Override
public void onPlaybackStateChanged(Object stateObj) {
if (mHasExtraCallback) {
// Ignore. ExtraCallback will handle this.
} else {
Callback.this.onPlaybackStateChanged(
PlaybackStateCompat.fromPlaybackState(stateObj));
}
}
@Override
public void onMetadataChanged(Object metadataObj) {
Callback.this.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj));
}
@Override
public void onQueueChanged(List> queue) {
Callback.this.onQueueChanged(QueueItem.fromQueueItemList(queue));
}
@Override
public void onQueueTitleChanged(CharSequence title) {
Callback.this.onQueueTitleChanged(title);
}
@Override
public void onExtrasChanged(Bundle extras) {
Callback.this.onExtrasChanged(extras);
}
@Override
public void onAudioInfoChanged(
int type, int stream, int control, int max, int current) {
Callback.this.onAudioInfoChanged(
new PlaybackInfo(type, stream, control, max, current));
}
}
private class StubCompat extends IMediaControllerCallback.Stub {
StubCompat() {
}
@Override
public void onEvent(String event, Bundle extras) throws RemoteException {
mHandler.post(MessageHandler.MSG_EVENT, event, extras);
}
@Override
public void onSessionDestroyed() throws RemoteException {
mHandler.post(MessageHandler.MSG_DESTROYED, null, null);
}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
}
@Override
public void onQueueChanged(List queue) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
}
@Override
public void onQueueTitleChanged(CharSequence title) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
}
@Override
public void onCaptioningEnabledChanged(boolean enabled) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_CAPTIONING_ENABLED, enabled, null);
}
@Override
public void onRepeatModeChanged(int repeatMode) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
}
@Override
public void onShuffleModeChangedDeprecated(boolean enabled) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE_DEPRECATED, enabled, null);
}
@Override
public void onShuffleModeChanged(int shuffleMode) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE, shuffleMode, null);
}
@Override
public void onExtrasChanged(Bundle extras) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
}
@Override
public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
PlaybackInfo pi = null;
if (info != null) {
pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
info.maxVolume, info.currentVolume);
}
mHandler.post(MessageHandler.MSG_UPDATE_VOLUME, pi, null);
}
}
private class MessageHandler extends Handler {
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 static final int MSG_UPDATE_REPEAT_MODE = 9;
private static final int MSG_UPDATE_SHUFFLE_MODE_DEPRECATED = 10;
private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11;
private static final int MSG_UPDATE_SHUFFLE_MODE = 12;
public MessageHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (!mRegistered) {
return;
}
switch (msg.what) {
case MSG_EVENT:
onSessionEvent((String) msg.obj, msg.getData());
break;
case MSG_UPDATE_PLAYBACK_STATE:
onPlaybackStateChanged((PlaybackStateCompat) msg.obj);
break;
case MSG_UPDATE_METADATA:
onMetadataChanged((MediaMetadataCompat) msg.obj);
break;
case MSG_UPDATE_QUEUE:
onQueueChanged((List) msg.obj);
break;
case MSG_UPDATE_QUEUE_TITLE:
onQueueTitleChanged((CharSequence) msg.obj);
break;
case MSG_UPDATE_CAPTIONING_ENABLED:
onCaptioningEnabledChanged((boolean) msg.obj);
break;
case MSG_UPDATE_REPEAT_MODE:
onRepeatModeChanged((int) msg.obj);
break;
case MSG_UPDATE_SHUFFLE_MODE_DEPRECATED:
onShuffleModeChanged((boolean) msg.obj);
break;
case MSG_UPDATE_SHUFFLE_MODE:
onShuffleModeChanged((int) msg.obj);
break;
case MSG_UPDATE_EXTRAS:
onExtrasChanged((Bundle) msg.obj);
break;
case MSG_UPDATE_VOLUME:
onAudioInfoChanged((PlaybackInfo) msg.obj);
break;
case MSG_DESTROYED:
onSessionDestroyed();
break;
}
}
public void post(int what, Object obj, Bundle data) {
Message msg = obtainMessage(what, obj);
msg.setData(data);
msg.sendToTarget();
}
}
}
/**
* Interface for controlling media playback on a session. This allows an app
* to send media transport commands to the session.
*/
public static abstract class TransportControls {
TransportControls() {
}
/**
* Request that the player prepare its playback without audio focus. In other words, other
* session 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 PlaybackStateCompat#STATE_PAUSED}. Afterwards,
* {@link #play} can be called to start playback. If the preparation is not needed,
* {@link #play} can be directly called without this method.
*/
public abstract void prepare();
/**
* Request that the player prepare playback for a specific media id. In other words, other
* session 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 PlaybackStateCompat#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 abstract void prepareFromMediaId(String mediaId, Bundle extras);
/**
* 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 session 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 PlaybackStateCompat#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 abstract void prepareFromSearch(String query, Bundle extras);
/**
* Request that the player prepare playback for a specific {@link Uri}.
* In other words, other session 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 PlaybackStateCompat#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 abstract void prepareFromUri(Uri uri, Bundle extras);
/**
* Request that the player start its playback at its current position.
*/
public abstract void play();
/**
* Request that the player start playback for a specific {@link Uri}.
*
* @param mediaId The uri of the requested media.
* @param extras Optional extras that can include extra information
* about the media item to be played.
*/
public abstract void playFromMediaId(String mediaId, Bundle extras);
/**
* 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 abstract void playFromSearch(String query, Bundle extras);
/**
* 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 abstract void playFromUri(Uri uri, Bundle extras);
/**
* 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 abstract void skipToQueueItem(long id);
/**
* Request that the player pause its playback and stay at its current
* position.
*/
public abstract void pause();
/**
* Request that the player stop its playback; it may clear its state in
* whatever way is appropriate.
*/
public abstract void stop();
/**
* Move to a new location in the media stream.
*
* @param pos Position to move to, in milliseconds.
*/
public abstract void seekTo(long pos);
/**
* Start fast forwarding. If playback is already fast forwarding this
* may increase the rate.
*/
public abstract void fastForward();
/**
* Skip to the next item.
*/
public abstract void skipToNext();
/**
* Start rewinding. If playback is already rewinding this may increase
* the rate.
*/
public abstract void rewind();
/**
* Skip to the previous item.
*/
public abstract void skipToPrevious();
/**
* 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 abstract void setRating(RatingCompat rating);
/**
* Enable/disable captioning for this session.
*
* @param enabled {@code true} to enable captioning, {@code false} to disable.
*/
public abstract void setCaptioningEnabled(boolean enabled);
/**
* Set the repeat mode for this session.
*
* @param repeatMode The repeat mode. Must be one of the followings:
* {@link PlaybackStateCompat#REPEAT_MODE_NONE},
* {@link PlaybackStateCompat#REPEAT_MODE_ONE},
* {@link PlaybackStateCompat#REPEAT_MODE_ALL},
* {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
*/
public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
/**
* Set the shuffle mode for this session.
*
* @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
* @deprecated Use {@link #setShuffleMode} instead.
*/
@Deprecated
public abstract void setShuffleModeEnabled(boolean enabled);
/**
* Set the shuffle mode for this session.
*
* @param shuffleMode The shuffle mode. Must be one of the followings:
* {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
* {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
* {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
*/
public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode);
/**
* Send a custom action for the {@link MediaSessionCompat} to perform.
*
* @param customAction The action to perform.
* @param args Optional arguments to supply to the
* {@link MediaSessionCompat} for this custom action.
*/
public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction,
Bundle args);
/**
* Send the id and args from a custom action for the
* {@link MediaSessionCompat} to perform.
*
* @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
* Bundle args)
* @see MediaSessionCompat#ACTION_FLAG_AS_INAPPROPRIATE
* @see MediaSessionCompat#ACTION_SKIP_AD
* @see MediaSessionCompat#ACTION_FOLLOW
* @see MediaSessionCompat#ACTION_UNFOLLOW
* @param action The action identifier of the
* {@link PlaybackStateCompat.CustomAction} as specified by
* the {@link MediaSessionCompat}.
* @param args Optional arguments to supply to the
* {@link MediaSessionCompat} for this custom action.
*/
public abstract void sendCustomAction(String action, Bundle args);
}
/**
* Holds information about the way volume is handled for this session.
*/
public static final class PlaybackInfo {
/**
* 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;
// TODO update audio stream with AudioAttributes support version
private final int mAudioStream;
private final int mVolumeControl;
private final int mMaxVolume;
private final int mCurrentVolume;
PlaybackInfo(int type, int stream, int control, int max, int current) {
mPlaybackType = type;
mAudioStream = stream;
mVolumeControl = control;
mMaxVolume = max;
mCurrentVolume = current;
}
/**
* Get the type of volume handling, either local or remote. One of:
*
*
{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}
*
{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}
*
*
* @return The type of volume handling this session is using.
*/
public int getPlaybackType() {
return mPlaybackType;
}
/**
* Get the stream this is currently controlling volume on. When the volume
* type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
* have meaning and should be ignored.
*
* @return The stream this session is playing on.
*/
public int getAudioStream() {
// TODO switch to AudioAttributesCompat when it is added.
return mAudioStream;
}
/**
* Get the type of volume control that can be used. One of:
*