blob: 1da552d37fb4aa2f61be75d2795ab3dc971c18f1 [file] [log] [blame]
/*
* Copyright 2018 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 androidx.media;
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
import static androidx.media.MediaConstants2.ARGUMENT_PID;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
import static androidx.media.MediaConstants2.ARGUMENT_RATING;
import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
import static androidx.media.MediaConstants2.ARGUMENT_UID;
import static androidx.media.MediaConstants2.ARGUMENT_URI;
import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
import static androidx.media.MediaPlayerBase.UNKNOWN_TIME;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.media.MediaPlaylistAgent.RepeatMode;
import androidx.media.MediaPlaylistAgent.ShuffleMode;
import androidx.media.MediaSession2.CommandButton;
import androidx.media.MediaSession2.ControllerInfo;
import androidx.media.MediaSession2.ErrorCode;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
* other commands can be sent to the session.
* <p>
* When you're done, use {@link #close()} to clean up resources. This also helps session service
* to be destroyed when there's no controller associated with it.
* <p>
* When controlling {@link MediaSession2}, the controller will be available immediately after
* the creation.
* <p>
* MediaController2 objects are thread-safe.
* <p>
* @see MediaSession2
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public class MediaController2 implements AutoCloseable {
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef({AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, AudioManager.ADJUST_SAME,
AudioManager.ADJUST_MUTE, AudioManager.ADJUST_UNMUTE, AudioManager.ADJUST_TOGGLE_MUTE})
@Retention(RetentionPolicy.SOURCE)
public @interface VolumeDirection {}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef(value = {AudioManager.FLAG_SHOW_UI, AudioManager.FLAG_ALLOW_RINGER_MODES,
AudioManager.FLAG_PLAY_SOUND, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
AudioManager.FLAG_VIBRATE}, flag = true)
@Retention(RetentionPolicy.SOURCE)
public @interface VolumeFlags {}
/**
* Interface for listening to change in activeness of the {@link MediaSession2}. It's
* active if and only if it has set a player.
*/
public abstract static class ControllerCallback {
/**
* Called when the controller is successfully connected to the session. The controller
* becomes available afterwards.
*
* @param controller the controller for this event
* @param allowedCommands commands that's allowed by the session.
*/
public void onConnected(@NonNull MediaController2 controller,
@NonNull SessionCommandGroup2 allowedCommands) { }
/**
* Called when the session refuses the controller or the controller is disconnected from
* the session. The controller becomes unavailable afterwards and the callback wouldn't
* be called.
* <p>
* It will be also called after the {@link #close()}, so you can put clean up code here.
* You don't need to call {@link #close()} after this.
*
* @param controller the controller for this event
*/
public void onDisconnected(@NonNull MediaController2 controller) { }
/**
* Called when the session set the custom layout through the
* {@link MediaSession2#setCustomLayout(ControllerInfo, List)}.
* <p>
* Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)}
* is called.
*
* @param controller the controller for this event
* @param layout
*/
public void onCustomLayoutChanged(@NonNull MediaController2 controller,
@NonNull List<CommandButton> layout) { }
/**
* Called when the session has changed anything related with the {@link PlaybackInfo}.
*
* @param controller the controller for this event
* @param info new playback info
*/
public void onPlaybackInfoChanged(@NonNull MediaController2 controller,
@NonNull PlaybackInfo info) { }
/**
* Called when the allowed commands are changed by session.
*
* @param controller the controller for this event
* @param commands newly allowed commands
*/
public void onAllowedCommandsChanged(@NonNull MediaController2 controller,
@NonNull SessionCommandGroup2 commands) { }
/**
* Called when the session sent a custom command.
*
* @param controller the controller for this event
* @param command
* @param args
* @param receiver
*/
public void onCustomCommand(@NonNull MediaController2 controller,
@NonNull SessionCommand2 command, @Nullable Bundle args,
@Nullable ResultReceiver receiver) { }
/**
* Called when the player state is changed.
*
* @param controller the controller for this event
* @param state
*/
public void onPlayerStateChanged(@NonNull MediaController2 controller, int state) { }
/**
* Called when playback speed is changed.
*
* @param controller the controller for this event
* @param speed speed
*/
public void onPlaybackSpeedChanged(@NonNull MediaController2 controller,
float speed) { }
/**
* Called to report buffering events for a data source.
* <p>
* Use {@link #getBufferedPosition()} for current buffering position.
*
* @param controller the controller for this event
* @param item the media item for which buffering is happening.
* @param state the new buffering state.
*/
public void onBufferingStateChanged(@NonNull MediaController2 controller,
@NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
/**
* Called to indicate that seeking is completed.
*
* @param controller the controller for this event.
* @param position the previous seeking request.
*/
public void onSeekCompleted(@NonNull MediaController2 controller, long position) { }
/**
* Called when a error from
*
* @param controller the controller for this event
* @param errorCode error code
* @param extras extra information
*/
public void onError(@NonNull MediaController2 controller, @ErrorCode int errorCode,
@Nullable Bundle extras) { }
/**
* Called when the player's currently playing item is changed
* <p>
* When it's called, you should invalidate previous playback information and wait for later
* callbacks.
*
* @param controller the controller for this event
* @param item new item
* @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
*/
public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
@NonNull MediaItem2 item) { }
/**
* Called when a playlist is changed.
*
* @param controller the controller for this event
* @param list new playlist
* @param metadata new metadata
*/
public void onPlaylistChanged(@NonNull MediaController2 controller,
@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
/**
* Called when a playlist metadata is changed.
*
* @param controller the controller for this event
* @param metadata new metadata
*/
public void onPlaylistMetadataChanged(@NonNull MediaController2 controller,
@Nullable MediaMetadata2 metadata) { }
/**
* Called when the shuffle mode is changed.
*
* @param controller the controller for this event
* @param shuffleMode repeat mode
* @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
* @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
* @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
*/
public void onShuffleModeChanged(@NonNull MediaController2 controller,
@MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
/**
* Called when the repeat mode is changed.
*
* @param controller the controller for this event
* @param repeatMode repeat mode
* @see MediaPlaylistAgent#REPEAT_MODE_NONE
* @see MediaPlaylistAgent#REPEAT_MODE_ONE
* @see MediaPlaylistAgent#REPEAT_MODE_ALL
* @see MediaPlaylistAgent#REPEAT_MODE_GROUP
*/
public void onRepeatModeChanged(@NonNull MediaController2 controller,
@MediaPlaylistAgent.RepeatMode int repeatMode) { }
/**
* Called when a property of the indicated media route has changed.
*
* @param controller the controller for this event
* @param routes The list of Bundle from MediaRouteDescriptor.asBundle().
* See MediaRouteDescriptor.fromBundle(Bundle bundle) to get
* MediaRouteDescriptor object from the {@code routes}
*/
public void onRoutesInfoChanged(@NonNull MediaController2 controller,
@Nullable List<Bundle> routes) { }
}
/**
* Holds information about the the way volume is handled for this session.
*/
// The same as MediaController.PlaybackInfo
public static final class PlaybackInfo {
private static final String KEY_PLAYBACK_TYPE = "android.media.audio_info.playback_type";
private static final String KEY_CONTROL_TYPE = "android.media.audio_info.control_type";
private static final String KEY_MAX_VOLUME = "android.media.audio_info.max_volume";
private static final String KEY_CURRENT_VOLUME = "android.media.audio_info.current_volume";
private static final String KEY_AUDIO_ATTRIBUTES = "android.media.audio_info.audio_attrs";
private final int mPlaybackType;
private final int mControlType;
private final int mMaxVolume;
private final int mCurrentVolume;
private final AudioAttributesCompat mAudioAttrsCompat;
/**
* The session uses remote playback.
*/
public static final int PLAYBACK_TYPE_REMOTE = 2;
/**
* The session uses local playback.
*/
public static final int PLAYBACK_TYPE_LOCAL = 1;
PlaybackInfo(int playbackType, AudioAttributesCompat attrs, int controlType, int max,
int current) {
mPlaybackType = playbackType;
mAudioAttrsCompat = attrs;
mControlType = controlType;
mMaxVolume = max;
mCurrentVolume = current;
}
/**
* 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 audio attributes for this session. The attributes will affect
* volume handling for the session. When the volume type is
* {@link #PLAYBACK_TYPE_REMOTE} these may be ignored by the
* remote volume handler.
*
* @return The attributes for this session.
*/
public AudioAttributesCompat getAudioAttributes() {
return mAudioAttrsCompat;
}
/**
* Get the type of volume control that can be used. One of:
* <ul>
* <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
* <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
* <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
* </ul>
*
* @return The type of volume control that may be used with this session.
*/
public int getControlType() {
return mControlType;
}
/**
* 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;
}
Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType);
bundle.putInt(KEY_CONTROL_TYPE, mControlType);
bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
if (mAudioAttrsCompat != null) {
bundle.putParcelable(KEY_AUDIO_ATTRIBUTES,
MediaUtils2.toAudioAttributesBundle(mAudioAttrsCompat));
}
return bundle;
}
static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributesCompat attrs,
int controlType, int max, int current) {
return new PlaybackInfo(playbackType, attrs, controlType, max, current);
}
static PlaybackInfo fromBundle(Bundle bundle) {
if (bundle == null) {
return null;
}
final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE);
final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
final AudioAttributesCompat attrs = MediaUtils2.fromAudioAttributesBundle(
bundle.getBundle(KEY_AUDIO_ATTRIBUTES));
return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume,
currentVolume);
}
}
private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
@Override
public void onSessionReady() {
sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (!mHandlerThread.isAlive()) {
return;
}
switch (resultCode) {
case CONNECT_RESULT_CONNECTED:
onConnectedNotLocked(resultData);
break;
case CONNECT_RESULT_DISCONNECTED:
mCallback.onDisconnected(MediaController2.this);
close();
break;
}
}
});
}
@Override
public void onSessionDestroyed() {
close();
}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
synchronized (mLock) {
mPlaybackStateCompat = state;
}
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
synchronized (mLock) {
mMediaMetadataCompat = metadata;
}
}
@Override
public void onSessionEvent(String event, Bundle extras) {
switch (event) {
case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
synchronized (mLock) {
mAllowedCommands = allowedCommands;
}
mCallback.onAllowedCommandsChanged(MediaController2.this, allowedCommands);
break;
}
case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
synchronized (mLock) {
mPlayerState = playerState;
}
mCallback.onPlayerStateChanged(MediaController2.this, playerState);
break;
}
case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
if (item == null) {
return;
}
synchronized (mLock) {
mCurrentMediaItem = item;
}
mCallback.onCurrentMediaItemChanged(MediaController2.this, item);
break;
}
case SESSION_EVENT_ON_ERROR: {
int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
mCallback.onError(MediaController2.this, errorCode, errorExtras);
break;
}
case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
List<Bundle> routes = MediaUtils2.toBundleList(
extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
mCallback.onRoutesInfoChanged(MediaController2.this, routes);
break;
}
case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
extras.getParcelableArray(ARGUMENT_PLAYLIST));
synchronized (mLock) {
mPlaylist = playlist;
mPlaylistMetadata = playlistMetadata;
}
mCallback.onPlaylistChanged(MediaController2.this, playlist, playlistMetadata);
break;
}
case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
synchronized (mLock) {
mPlaylistMetadata = playlistMetadata;
}
mCallback.onPlaylistMetadataChanged(MediaController2.this, playlistMetadata);
break;
}
case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
synchronized (mLock) {
mRepeatMode = repeatMode;
}
mCallback.onRepeatModeChanged(MediaController2.this, repeatMode);
break;
}
case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
synchronized (mLock) {
mShuffleMode = shuffleMode;
}
mCallback.onShuffleModeChanged(MediaController2.this, shuffleMode);
break;
}
case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
if (commandBundle == null) {
return;
}
SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
mCallback.onCustomCommand(MediaController2.this, command, args, receiver);
break;
}
case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
if (layout == null) {
return;
}
mCallback.onCustomLayoutChanged(MediaController2.this, layout);
break;
}
case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
PlaybackInfo info = PlaybackInfo.fromBundle(
extras.getBundle(ARGUMENT_PLAYBACK_INFO));
if (info == null) {
return;
}
synchronized (mLock) {
mPlaybackInfo = info;
}
mCallback.onPlaybackInfoChanged(MediaController2.this, info);
break;
}
case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
PlaybackStateCompat state =
extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
if (state == null) {
return;
}
synchronized (mLock) {
mPlaybackStateCompat = state;
}
mCallback.onPlaybackSpeedChanged(
MediaController2.this, state.getPlaybackSpeed());
break;
}
case SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED: {
MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
if (item == null) {
return;
}
synchronized (mLock) {
mBufferingState = bufferingState;
}
mCallback.onBufferingStateChanged(MediaController2.this, item, bufferingState);
break;
}
}
}
}
private static final String TAG = "MediaController2";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
// the rootHints so it becomes non-null.
static final Bundle sDefaultRootExtras = new Bundle();
static {
sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
}
private final Context mContext;
private final Object mLock = new Object();
private final SessionToken2 mToken;
private final ControllerCallback mCallback;
private final Executor mCallbackExecutor;
private final IBinder.DeathRecipient mDeathRecipient;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
@GuardedBy("mLock")
private MediaBrowserCompat mBrowserCompat;
@GuardedBy("mLock")
private boolean mIsReleased;
@GuardedBy("mLock")
private List<MediaItem2> mPlaylist;
@GuardedBy("mLock")
private MediaMetadata2 mPlaylistMetadata;
@GuardedBy("mLock")
private @RepeatMode int mRepeatMode;
@GuardedBy("mLock")
private @ShuffleMode int mShuffleMode;
@GuardedBy("mLock")
private int mPlayerState;
@GuardedBy("mLock")
private MediaItem2 mCurrentMediaItem;
@GuardedBy("mLock")
private int mBufferingState;
@GuardedBy("mLock")
private PlaybackInfo mPlaybackInfo;
@GuardedBy("mLock")
private SessionCommandGroup2 mAllowedCommands;
// Media 1.0 variables
@GuardedBy("mLock")
private MediaControllerCompat mControllerCompat;
@GuardedBy("mLock")
private ControllerCompatCallback mControllerCompatCallback;
@GuardedBy("mLock")
private PlaybackStateCompat mPlaybackStateCompat;
@GuardedBy("mLock")
private MediaMetadataCompat mMediaMetadataCompat;
// Assignment should be used with the lock hold, but should be used without a lock to prevent
// potential deadlock.
@GuardedBy("mLock")
private volatile boolean mConnected;
/**
* Create a {@link MediaController2} from the {@link SessionToken2}.
* This connects to the session and may wake up the service if it's not available.
*
* @param context Context
* @param token token to connect to
* @param executor executor to run callbacks on.
* @param callback controller callback to receive changes in
*/
public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
@NonNull Executor executor, @NonNull ControllerCallback callback) {
super();
if (context == null) {
throw new IllegalArgumentException("context shouldn't be null");
}
if (token == null) {
throw new IllegalArgumentException("token shouldn't be null");
}
if (callback == null) {
throw new IllegalArgumentException("callback shouldn't be null");
}
if (executor == null) {
throw new IllegalArgumentException("executor shouldn't be null");
}
mContext = context;
mHandlerThread = new HandlerThread("MediaController2_Thread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mToken = token;
mCallback = callback;
mCallbackExecutor = executor;
mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
MediaController2.this.close();
}
};
initialize();
}
/**
* Release this object, and disconnect from the session. After this, callbacks wouldn't be
* received.
*/
@Override
public void close() {
if (DEBUG) {
//Log.d(TAG, "release from " + mToken, new IllegalStateException());
}
synchronized (mLock) {
if (mIsReleased) {
// Prevent re-enterance from the ControllerCallback.onDisconnected()
return;
}
mHandler.removeCallbacksAndMessages(null);
mHandlerThread.quitSafely();
mIsReleased = true;
// Send command before the unregister callback to use mIControllerCallback in the
// callback.
sendCommand(CONTROLLER_COMMAND_DISCONNECT);
if (mControllerCompat != null) {
mControllerCompat.unregisterCallback(mControllerCompatCallback);
}
if (mBrowserCompat != null) {
mBrowserCompat.disconnect();
mBrowserCompat = null;
}
if (mControllerCompat != null) {
mControllerCompat.unregisterCallback(mControllerCompatCallback);
mControllerCompat = null;
}
mConnected = false;
}
mCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
mCallback.onDisconnected(MediaController2.this);
}
});
}
/**
* @return token
*/
public @NonNull SessionToken2 getSessionToken() {
return mToken;
}
/**
* Returns whether this class is connected to active {@link MediaSession2} or not.
*/
public boolean isConnected() {
synchronized (mLock) {
return mConnected;
}
}
/**
* Requests that the player starts or resumes playback.
*/
public void play() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
}
}
/**
* Requests that the player pauses playback.
*/
public void pause() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
}
}
/**
* Requests that the player be reset to its uninitialized state.
*/
public void reset() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
sendCommand(COMMAND_CODE_PLAYBACK_RESET);
}
}
/**
* 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 MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
* to start playback.
*/
public void prepare() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
}
}
/**
* Start fast forwarding. If playback is already fast forwarding this
* may increase the rate.
*/
public void fastForward() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
}
}
/**
* Start rewinding. If playback is already rewinding this may increase
* the rate.
*/
public void rewind() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
sendCommand(COMMAND_CODE_SESSION_REWIND);
}
}
/**
* Move to a new location in the media stream.
*
* @param pos Position to move to, in milliseconds.
*/
public void seekTo(long pos) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putLong(ARGUMENT_SEEK_POSITION, pos);
sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
}
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void skipForward() {
// To match with KEYCODE_MEDIA_SKIP_FORWARD
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void skipBackward() {
// To match with KEYCODE_MEDIA_SKIP_BACKWARD
}
/**
* 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(@NonNull String mediaId, @Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putString(ARGUMENT_MEDIA_ID, mediaId);
args.putBundle(ARGUMENT_EXTRAS, extras);
sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
}
}
/**
* Request that the player start playback for a specific search query.
*
* @param query The search query. Should not be an empty string.
* @param extras Optional extras that can include extra information about the query.
*/
public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putString(ARGUMENT_QUERY, query);
args.putBundle(ARGUMENT_EXTRAS, extras);
sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
}
}
/**
* 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(@NonNull Uri uri, @Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putParcelable(ARGUMENT_URI, uri);
args.putBundle(ARGUMENT_EXTRAS, extras);
sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
}
}
/**
* 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 MediaPlayerBase#PLAYER_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(@NonNull String mediaId, @Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putString(ARGUMENT_MEDIA_ID, mediaId);
args.putBundle(ARGUMENT_EXTRAS, extras);
sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
}
}
/**
* Request that the player prepare playback for a specific search query.
* 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 MediaPlayerBase#PLAYER_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. Should not be an empty string.
* @param extras Optional extras that can include extra information about the query.
*/
public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putString(ARGUMENT_QUERY, query);
args.putBundle(ARGUMENT_EXTRAS, extras);
sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
}
}
/**
* 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 MediaPlayerBase#PLAYER_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(@NonNull Uri uri, @Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putParcelable(ARGUMENT_URI, uri);
args.putBundle(ARGUMENT_EXTRAS, extras);
sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
}
}
/**
* 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}.
* <p>
* If the session is local playback, this changes the device's volume with the stream that
* session's player is using. Flags will be specified for the {@link AudioManager}.
* <p>
* If the session is remote player (i.e. session has set volume provider), its volume provider
* will receive this request instead.
*
* @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 for local
* playback
*/
public void setVolumeTo(int value, @VolumeFlags int flags) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putInt(ARGUMENT_VOLUME, value);
args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
}
}
/**
* 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}.
* <p>
* The command will be ignored if the session does not support
* {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
* {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
* <p>
* If the session is local playback, this changes the device's volume with the stream that
* session's player is using. Flags will be specified for the {@link AudioManager}.
* <p>
* If the session is remote player (i.e. session has set volume provider), its volume provider
* will receive this request instead.
*
* @see #getPlaybackInfo()
* @param direction The direction to adjust the volume in.
* @param flags flags from {@link AudioManager} to include with the volume request for local
* playback
*/
public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
}
}
/**
* 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() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return mControllerCompat.getSessionActivity();
}
}
/**
* Get the lastly cached player state from
* {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
*
* @return player state
*/
public int getPlayerState() {
synchronized (mLock) {
return mPlayerState;
}
}
/**
* Gets the duration of the current media item, or {@link MediaPlayerBase#UNKNOWN_TIME} if
* unknown.
* @return the duration in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
*/
public long getDuration() {
synchronized (mLock) {
if (mMediaMetadataCompat != null
&& mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
}
}
return MediaPlayerBase.UNKNOWN_TIME;
}
/**
* Gets the current playback position.
* <p>
* This returns the calculated value of the position, based on the difference between the
* update time and current time.
*
* @return position
*/
public long getCurrentPosition() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return UNKNOWN_TIME;
}
if (mPlaybackStateCompat != null) {
long timeDiff = SystemClock.elapsedRealtime()
- mPlaybackStateCompat.getLastPositionUpdateTime();
long expectedPosition = mPlaybackStateCompat.getPosition()
+ (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
return Math.max(0, expectedPosition);
}
return UNKNOWN_TIME;
}
}
/**
* Get the lastly cached playback speed from
* {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
*
* @return speed the lastly cached playback speed, or 0.0f if unknown.
*/
public float getPlaybackSpeed() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return 0f;
}
return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
}
}
/**
* Set the playback speed.
*/
public void setPlaybackSpeed(float speed) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
}
}
/**
* Gets the current buffering state of the player.
* During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
* buffered.
* @return the buffering state.
*/
public @MediaPlayerBase.BuffState int getBufferingState() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return BUFFERING_STATE_UNKNOWN;
}
return mBufferingState;
}
}
/**
* Gets the lastly cached buffered position from the session when
* {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
* called.
*
* @return buffering position in millis, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
*/
public long getBufferedPosition() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return UNKNOWN_TIME;
}
return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
: mPlaybackStateCompat.getBufferedPosition();
}
}
/**
* Get the current playback info for this session.
*
* @return The current playback info or null.
*/
public @Nullable PlaybackInfo getPlaybackInfo() {
synchronized (mLock) {
return mPlaybackInfo;
}
}
/**
* Rate the media. This will cause the rating to be set for the current user.
* The rating style must follow the user rating style from the session.
* You can get the rating style from the session through the
* {@link MediaMetadata2#getRating(String)} with the key
* {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
* <p>
* If the user rating was {@code null}, the media item does not accept setting user rating.
*
* @param mediaId The id of the media
* @param rating The rating to set
*/
public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle args = new Bundle();
args.putString(ARGUMENT_MEDIA_ID, mediaId);
args.putBundle(ARGUMENT_RATING, rating.toBundle());
sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
}
}
/**
* Send custom command to the session
*
* @param command custom command
* @param args optional argument
* @param cb optional result receiver
*/
public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
@Nullable ResultReceiver cb) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return;
}
Bundle bundle = new Bundle();
bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
bundle.putBundle(ARGUMENT_ARGUMENTS, args);
sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
}
}
/**
* Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
* <p>
* This list may differ with the list that was specified with
* {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
* implementation. Use media items returned here for other playlist agent APIs such as
* {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
*
* @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
* enough permission.
* @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
*/
public @Nullable List<MediaItem2> getPlaylist() {
synchronized (mLock) {
return mPlaylist;
}
}
/**
* Sets the playlist.
* <p>
* Even when the playlist is successfully set, use the playlist returned from
* {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
* Otherwise the session in the remote process can't distinguish between media items.
*
* @param list playlist
* @param metadata metadata of the playlist
* @see #getPlaylist()
* @see ControllerCallback#onPlaylistChanged
*/
public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
if (list == null) {
throw new IllegalArgumentException("list shouldn't be null");
}
Bundle args = new Bundle();
args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
}
/**
* Updates the playlist metadata
*
* @param metadata metadata of the playlist
*/
public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
Bundle args = new Bundle();
args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
}
/**
* Gets the lastly cached playlist playlist metadata either from
* {@link ControllerCallback#onPlaylistMetadataChanged or
* {@link ControllerCallback#onPlaylistChanged}.
*
* @return metadata metadata of the playlist, or null if none is set
*/
public @Nullable MediaMetadata2 getPlaylistMetadata() {
synchronized (mLock) {
return mPlaylistMetadata;
}
}
/**
* Adds the media item to the playlist at position index. Index equals or greater than
* the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
* the playlist.
* <p>
* This will not change the currently playing media item.
* If index is less than or equal to the current index of the playlist,
* the current index of the playlist will be incremented correspondingly.
*
* @param index the index you want to add
* @param item the media item you want to add
*/
public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
Bundle args = new Bundle();
args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
}
/**
* Removes the media item at index in the playlist.
*<p>
* If the item is the currently playing item of the playlist, current playback
* will be stopped and playback moves to next source in the list.
*
* @param item the media item you want to add
*/
public void removePlaylistItem(@NonNull MediaItem2 item) {
Bundle args = new Bundle();
args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
}
/**
* Replace the media item at index in the playlist. This can be also used to update metadata of
* an item.
*
* @param index the index of the item to replace
* @param item the new item
*/
public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
Bundle args = new Bundle();
args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
}
/**
* Get the lastly cached current item from
* {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
*
* @return the currently playing item, or null if unknown.
*/
public MediaItem2 getCurrentMediaItem() {
synchronized (mLock) {
return mCurrentMediaItem;
}
}
/**
* Skips to the previous item in the playlist.
* <p>
* This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
*/
public void skipToPreviousItem() {
sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
}
/**
* Skips to the next item in the playlist.
* <p>
* This calls {@link MediaPlaylistAgent#skipToNextItem()}.
*/
public void skipToNextItem() {
sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
}
/**
* Skips to the item in the playlist.
* <p>
* This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
*
* @param item The item in the playlist you want to play
*/
public void skipToPlaylistItem(@NonNull MediaItem2 item) {
Bundle args = new Bundle();
args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
}
/**
* Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
*
* @return repeat mode
* @see MediaPlaylistAgent#REPEAT_MODE_NONE
* @see MediaPlaylistAgent#REPEAT_MODE_ONE
* @see MediaPlaylistAgent#REPEAT_MODE_ALL
* @see MediaPlaylistAgent#REPEAT_MODE_GROUP
*/
public @RepeatMode int getRepeatMode() {
synchronized (mLock) {
return mRepeatMode;
}
}
/**
* Sets the repeat mode.
*
* @param repeatMode repeat mode
* @see MediaPlaylistAgent#REPEAT_MODE_NONE
* @see MediaPlaylistAgent#REPEAT_MODE_ONE
* @see MediaPlaylistAgent#REPEAT_MODE_ALL
* @see MediaPlaylistAgent#REPEAT_MODE_GROUP
*/
public void setRepeatMode(@RepeatMode int repeatMode) {
Bundle args = new Bundle();
args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
}
/**
* Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
*
* @return The shuffle mode
* @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
* @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
* @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
*/
public @ShuffleMode int getShuffleMode() {
synchronized (mLock) {
return mShuffleMode;
}
}
/**
* Sets the shuffle mode.
*
* @param shuffleMode The shuffle mode
* @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
* @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
* @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
*/
public void setShuffleMode(@ShuffleMode int shuffleMode) {
Bundle args = new Bundle();
args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
}
/**
* Queries for information about the routes currently known.
*/
public void subscribeRoutesInfo() {
sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
}
/**
* Unsubscribes for changes to the routes.
* <p>
* The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
* the routes once this method returns.
* </p>
*/
public void unsubscribeRoutesInfo() {
sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
}
/**
* Selects the specified route.
*
* @param route The route to select.
*/
public void selectRoute(@NonNull Bundle route) {
if (route == null) {
throw new IllegalArgumentException("route shouldn't be null");
}
Bundle args = new Bundle();
args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
}
// Should be used without a lock to prevent potential deadlock.
void onConnectedNotLocked(Bundle data) {
// is enough or should we pass it while connecting?
final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
final PlaybackStateCompat playbackStateCompat = data.getParcelable(
ARGUMENT_PLAYBACK_STATE_COMPAT);
final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
data.getParcelableArray(ARGUMENT_PLAYLIST));
final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
data.getBundle(ARGUMENT_MEDIA_ITEM));
final PlaybackInfo playbackInfo =
PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
data.getBundle(ARGUMENT_PLAYLIST_METADATA));
if (DEBUG) {
Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
+ ", allowedCommands=" + allowedCommands);
}
boolean close = false;
try {
synchronized (mLock) {
if (mIsReleased) {
return;
}
if (mConnected) {
Log.e(TAG, "Cannot be notified about the connection result many times."
+ " Probably a bug or malicious app.");
close = true;
return;
}
mAllowedCommands = allowedCommands;
mPlayerState = playerState;
mBufferingState = bufferingState;
mPlaybackStateCompat = playbackStateCompat;
mRepeatMode = repeatMode;
mShuffleMode = shuffleMode;
mPlaylist = playlist;
mCurrentMediaItem = currentMediaItem;
mPlaylistMetadata = metadata;
mConnected = true;
mPlaybackInfo = playbackInfo;
}
mCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
// Note: We may trigger ControllerCallbacks with the initial values
// But it's hard to define the order of the controller callbacks
// Only notify about the
mCallback.onConnected(MediaController2.this, allowedCommands);
}
});
} finally {
if (close) {
// Trick to call release() without holding the lock, to prevent potential deadlock
// with the developer's custom lock within the ControllerCallback.onDisconnected().
close();
}
}
}
private void initialize() {
if (mToken.getType() == SessionToken2.TYPE_SESSION) {
synchronized (mLock) {
mBrowserCompat = null;
}
connectToSession(mToken.getSessionCompatToken());
} else {
connectToService();
}
}
private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
MediaControllerCompat controllerCompat = null;
try {
controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
} catch (RemoteException e) {
e.printStackTrace();
}
synchronized (mLock) {
mControllerCompat = controllerCompat;
mControllerCompatCallback = new ControllerCompatCallback();
mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
}
if (controllerCompat.isSessionReady()) {
sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (!mHandlerThread.isAlive()) {
return;
}
switch (resultCode) {
case CONNECT_RESULT_CONNECTED:
onConnectedNotLocked(resultData);
break;
case CONNECT_RESULT_DISCONNECTED:
mCallback.onDisconnected(MediaController2.this);
close();
break;
}
}
});
}
}
private void connectToService() {
synchronized (mLock) {
mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
new ConnectionCallback(), sDefaultRootExtras);
mBrowserCompat.connect();
}
}
private void sendCommand(int commandCode) {
sendCommand(commandCode, null);
}
private void sendCommand(int commandCode, Bundle args) {
if (args == null) {
args = new Bundle();
}
args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
}
private void sendCommand(String command) {
sendCommand(command, null, null);
}
private void sendCommand(String command, ResultReceiver receiver) {
sendCommand(command, null, receiver);
}
private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
if (args == null) {
args = new Bundle();
}
MediaControllerCompat controller;
ControllerCompatCallback callback;
synchronized (mLock) {
controller = mControllerCompat;
callback = mControllerCompatCallback;
}
args.putBinder(ARGUMENT_ICONTROLLER_CALLBACK, callback.getIControllerCallback().asBinder());
args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
args.putInt(ARGUMENT_UID, Process.myUid());
args.putInt(ARGUMENT_PID, Process.myPid());
controller.sendCommand(command, args, receiver);
}
@NonNull Context getContext() {
return mContext;
}
@NonNull ControllerCallback getCallback() {
return mCallback;
}
@NonNull Executor getCallbackExecutor() {
return mCallbackExecutor;
}
@Nullable MediaBrowserCompat getBrowserCompat() {
synchronized (mLock) {
return mBrowserCompat;
}
}
private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
@Override
public void onConnected() {
MediaBrowserCompat browser = getBrowserCompat();
if (browser != null) {
connectToSession(browser.getSessionToken());
} else if (DEBUG) {
Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
}
}
@Override
public void onConnectionSuspended() {
close();
}
@Override
public void onConnectionFailed() {
close();
}
}
}