java.lang.Object
↳androidx.media2.session.MediaController
Subclasses:
MediaBrowser
Gradle dependencies
compile group: 'androidx.media2', name: 'media2-session', version: '1.3.0'
- groupId: androidx.media2
- artifactId: media2-session
- version: 1.3.0
Artifact androidx.media2:media2-session:1.3.0 it located at Google repository (https://maven.google.com/)
Overview
Allows an app to interact with an active MediaSession or a MediaSessionService
which would provide MediaSession. Media buttons and other commands can be sent to the
session.
MediaController objects are thread-safe.
Topics covered here:
- Controller Lifecycle
- Controlling the MediaSession in the same
process
- Package Visibility Filter
Controller Lifecycle
When a controller is created with the SessionToken for a MediaSession (i.e.
session token type is SessionToken.TYPE_SESSION), the controller will connect to the
specific session.
When a controller is created with the SessionToken for a MediaSessionService
(i.e. session token type is SessionToken.TYPE_SESSION_SERVICE or SessionToken.TYPE_LIBRARY_SERVICE), the controller binds to the service for connecting to a
MediaSession in it. MediaSessionService will provide a session to connect.
When a controller connects to a session, MediaSession.SessionCallback.onConnect(MediaSession, MediaSession.ControllerInfo) will be called
to either accept or reject the connection. Wait MediaController.ControllerCallback.onConnected(MediaController, SessionCommandGroup) or MediaController.ControllerCallback.onDisconnected(MediaController) for the result.
When the connected session is closed, the controller will receive MediaController.ControllerCallback.onDisconnected(MediaController).
When you're done, use MediaController.close() to clean up resources. This also helps session service
to be destroyed when there's no controller associated with it.
Controlling the MediaSession in the same process
When you control the
MediaSession and its
SessionPlayer, it's recommended to use
them directly rather than creating
MediaController. However, if you need to use
MediaController in the same process, be careful not to block session callback executor's thread.
Here's an example code that would never return due to the thread issue.
// Code runs on the main thread.
MediaSession session = new MediaSession.Builder(context, player)
.setSessionCallback(sessionCallback, Context.getMainExecutor(context)).build();
MediaController controller = new MediaController.Builder(context)
.setSessionToken(session.getToken())
.setControllerCallback(Context.getMainExecutor(context), controllerCallback)
.build();
// This will hang and never return.
controller.play().get();
When a session gets a command from a controller, the session's
MediaSession.SessionCallback.onCommandRequest(MediaSession, MediaSession.ControllerInfo, SessionCommand) would be executed on the session's callback
executor to decide whether to ignore or handle the incoming command. To do so, the session's
callback executor shouldn't be blocked to handle the incoming calls. However, if you call on the thread for the session callback executor, then your call wouldn't be
executed and never return.
To avoid such issue, don't block the session callback executor's thread. Creating a dedicated
thread for the session callback executor would be helpful. See newSingleThreadExecutor
for creating a new thread.
Package Visibility Filter
The app targeting API level 30 or higher must include a element in their
manifest to connect to a service component of another app like MediaSessionService,
MediaLibraryService, or MediaBrowserServiceCompat). See the
following example and this guide for more
information.
Summary
Methods |
---|
public <any> | addPlaylistItem(int index, java.lang.String mediaId)
Requests that the SessionPlayer associated with the connected MediaSession
adds the media item to the playlist at the index with the media
ID. |
public <any> | adjustVolume(int direction, int flags)
Requests that the connected MediaSession adjusts the volume of the output that is
playing on. |
public void | close()
Releases this object, and disconnects from the session. |
public <any> | deselectTrack(SessionPlayer.TrackInfo trackInfo)
Requests that the SessionPlayer associated with the connected MediaSession
deselects the SessionPlayer.TrackInfo for the current media item. |
public <any> | fastForward()
Requests that the SessionPlayer associated with the connected MediaSession
to fast forward playback. |
public SessionCommandGroup | getAllowedCommands()
Gets the cached allowed commands from MediaController.ControllerCallback.onAllowedCommandsChanged(MediaController, SessionCommandGroup). |
public long | getBufferedPosition()
Gets the position for how much has been buffered of the SessionPlayer associated
with the connected MediaSession, or SessionPlayer.UNKNOWN_TIME if
unknown or not connected. |
public int | getBufferingState()
Gets the current buffering state of the SessionPlayer associated with the connected
MediaSession. |
public SessionToken | getConnectedToken()
Returns the SessionToken of the connected session. |
public MediaItem | getCurrentMediaItem()
Gets the current media item of the SessionPlayer associated with the connected
MediaSession. |
public int | getCurrentMediaItemIndex()
Gets the current item index in the playlist of the SessionPlayer associated with
the connected MediaSession. |
public long | getCurrentPosition()
Gets the playback position of the SessionPlayer associated with the connected
MediaSession. |
public long | getDuration()
Gets the duration of the current media item, or SessionPlayer.UNKNOWN_TIME if
unknown or not connected. |
public java.util.List<Pair> | getExtraControllerCallbacks()
|
public int | getNextMediaItemIndex()
Gets the next item index in the playlist of the SessionPlayer associated with
the connected MediaSession. |
public MediaController.PlaybackInfo | getPlaybackInfo()
Get the current playback info for this session. |
public float | getPlaybackSpeed()
Gets the playback speed to be used by the of the SessionPlayer associated with the
connected MediaSession when playing. |
public int | getPlayerState()
Gets the state of the SessionPlayer associated with the connected
MediaSession. |
public java.util.List<MediaItem> | getPlaylist()
Gets the playlist of the SessionPlayer associated with the connected
MediaSession. |
public MediaMetadata | getPlaylistMetadata()
Gets the playlist metadata of the SessionPlayer associated with the connected
MediaSession. |
public int | getPreviousMediaItemIndex()
Gets the previous item index in the playlist of the SessionPlayer associated with
the connected MediaSession. |
public int | getRepeatMode()
Gets the repeat mode of the SessionPlayer associated with the connected
MediaSession. |
public SessionPlayer.TrackInfo | getSelectedTrack(int trackType)
Gets the currently selected track for the given track type of the SessionPlayer
associated with the connected MediaSession. |
public PendingIntent | getSessionActivity()
Gets an intent for launching UI associated with this session if one exists. |
public int | getShuffleMode()
Gets the shuffle mode of the SessionPlayer associated with the connected
MediaSession. |
public java.util.List<SessionPlayer.TrackInfo> | getTracks()
Gets the full list of selected and unselected tracks that the media contains of the
SessionPlayer associated with the connected MediaSession. |
public VideoSize | getVideoSize()
Gets the video size of the SessionPlayer associated with the connected
MediaSession. |
public boolean | isConnected()
Returns whether this class is connected to active MediaSession or not. |
public <any> | movePlaylistItem(int fromIndex, int toIndex)
Requests that the SessionPlayer associated with the connected MediaSession
moves the media item at fromIdx to toIdx in the playlist. |
public void | notifyAllControllerCallbacks(MediaController.ControllerCallbackRunnable callbackRunnable)
|
public <any> | pause()
Requests that the SessionPlayer associated with the connected MediaSession
pauses playback. |
public <any> | play()
Requests that the SessionPlayer associated with the connected MediaSession
starts or resumes playback. |
public <any> | prepare()
Requests that the SessionPlayer associated with the connected MediaSession
prepares the media items for playback. |
public void | registerExtraCallback(java.util.concurrent.Executor executor, MediaController.ControllerCallback callback)
Registers an extra MediaController.ControllerCallback. |
public <any> | removePlaylistItem(int index)
Requests that the SessionPlayer associated with the connected MediaSession
removes the media item at index in the playlist. |
public <any> | replacePlaylistItem(int index, java.lang.String mediaId)
Requests that the SessionPlayer associated with the connected MediaSession
replaces the media item at index in the playlist with the media ID. |
public <any> | rewind()
Requests that the SessionPlayer associated with the connected MediaSession
to rewind playback. |
public <any> | seekTo(long position)
Requests that the SessionPlayer associated with the connected MediaSession
seeks to the specified position. |
public <any> | selectTrack(SessionPlayer.TrackInfo trackInfo)
Requests that the SessionPlayer associated with the connected MediaSession
selects the SessionPlayer.TrackInfo for the current media item. |
public <any> | sendCustomCommand(SessionCommand command, Bundle args)
Sends a custom command to the session |
public <any> | setMediaItem(java.lang.String mediaId)
Requests that the SessionPlayer associated with the connected MediaSession
sets a MediaItem for playback. |
public <any> | setMediaUri(Uri uri, Bundle extras)
Requests that the connected MediaSession sets a specific for playback. |
public <any> | setPlaybackSpeed(float playbackSpeed)
Requests that the SessionPlayer associated with the connected MediaSession
sets the playback speed. |
public <any> | setPlaylist(java.util.List<java.lang.String> list, MediaMetadata metadata)
Requests that the SessionPlayer associated with the connected MediaSession
sets the playlist with the list of media IDs. |
public <any> | setRating(java.lang.String mediaId, Rating rating)
Requests that the connected MediaSession rates the media. |
public <any> | setRepeatMode(int repeatMode)
Requests that the SessionPlayer associated with the connected MediaSession
sets the repeat mode. |
public <any> | setShuffleMode(int shuffleMode)
Requests that the SessionPlayer associated with the connected MediaSession
sets the shuffle mode. |
public <any> | setSurface(Surface surface)
Requests that the SessionPlayer associated with the connected MediaSession
sets the to be used as the sink for the video portion of the media. |
public void | setTimeDiff(java.lang.Long timeDiff)
Sets the time diff forcefully when calculating current position. |
public <any> | setVolumeTo(int value, int flags)
Requests that the connected MediaSession sets the volume of the output that is
playing on. |
public <any> | skipBackward()
Requests that the SessionPlayer associated with the connected MediaSession
skips forward within the current media item. |
public <any> | skipForward()
Requests that the SessionPlayer associated with the connected MediaSession
skips backward within the current media item. |
public <any> | skipToNextPlaylistItem()
Requests that the SessionPlayer associated with the connected MediaSession
skips to the next item in the playlist. |
public <any> | skipToPlaylistItem(int index)
Requests that the SessionPlayer associated with the connected MediaSession
skips to the item in the playlist at the index. |
public <any> | skipToPreviousPlaylistItem()
Requests that the SessionPlayer associated with the connected MediaSession
skips to the previous item in the playlist. |
public void | unregisterExtraCallback(MediaController.ControllerCallback callback)
Unregisters an MediaController.ControllerCallback that has been registered by
MediaController.registerExtraCallback(Executor, MediaController.ControllerCallback). |
public <any> | updatePlaylistMetadata(MediaMetadata metadata)
Requests that the SessionPlayer associated with the connected MediaSession
updates the playlist metadata while keeping the playlist as-is. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Methods
Releases this object, and disconnects from the session. After this, callbacks wouldn't be
received.
Returns the SessionToken of the connected session.
If it is not connected yet, it returns null.
This may differ from the SessionToken from the constructor. For example, if the
controller is created with the token for MediaSessionService, this would return
token for the MediaSession in the service.
Returns:
SessionToken of the connected session, or null if not connected
public boolean
isConnected()
Returns whether this class is connected to active MediaSession or not.
Requests that the SessionPlayer associated with the connected MediaSession
starts or resumes playback.
On success, this transfers the player state to SessionPlayer.PLAYER_STATE_PLAYING
and a SessionResult would be returned with the current media item when the command
was completed.
If the player state is SessionPlayer.PLAYER_STATE_IDLE, the session would also call
SessionPlayer.prepare() and then SessionPlayer.play() to start playback. If you
want to have finer grained control of the playback start, call MediaController.prepare() manually
before this. Calling MediaController.prepare() in advance would help this method to start playback
faster and also help to take audio focus at the last moment.
Interoperability: When connected to
android.support.v4.media.session.MediaSessionCompat
, then this will be grouped
together with previously called MediaController.setMediaUri(Uri, Bundle). See MediaController.setMediaUri(Uri, Bundle) for
details.
Returns:
a representing the pending completion of the command
See also: MediaController.prepare(), MediaController.setMediaUri(Uri, Bundle)
Requests that the SessionPlayer associated with the connected MediaSession
pauses playback.
On success, this transfers the player state to SessionPlayer.PLAYER_STATE_PAUSED and
a SessionResult would be returned with the current media item when the command
was completed. If it is called in SessionPlayer.PLAYER_STATE_IDLE or
SessionPlayer.PLAYER_STATE_ERROR, it whould be ignored and a SessionResult
would be returned with BaseResult.RESULT_ERROR_INVALID_STATE.
Returns:
a representing the pending completion of the command
Requests that the SessionPlayer associated with the connected MediaSession
prepares the media items for playback. During this time, the player may allocate resources
required to play, such as audio and video decoders. Before calling this API, sets media
item(s) through either MediaController.setMediaItem(String) or MediaController.setPlaylist(List, MediaMetadata).
On success, this transfers the player state from SessionPlayer.PLAYER_STATE_IDLE to
SessionPlayer.PLAYER_STATE_PAUSED and a SessionResult would be returned
with the prepared media item when the command completed. If it's not called in
SessionPlayer.PLAYER_STATE_IDLE, it would be ignored and SessionResult
would be returned with BaseResult.RESULT_ERROR_INVALID_STATE.
Playback can be started without this. But this provides finer grained control of playback
start. See MediaController.play() for details.
Interoperability: When connected to
android.support.v4.media.session.MediaSessionCompat
, then this call may be grouped
together with previously called MediaController.setMediaUri(Uri, Bundle). See MediaController.setMediaUri(Uri, Bundle) for
details.
Returns:
a representing the pending completion of the command
See also: MediaController.play(), MediaController.setMediaUri(Uri, Bundle)
public <any>
fastForward()
Requests that the SessionPlayer associated with the connected MediaSession
to fast forward playback.
The implementation may be different depending on the players. For example, it can be
implemented by seeking forward once, series of seeking forward, or increasing playback speed.
If you need full control, then use MediaController.seekTo(long) or MediaController.setPlaybackSpeed(float) directly.
Returns:
a representing the pending completion of the command
See also: MediaSession.SessionCallback.onFastForward(MediaSession, MediaSession.ControllerInfo)
Requests that the SessionPlayer associated with the connected MediaSession
to rewind playback.
The implementation may be different depending on the players. For example, it can be
implemented by seeking backward once, series of seeking backward, or decreasing playback
speed. If you need full control, then use MediaController.seekTo(long) or MediaController.setPlaybackSpeed(float)
directly.
Returns:
a representing the pending completion of the command
See also: MediaSession.SessionCallback.onRewind(MediaSession, MediaSession.ControllerInfo)
public <any>
skipForward()
Requests that the SessionPlayer associated with the connected MediaSession
skips backward within the current media item.
The implementation may be different depending on the players. For example, it can be
implemented by seeking forward once with the fixed amount of seconds, or seeking forward to
the nearest bookmark. If you need full control, then use MediaController.seekTo(long) directly.
Returns:
a representing the pending completion of the command
See also: MediaSession.SessionCallback.onSkipForward(MediaSession, MediaSession.ControllerInfo)
public <any>
skipBackward()
Requests that the SessionPlayer associated with the connected MediaSession
skips forward within the current media item.
The implementation may be different depending on the players. For example, it can be
implemented by seeking backward once with the fixed amount of seconds, or seeking backward to
the nearest bookmark. If you need full control, then use MediaController.seekTo(long) directly.
Returns:
a representing the pending completion of the command
See also: MediaSession.SessionCallback.onSkipBackward(MediaSession, MediaSession.ControllerInfo)
public <any>
seekTo(long position)
Requests that the SessionPlayer associated with the connected MediaSession
seeks to the specified position.
The position is the relative position based on the MediaItem.getStartPosition(). So
calling MediaController.seekTo(long) with 0 means the seek to the start position.
On success, a SessionResult would be returned with the current media item when the
command completed. If it's called in SessionPlayer.PLAYER_STATE_IDLE, it is ignored
and a SessionResult would be returned with
BaseResult.RESULT_ERROR_INVALID_STATE.
Parameters:
position: the new playback position in ms. The value should be in the range of start
and end positions defined in MediaItem.
Returns:
a representing the pending completion of the command
public <any>
setVolumeTo(int value, int flags)
Requests that the connected MediaSession sets the volume of the output that is
playing on. The command will be ignored if it does not support
VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE.
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 AudioManager
.
If the session is remote player (i.e. session has set volume provider), its volume provider
will receive this request instead.
Parameters:
value: the value to set it to, between 0 and the reported max
flags: flags from AudioManager
to include with the volume request for local
playback
Returns:
a representing the pending completion of the command
See also: MediaController.getPlaybackInfo()
public <any>
adjustVolume(int direction, int flags)
Requests that the connected MediaSession adjusts the volume of the output that is
playing on. The direction must be one of AudioManager
,
AudioManager
, or AudioManager
.
The command will be ignored if the session does not support
VolumeProviderCompat.VOLUME_CONTROL_RELATIVE or
VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE.
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 AudioManager
.
If the session is remote player (i.e. session has set volume provider), its volume provider
will receive this request instead.
Parameters:
direction: the direction to adjust the volume in
flags: flags from AudioManager
to include with the volume request for local
playback
Returns:
a representing the pending completion of the command
See also: MediaController.getPlaybackInfo()
public PendingIntent
getSessionActivity()
Gets an intent for launching UI associated with this session if one exists.
If it is not connected yet, it returns null.
Returns:
a PendingIntent
to launch UI or null
public int
getPlayerState()
Gets the state of the SessionPlayer associated with the connected
MediaSession. If it is not connected yet, it returns
SessionPlayer.PLAYER_STATE_IDLE.
Returns:
the player state
See also: MediaController.ControllerCallback.onPlayerStateChanged(MediaController, int), SessionPlayer.PLAYER_STATE_IDLE, SessionPlayer.PLAYER_STATE_PAUSED, SessionPlayer.PLAYER_STATE_PLAYING, SessionPlayer.PLAYER_STATE_ERROR
public long
getDuration()
Gets the duration of the current media item, or SessionPlayer.UNKNOWN_TIME if
unknown or not connected. If the current MediaItem has either start or end position,
then duration would be adjusted accordingly instead of returning the whole size of the
MediaItem.
Returns:
the duration in ms, or SessionPlayer.UNKNOWN_TIME if unknonw or not
connected.
public long
getCurrentPosition()
Gets the playback position of the SessionPlayer associated with the connected
MediaSession.
The position is the relative position based on the MediaItem.getStartPosition().
So the position 0 means the start position of the MediaItem.
Returns:
the current playback position in ms, or SessionPlayer.UNKNOWN_TIME
if unknown or not connected
public float
getPlaybackSpeed()
Gets the playback speed to be used by the of the SessionPlayer associated with the
connected MediaSession when playing. A value of 1.0f
is the default playback value, and a negative value indicates reverse playback.
Note that it may differ from the speed set in MediaController.setPlaybackSpeed(float).
Returns:
speed the playback speed, or 0f if unknown or not connected
public <any>
setPlaybackSpeed(float playbackSpeed)
Requests that the SessionPlayer associated with the connected MediaSession
sets the playback speed. The default playback speed is 1.0f is the default, and
negative values indicate reverse playback and 0.0f is not allowed.
The supported playback speed range depends on the player, so it is recommended to query the
actual speed of the player via MediaController.getPlaybackSpeed() after the operation completes.
In particular, please note that the player may not support reverse playback.
On success, a SessionResult would be returned with the current media item when the
command completed.
Parameters:
playbackSpeed: the requested playback speed
Returns:
a representing the pending completion of the command
See also: MediaController.getPlaybackSpeed()
public int
getBufferingState()
Gets the current buffering state of the SessionPlayer associated with the connected
MediaSession.
The position is the relative position based on the MediaItem.getStartPosition().
So the position 0 means the start position of the MediaItem.
Returns:
the buffering state, or SessionPlayer.BUFFERING_STATE_UNKNOWN
if unknown or not connected
public long
getBufferedPosition()
Gets the position for how much has been buffered of the SessionPlayer associated
with the connected MediaSession, or SessionPlayer.UNKNOWN_TIME if
unknown or not connected.
Returns:
buffering position in ms, or SessionPlayer.UNKNOWN_TIME if
unknown or not connected
Get the current playback info for this session.
If it is not connected yet, it returns null.
Returns:
the current playback info or null
public <any>
setRating(java.lang.String mediaId,
Rating rating)
Requests that the connected MediaSession rates 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
MediaMetadata.getRating(String) with the key
MediaMetadata.METADATA_KEY_USER_RATING.
If the user rating was null, the media item does not accept setting user rating.
Parameters:
mediaId: the non-empty media id
rating: the rating to set
Sends a custom command to the session
Interoperability: When connected to
android.support.v4.media.session.MediaSessionCompat
,
SessionResult.getResultCode() will return the custom result code from the
instead of the standard result codes
defined in the SessionResult.
A command is not accepted if it is not a custom command.
Parameters:
command: custom command
args: optional argument
public java.util.List<MediaItem>
getPlaylist()
Gets the playlist of the SessionPlayer associated with the connected
MediaSession. It can be null if the playlist hasn't been set or it's reset
by MediaController.setMediaItem(String).
This list may differ from the list that was specified with
MediaController.setPlaylist(List, MediaMetadata) depending on the SessionPlayer
implementation.
Returns:
playlist, or null if the playlist hasn't been set or the controller isn't
connected
See also: SessionCommand.COMMAND_CODE_PLAYER_GET_PLAYLIST
public <any>
setPlaylist(java.util.List<java.lang.String> list,
MediaMetadata metadata)
Requests that the SessionPlayer associated with the connected MediaSession
sets the playlist with the list of media IDs. Use this, MediaController.setMediaUri(Uri, Bundle), or
MediaController.setMediaItem(String) to specify which items to play.
All media IDs in the list shouldn't be an empty string.
This can be called multiple times in any states other than
SessionPlayer.PLAYER_STATE_ERROR. This would override previous call of this,
MediaController.setMediaItem(String), or MediaController.setMediaUri(Uri, Bundle).
The MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) and/or
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) would be called when it's completed.
The current item would be the first item in the playlist.
Parameters:
list: list of media id. Shouldn't contain an empty id
metadata: metadata of the playlist
See also: MediaController.setMediaItem(String), MediaController.setMediaUri(Uri, Bundle), MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem), MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata), MediaMetadata.METADATA_KEY_MEDIA_ID
public <any>
setMediaItem(java.lang.String mediaId)
Requests that the SessionPlayer associated with the connected MediaSession
sets a MediaItem for playback. Use this, MediaController.setMediaUri(Uri, Bundle), or
MediaController.setPlaylist(List, MediaMetadata) to specify which items to play.
If you want to change current item in the playlist, use one of MediaController.skipToPlaylistItem(int),
MediaController.skipToNextPlaylistItem(), or MediaController.skipToPreviousPlaylistItem() instead of this
method.
This can be called multiple times in any states other than
SessionPlayer.PLAYER_STATE_ERROR. This would override previous call of this,
MediaController.setMediaUri(Uri, Bundle), or MediaController.setPlaylist(List, MediaMetadata).
The MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) and/or
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) would be called when it's completed.
On success, a SessionResult would be returned with item set.
Parameters:
mediaId: the non-empty media id of the item to play
See also: MediaController.setMediaUri(Uri, Bundle), MediaController.setPlaylist(List, MediaMetadata), MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem), MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata)
public <any>
setMediaUri(Uri uri, Bundle extras)
Requests that the connected MediaSession sets a specific for playback.
Use this, MediaController.setMediaItem(String), or MediaController.setPlaylist(List, MediaMetadata) to specify which items to play.
This can be called multiple times in any states other than
SessionPlayer.PLAYER_STATE_ERROR. This would override previous call of this,
MediaController.setMediaItem(String), or MediaController.setPlaylist(List, MediaMetadata).
The MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) and/or
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) would be called when it's completed.
On success, a SessionResult would be returned with item set.
Interoperability: When connected to
android.support.v4.media.session.MediaSessionCompat
, this call will be grouped
together with later MediaController.prepare() or MediaController.play(), depending on the Uri pattern as
follows:
Returned will return SessionResult.RESULT_SUCCESS when it's
handled together with MediaController.prepare() or MediaController.play(). If this API is called multiple times
without prepare or play, then BaseResult.RESULT_INFO_SKIPPED will be returned
for previous calls.
Parameters:
uri: the Uri of the item to play
See also: MediaController.setMediaItem(String), MediaController.setPlaylist(List, MediaMetadata), MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem), MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata), MediaConstants.MEDIA_URI_AUTHORITY, MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID, MediaConstants.MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID, MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_SEARCH, MediaConstants.MEDIA_URI_PATH_PLAY_FROM_SEARCH, MediaConstants.MEDIA_URI_PATH_SET_MEDIA_URI, MediaConstants.MEDIA_URI_QUERY_ID, MediaConstants.MEDIA_URI_QUERY_QUERY, MediaConstants.MEDIA_URI_QUERY_URI
Requests that the SessionPlayer associated with the connected MediaSession
updates the playlist metadata while keeping the playlist as-is.
On success, a SessionResult would be returned with the current media item when the
command completed.
Parameters:
metadata: metadata of the playlist
See also: MediaController.ControllerCallback.onPlaylistMetadataChanged(MediaController, MediaMetadata)
Gets the playlist metadata of the SessionPlayer associated with the connected
MediaSession.
Returns:
metadata of the playlist, or null if none is set or the controller is not
connected
See also: MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata), MediaController.ControllerCallback.onPlaylistMetadataChanged(MediaController, MediaMetadata)
public <any>
addPlaylistItem(int index, java.lang.String mediaId)
Requests that the SessionPlayer associated with the connected MediaSession
adds the media item to the playlist at the index with the media
ID. Index equals to or greater than the current playlist size
(e.g. MAX_VALUE
) will add the item at the end of the playlist.
If index is less than or equal to the current index of the playlist,
the current index of the playlist will be increased correspondingly.
On success, a SessionResult would be returned with item added.
Parameters:
index: the index you want to add
mediaId: the non-empty media id of the new item
See also: MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata), MediaMetadata.METADATA_KEY_MEDIA_ID
public <any>
removePlaylistItem(int index)
Requests that the SessionPlayer associated with the connected MediaSession
removes the media item at index in the playlist.
On success, a SessionResult would be returned with item removed.
Parameters:
index: the media item you want to add
See also: MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata)
public <any>
replacePlaylistItem(int index, java.lang.String mediaId)
Requests that the SessionPlayer associated with the connected MediaSession
replaces the media item at index in the playlist with the media ID.
On success, a SessionResult would be returned with item set.
Parameters:
index: the index of the item to replace
mediaId: the non-empty media id of the new item
See also: MediaMetadata.METADATA_KEY_MEDIA_ID, MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata)
public <any>
movePlaylistItem(int fromIndex, int toIndex)
Requests that the SessionPlayer associated with the connected MediaSession
moves the media item at fromIdx to toIdx in the playlist.
On success, a SessionResult would be returned with item set.
Parameters:
fromIndex: the media item's initial index in the playlist
toIndex: the media item's target index in the playlist
See also: MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata)
Gets the current media item of the SessionPlayer associated with the connected
MediaSession. This can be currently playing or would be played with later
MediaController.play(). This value may be updated when
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) or
MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) is
called.
Returns:
the current media item. Can be null only when media item or playlist hasn't
been set or the controller is not connected.
See also: MediaController.setMediaItem(String), MediaController.setPlaylist(List, MediaMetadata)
public int
getCurrentMediaItemIndex()
Gets the current item index in the playlist of the SessionPlayer associated with
the connected MediaSession. The value would be updated when
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) or
MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) is called.
Returns:
the index of current item in playlist, or SessionPlayer.INVALID_ITEM_INDEX
if current media item does not exist or playlist hasn't been set
public int
getPreviousMediaItemIndex()
Gets the previous item index in the playlist of the SessionPlayer associated with
the connected MediaSession. This value would be updated when
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) or
MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) is called.
Interoperability: When connected to
android.support.v4.media.session.MediaSessionCompat
, this will always return
SessionPlayer.INVALID_ITEM_INDEX.
Returns:
the index of previous item in playlist, or SessionPlayer.INVALID_ITEM_INDEX
if previous media item does not exist or playlist hasn't been set
public int
getNextMediaItemIndex()
Gets the next item index in the playlist of the SessionPlayer associated with
the connected MediaSession. This value would be updated when
MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem) or
MediaController.ControllerCallback.onPlaylistChanged(MediaController, List, MediaMetadata) is called.
Interoperability: When connected to
android.support.v4.media.session.MediaSessionCompat
, this will always return
SessionPlayer.INVALID_ITEM_INDEX..
Returns:
the index of next item in playlist, or SessionPlayer.INVALID_ITEM_INDEX
if next media item does not exist or playlist hasn't been set
public <any>
skipToPreviousPlaylistItem()
Requests that the SessionPlayer associated with the connected MediaSession
skips to the previous item in the playlist.
On success, a SessionResult would be returned with the current media item when the
command completed.
Returns:
a representing the pending completion of the command
See also: MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem)
public <any>
skipToNextPlaylistItem()
Requests that the SessionPlayer associated with the connected MediaSession
skips to the next item in the playlist.
On success, a SessionResult would be returned with the current media item when the
command completed.
Returns:
a representing the pending completion of the command
See also: MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem)
public <any>
skipToPlaylistItem(int index)
Requests that the SessionPlayer associated with the connected MediaSession
skips to the item in the playlist at the index.
On success, a SessionResult would be returned with the current media item when the
command completed.
Parameters:
index: The index of the item you want to play in the playlist
Returns:
a representing the pending completion of the command
See also: MediaController.ControllerCallback.onCurrentMediaItemChanged(MediaController, MediaItem)
public int
getRepeatMode()
Gets the repeat mode of the SessionPlayer associated with the connected
MediaSession. If it is not connected yet, it returns
SessionPlayer.REPEAT_MODE_NONE.
Returns:
repeat mode
See also: SessionPlayer.REPEAT_MODE_NONE, SessionPlayer.REPEAT_MODE_ONE, SessionPlayer.REPEAT_MODE_ALL, SessionPlayer.REPEAT_MODE_GROUP
public <any>
setRepeatMode(int repeatMode)
Requests that the SessionPlayer associated with the connected MediaSession
sets the repeat mode.
On success, a SessionResult would be returned with the current media item when the
command completed.
Parameters:
repeatMode: repeat mode
Returns:
a which represents the pending completion of the command
See also: SessionPlayer.REPEAT_MODE_NONE, SessionPlayer.REPEAT_MODE_ONE, SessionPlayer.REPEAT_MODE_ALL, SessionPlayer.REPEAT_MODE_GROUP
public int
getShuffleMode()
Gets the shuffle mode of the SessionPlayer associated with the connected
MediaSession. If it is not connected yet, it returns
SessionPlayer.SHUFFLE_MODE_NONE.
Returns:
the shuffle mode
See also: SessionPlayer.SHUFFLE_MODE_NONE, SessionPlayer.SHUFFLE_MODE_ALL, SessionPlayer.SHUFFLE_MODE_GROUP
public <any>
setShuffleMode(int shuffleMode)
Requests that the SessionPlayer associated with the connected MediaSession
sets the shuffle mode.
On success, a SessionResult would be returned with the current media item when the
command completed.
Parameters:
shuffleMode: the shuffle mode
Returns:
a which represents the pending completion of the command
See also: SessionPlayer.SHUFFLE_MODE_NONE, SessionPlayer.SHUFFLE_MODE_ALL, SessionPlayer.SHUFFLE_MODE_GROUP
Gets the video size of the SessionPlayer associated with the connected
MediaSession. If it is not connected yet, it returns new VideoSize(0, 0).
Returns:
the size of the video. The width and height of size could be 0 if there is no video
or the size has not been determined yet.
See also: MediaController.ControllerCallback.onVideoSizeChanged(MediaController, VideoSize)
public <any>
setSurface(Surface surface)
Requests that the SessionPlayer associated with the connected MediaSession
sets the to be used as the sink for the video portion of the media.
A null surface will reset any Surface and result in only the audio track being played.
On success, a SessionResult is returned with the current media item when the command
completed.
Parameters:
surface: the to be used for the video portion of the media
Returns:
a which represents the pending completion of the command
public java.util.List<SessionPlayer.TrackInfo>
getTracks()
Gets the full list of selected and unselected tracks that the media contains of the
SessionPlayer associated with the connected MediaSession. The order of
the list is irrelevant as different players expose tracks in different ways, but the tracks
will generally be ordered based on track type.
The types of tracks supported may vary based on player implementation.
Returns:
list of tracks. The total number of tracks is the size of the list. If empty,
an empty list would be returned.
See also: SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA
Requests that the SessionPlayer associated with the connected MediaSession
selects the SessionPlayer.TrackInfo for the current media item.
Generally one track will be selected for each track type.
The types of tracks supported may vary based on players.
Note: MediaController.getTracks() returns the list of tracks that can be selected, but the
list may be invalidated when
MediaController.ControllerCallback.onTracksChanged(MediaController, List) is called.
Parameters:
trackInfo: track to be selected
Returns:
a which represents the pending completion of the command
See also: SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA, MediaController.ControllerCallback
Requests that the SessionPlayer associated with the connected MediaSession
deselects the SessionPlayer.TrackInfo for the current media item.
Generally, a track should already be selected in order to be deselected and audio and video
tracks should not be deselected.
The types of tracks supported may vary based on players.
Note: MediaController.getSelectedTrack(int) returns the currently selected track per track type that
can be deselected, but the list may be invalidated when
MediaController.ControllerCallback.onTracksChanged(MediaController, List) is called.
Parameters:
trackInfo: track to be deselected
Returns:
a which represents the pending completion of the command
See also: SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA, MediaController.ControllerCallback
Gets the currently selected track for the given track type of the SessionPlayer
associated with the connected MediaSession. If it is not connected yet, it returns
null.
The returned value can be outdated after
MediaController.ControllerCallback.onTracksChanged(MediaController, List),
MediaController.ControllerCallback,
or MediaController.ControllerCallback is called.
Parameters:
trackType: type of selected track
Returns:
selected track info
See also: SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA
public void
setTimeDiff(java.lang.Long timeDiff)
Sets the time diff forcefully when calculating current position.
Parameters:
timeDiff: null for reset
Registers an extra MediaController.ControllerCallback.
Parameters:
executor: a callback executor
callback: a ControllerCallback
See also: MediaController.unregisterExtraCallback(MediaController.ControllerCallback)
Unregisters an MediaController.ControllerCallback that has been registered by
MediaController.registerExtraCallback(Executor, MediaController.ControllerCallback).
The callback passed to MediaController.Builder
can not be unregistered by this method.
Parameters:
callback: a ControllerCallback
See also: MediaController.registerExtraCallback(Executor, MediaController.ControllerCallback)
public java.util.List<Pair>
getExtraControllerCallbacks()
Gets the cached allowed commands from MediaController.ControllerCallback.onAllowedCommandsChanged(MediaController, SessionCommandGroup).
If it is not connected yet, it returns null.
Returns:
the allowed commands
Source
/*
* Copyright 2019 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.media2.session;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media2.common.SessionPlayer.BUFFERING_STATE_UNKNOWN;
import static androidx.media2.common.SessionPlayer.INVALID_ITEM_INDEX;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE;
import static androidx.media2.common.SessionPlayer.REPEAT_MODE_NONE;
import static androidx.media2.common.SessionPlayer.SHUFFLE_MODE_NONE;
import static androidx.media2.common.SessionPlayer.UNKNOWN_TIME;
import android.app.PendingIntent;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Pair;
import androidx.media.AudioAttributesCompat;
import androidx.media.VolumeProviderCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.SessionPlayer.RepeatMode;
import androidx.media2.common.SessionPlayer.ShuffleMode;
import androidx.media2.common.SessionPlayer.TrackInfo;
import androidx.media2.common.SubtitleData;
import androidx.media2.common.VideoSize;
import androidx.media2.session.MediaSession.CommandButton;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.Closeable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Allows an app to interact with an active {@link MediaSession} or a {@link MediaSessionService}
* which would provide {@link MediaSession}. Media buttons and other commands can be sent to the
* session.
*
* <p>MediaController objects are thread-safe.
*
* <p>Topics covered here:
*
* <ol>
* <li><a href="#ControllerLifeCycle">Controller Lifecycle</a>
* <li><a href="#MediaSessionInTheSameProcess">Controlling the {@link MediaSession} in the same
* process</a>
* <li><a href="#PackageVisibilityFilter">Package Visibility Filter</a>
* </ol>
*
* <h3 id="ControllerLifeCycle">Controller Lifecycle</h3>
*
* <p>When a controller is created with the {@link SessionToken} for a {@link MediaSession} (i.e.
* session token type is {@link SessionToken#TYPE_SESSION}), the controller will connect to the
* specific session.
*
* <p>When a controller is created with the {@link SessionToken} for a {@link MediaSessionService}
* (i.e. session token type is {@link SessionToken#TYPE_SESSION_SERVICE} or {@link
* SessionToken#TYPE_LIBRARY_SERVICE}), the controller binds to the service for connecting to a
* {@link MediaSession} in it. {@link MediaSessionService} will provide a session to connect.
*
* <p>When a controller connects to a session, {@link
* MediaSession.SessionCallback#onConnect(MediaSession, MediaSession.ControllerInfo)} will be called
* to either accept or reject the connection. Wait {@link
* ControllerCallback#onConnected(MediaController, SessionCommandGroup)} or {@link
* ControllerCallback#onDisconnected(MediaController)} for the result.
*
* <p>When the connected session is closed, the controller will receive {@link
* ControllerCallback#onDisconnected(MediaController)}.
*
* <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><a name="MediaSessionInTheSameProcess"></a>
*
* <h3>Controlling the MediaSession in the same process</h3>
*
* When you control the {@link MediaSession} and its {@link SessionPlayer}, it's recommended to use
* them directly rather than creating {@link MediaController}. However, if you need to use {@link
* MediaController} in the same process, be careful not to block session callback executor's thread.
* Here's an example code that would never return due to the thread issue.
*
* <p>
*
* <pre>{@code
* // Code runs on the main thread.
* MediaSession session = new MediaSession.Builder(context, player)
* .setSessionCallback(sessionCallback, Context.getMainExecutor(context)).build();
* MediaController controller = new MediaController.Builder(context)
* .setSessionToken(session.getToken())
* .setControllerCallback(Context.getMainExecutor(context), controllerCallback)
* .build();
*
* // This will hang and never return.
* controller.play().get();
* }</pre>
*
* When a session gets a command from a controller, the session's {@link
* MediaSession.SessionCallback#onCommandRequest} would be executed on the session's callback
* executor to decide whether to ignore or handle the incoming command. To do so, the session's
* callback executor shouldn't be blocked to handle the incoming calls. However, if you call {@link
* ListenableFuture#get} on the thread for the session callback executor, then your call wouldn't be
* executed and never return.
*
* <p>To avoid such issue, don't block the session callback executor's thread. Creating a dedicated
* thread for the session callback executor would be helpful. See {@link
* Executors#newSingleThreadExecutor} for creating a new thread.
*
* <h3 id="PackageVisibilityFilter">Package Visibility Filter</h3>
*
* <p>The app targeting API level 30 or higher must include a {@code <queries>} element in their
* manifest to connect to a service component of another app like {@link MediaSessionService},
* {@link MediaLibraryService}, or {@link androidx.media.MediaBrowserServiceCompat}). See the
* following example and <a href="{@docRoot}training/package-visibility">this guide</a> for more
* information.
*
* <pre>{@code
* <!-- As intent actions -->
* <intent>
* <action android:name="androidx.media2.session.MediaSessionService" />
* </intent>
* <intent>
* <action android:name="androidx.media2.session.MediaLibraryService" />
* </intent>
* <intent>
* <action android:name="android.media.browse.MediaBrowserService" />
* </intent>
* <!-- Or, as a package name -->
* <package android:name="package_name_of_the_other_app" />
* }</pre>
*
* @see MediaSession
* @see MediaSessionService
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
public class MediaController implements Closeable {
private static final String TAG = "MediaController";
/**
*/
@RestrictTo(LIBRARY)
@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 {}
/**
*/
@RestrictTo(LIBRARY)
@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 {}
final Object mLock = new Object();
@GuardedBy("mLock")
MediaControllerImpl mImpl;
@GuardedBy("mLock")
boolean mClosed;
final ControllerCallback mPrimaryCallback;
final Executor mPrimaryCallbackExecutor;
@GuardedBy("mLock")
private final List<Pair<ControllerCallback, Executor>> mExtraControllerCallbacks =
new ArrayList<>();
// For testing.
Long mTimeDiff;
/**
* Creates a {@link MediaController} from the {@link SessionToken}.
*
* @param context context
* @param token token to connect to
* @param executor executor to run callbacks on
* @param callback controller callback to receive changes in
*/
MediaController(@NonNull final Context context, @NonNull final SessionToken token,
@Nullable Bundle connectionHints, @Nullable Executor executor,
@Nullable ControllerCallback callback) {
if (context == null) {
throw new NullPointerException("context shouldn't be null");
}
if (token == null) {
throw new NullPointerException("token shouldn't be null");
}
mPrimaryCallback = callback;
mPrimaryCallbackExecutor = executor;
synchronized (mLock) {
mImpl = createImpl(context, token, connectionHints);
}
}
/**
* Creates a {@link MediaController} from the {@link MediaSessionCompat.Token}.
*
* @param context context
* @param token token to connect to
* @param executor executor to run callbacks on
* @param callback controller callback to receive changes in
*/
MediaController(@NonNull final Context context, @NonNull final MediaSessionCompat.Token token,
@Nullable final Bundle connectionHints, @Nullable final Executor executor,
@Nullable final ControllerCallback callback) {
if (context == null) {
throw new NullPointerException("context shouldn't be null");
}
if (token == null) {
throw new NullPointerException("token shouldn't be null");
}
mPrimaryCallback = callback;
mPrimaryCallbackExecutor = executor;
SessionToken.createSessionToken(context, token, (compatToken, sessionToken) -> {
boolean closed;
synchronized (mLock) {
closed = mClosed;
if (!closed) {
mImpl = createImpl(context, sessionToken, connectionHints);
}
}
if (closed) {
notifyAllControllerCallbacks(cb -> cb.onDisconnected(MediaController.this));
}
});
}
MediaControllerImpl createImpl(@NonNull Context context, @NonNull SessionToken token,
@Nullable Bundle connectionHints) {
if (token.isLegacySession()) {
return new MediaControllerImplLegacy(context, this, token);
} else {
return new MediaControllerImplBase(context, this, token, connectionHints);
}
}
MediaControllerImpl getImpl() {
synchronized (mLock) {
return mImpl;
}
}
/**
* Releases this object, and disconnects from the session. After this, callbacks wouldn't be
* received.
*/
@Override
public void close() {
try {
MediaControllerImpl impl;
synchronized (mLock) {
if (mClosed) {
return;
}
mClosed = true;
impl = mImpl;
}
if (impl != null) {
impl.close();
}
} catch (Exception e) {
// Should not be here.
}
}
/**
* Returns the {@link SessionToken} of the connected session.
* If it is not connected yet, it returns {@code null}.
* <p>
* This may differ from the {@link SessionToken} from the constructor. For example, if the
* controller is created with the token for {@link MediaSessionService}, this would return
* token for the {@link MediaSession} in the service.
*
* @return SessionToken of the connected session, or {@code null} if not connected
*/
@Nullable
public SessionToken getConnectedToken() {
return isConnected() ? getImpl().getConnectedToken() : null;
}
/**
* Returns whether this class is connected to active {@link MediaSession} or not.
*/
public boolean isConnected() {
MediaControllerImpl impl = getImpl();
return impl != null && impl.isConnected();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* starts or resumes playback.
* <p>
* On success, this transfers the player state to {@link SessionPlayer#PLAYER_STATE_PLAYING}
* and a {@link SessionResult} would be returned with the current media item when the command
* was completed.
* If the player state is {@link SessionPlayer#PLAYER_STATE_IDLE}, the session would also call
* {@link SessionPlayer#prepare} and then {@link SessionPlayer#play} to start playback. If you
* want to have finer grained control of the playback start, call {@link #prepare} manually
* before this. Calling {@link #prepare} in advance would help this method to start playback
* faster and also help to take audio focus at the last moment.
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat}, then this will be grouped
* together with previously called {@link #setMediaUri}. See {@link #setMediaUri} for
* details.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #prepare
* @see #setMediaUri
*/
@NonNull
public ListenableFuture<SessionResult> play() {
if (isConnected()) {
return getImpl().play();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* pauses playback.
* <p>
* On success, this transfers the player state to {@link SessionPlayer#PLAYER_STATE_PAUSED} and
* a {@link SessionResult} would be returned with the current media item when the command
* was completed. If it is called in {@link SessionPlayer#PLAYER_STATE_IDLE} or
* {@link SessionPlayer#PLAYER_STATE_ERROR}, it whould be ignored and a {@link SessionResult}
* would be returned with {@link SessionResult#RESULT_ERROR_INVALID_STATE}.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
*/
@NonNull
public ListenableFuture<SessionResult> pause() {
if (isConnected()) {
return getImpl().pause();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* prepares the media items for playback. During this time, the player may allocate resources
* required to play, such as audio and video decoders. Before calling this API, sets media
* item(s) through either {@link #setMediaItem} or {@link #setPlaylist}.
* <p>
* On success, this transfers the player state from {@link SessionPlayer#PLAYER_STATE_IDLE} to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} and a {@link SessionResult} would be returned
* with the prepared media item when the command completed. If it's not called in
* {@link SessionPlayer#PLAYER_STATE_IDLE}, it would be ignored and {@link SessionResult}
* would be returned with {@link SessionResult#RESULT_ERROR_INVALID_STATE}.
* <p>
* Playback can be started without this. But this provides finer grained control of playback
* start. See {@link #play} for details.
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat}, then this call may be grouped
* together with previously called {@link #setMediaUri}. See {@link #setMediaUri} for
* details.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #play
* @see #setMediaUri
*/
@NonNull
public ListenableFuture<SessionResult> prepare() {
if (isConnected()) {
return getImpl().prepare();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* to fast forward playback.
* <p>
* The implementation may be different depending on the players. For example, it can be
* implemented by seeking forward once, series of seeking forward, or increasing playback speed.
* If you need full control, then use {@link #seekTo} or {@link #setPlaybackSpeed} directly.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see MediaSession.SessionCallback#onFastForward(MediaSession, MediaSession.ControllerInfo)
*/
@NonNull
public ListenableFuture<SessionResult> fastForward() {
if (isConnected()) {
return getImpl().fastForward();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* to rewind playback.
* <p>
* The implementation may be different depending on the players. For example, it can be
* implemented by seeking backward once, series of seeking backward, or decreasing playback
* speed. If you need full control, then use {@link #seekTo} or {@link #setPlaybackSpeed}
* directly.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see MediaSession.SessionCallback#onRewind(MediaSession, MediaSession.ControllerInfo)
*/
@NonNull
public ListenableFuture<SessionResult> rewind() {
if (isConnected()) {
return getImpl().rewind();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* skips backward within the current media item.
* <p>
* The implementation may be different depending on the players. For example, it can be
* implemented by seeking forward once with the fixed amount of seconds, or seeking forward to
* the nearest bookmark. If you need full control, then use {@link #seekTo} directly.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see MediaSession.SessionCallback#onSkipForward(MediaSession, MediaSession.ControllerInfo)
*/
@NonNull
public ListenableFuture<SessionResult> skipForward() {
// To match with KEYCODE_MEDIA_SKIP_FORWARD
if (isConnected()) {
return getImpl().skipForward();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* skips forward within the current media item.
* <p>
* The implementation may be different depending on the players. For example, it can be
* implemented by seeking backward once with the fixed amount of seconds, or seeking backward to
* the nearest bookmark. If you need full control, then use {@link #seekTo} directly.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see MediaSession.SessionCallback#onSkipBackward(MediaSession, MediaSession.ControllerInfo)
*/
@NonNull
public ListenableFuture<SessionResult> skipBackward() {
// To match with KEYCODE_MEDIA_SKIP_BACKWARD
if (isConnected()) {
return getImpl().skipBackward();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* seeks to the specified position.
* <p>
* The position is the relative position based on the {@link MediaItem#getStartPosition()}. So
* calling {@link #seekTo(long)} with {@code 0} means the seek to the start position.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed. If it's called in {@link SessionPlayer#PLAYER_STATE_IDLE}, it is ignored
* and a {@link SessionResult} would be returned with
* {@link SessionResult#RESULT_ERROR_INVALID_STATE}.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @param position the new playback position in ms. The value should be in the range of start
* and end positions defined in {@link MediaItem}.
*/
@NonNull
public ListenableFuture<SessionResult> seekTo(long position) {
if (isConnected()) {
return getImpl().seekTo(position);
}
return createDisconnectedFuture();
}
/**
* Requests that the connected {@link MediaSession} sets the volume of the output that 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.
*
* @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
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #getPlaybackInfo()
*/
@NonNull
public ListenableFuture<SessionResult> setVolumeTo(int value, @VolumeFlags int flags) {
if (isConnected()) {
return getImpl().setVolumeTo(value, flags);
}
return createDisconnectedFuture();
}
/**
* Requests that the connected {@link MediaSession} adjusts the volume of the output that 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.
*
* @param direction the direction to adjust the volume in
* @param flags flags from {@link AudioManager} to include with the volume request for local
* playback
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #getPlaybackInfo()
*/
@NonNull
public ListenableFuture<SessionResult> adjustVolume(@VolumeDirection int direction,
@VolumeFlags int flags) {
if (isConnected()) {
return getImpl().adjustVolume(direction, flags);
}
return createDisconnectedFuture();
}
/**
* Gets an intent for launching UI associated with this session if one exists.
* If it is not connected yet, it returns {@code null}.
*
* @return a {@link PendingIntent} to launch UI or null
*/
@Nullable
public PendingIntent getSessionActivity() {
return isConnected() ? getImpl().getSessionActivity() : null;
}
/**
* Gets the state of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}. If it is not connected yet, it returns
* {@link SessionPlayer#PLAYER_STATE_IDLE}.
*
* @return the player state
* @see ControllerCallback#onPlayerStateChanged(MediaController, int)
* @see SessionPlayer#PLAYER_STATE_IDLE
* @see SessionPlayer#PLAYER_STATE_PAUSED
* @see SessionPlayer#PLAYER_STATE_PLAYING
* @see SessionPlayer#PLAYER_STATE_ERROR
*/
public int getPlayerState() {
return isConnected() ? getImpl().getPlayerState() : PLAYER_STATE_IDLE;
}
/**
* Gets the duration of the current media item, or {@link SessionPlayer#UNKNOWN_TIME} if
* unknown or not connected. If the current {@link MediaItem} has either start or end position,
* then duration would be adjusted accordingly instead of returning the whole size of the
* {@link MediaItem}.
*
* @return the duration in ms, or {@link SessionPlayer#UNKNOWN_TIME} if unknonw or not
* connected.
*/
public long getDuration() {
return isConnected() ? getImpl().getDuration() : UNKNOWN_TIME;
}
/**
* Gets the playback position of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}.
* <p>
* The position is the relative position based on the {@link MediaItem#getStartPosition()}.
* So the position {@code 0} means the start position of the {@link MediaItem}.
*
* @return the current playback position in ms, or {@link SessionPlayer#UNKNOWN_TIME}
* if unknown or not connected
*/
public long getCurrentPosition() {
return isConnected() ? getImpl().getCurrentPosition() : UNKNOWN_TIME;
}
/**
* Gets the playback speed to be used by the of the {@link SessionPlayer} associated with the
* connected {@link MediaSession} when playing. A value of {@code 1.0f}
* is the default playback value, and a negative value indicates reverse playback.
* <p>
* Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
*
* @return speed the playback speed, or 0f if unknown or not connected
*/
public float getPlaybackSpeed() {
return isConnected() ? getImpl().getPlaybackSpeed() : 0f;
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* sets the playback speed. The default playback speed is {@code 1.0f} is the default, and
* negative values indicate reverse playback and {@code 0.0f} is not allowed.
* <p>
* The supported playback speed range depends on the player, so it is recommended to query the
* actual speed of the player via {@link #getPlaybackSpeed()} after the operation completes.
* In particular, please note that the player may not support reverse playback.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @param playbackSpeed the requested playback speed
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #getPlaybackSpeed()
* @see SessionPlayer.PlayerCallback#onPlaybackSpeedChanged(SessionPlayer, float)
* @throws IllegalArgumentException if the {@code speed} is equal to zero.
*/
@NonNull
public ListenableFuture<SessionResult> setPlaybackSpeed(float playbackSpeed) {
if (playbackSpeed == 0.0f) {
throw new IllegalArgumentException("speed must not be zero");
}
if (isConnected()) {
return getImpl().setPlaybackSpeed(playbackSpeed);
}
return createDisconnectedFuture();
}
/**
* Gets the current buffering state of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}.
* <p>
* The position is the relative position based on the {@link MediaItem#getStartPosition()}.
* So the position {@code 0} means the start position of the {@link MediaItem}.
*
* @return the buffering state, or {@link SessionPlayer#BUFFERING_STATE_UNKNOWN}
* if unknown or not connected
*/
@SessionPlayer.BuffState
public int getBufferingState() {
return isConnected() ? getImpl().getBufferingState() : BUFFERING_STATE_UNKNOWN;
}
/**
* Gets the position for how much has been buffered of the {@link SessionPlayer} associated
* with the connected {@link MediaSession}, or {@link SessionPlayer#UNKNOWN_TIME} if
* unknown or not connected.
*
* @return buffering position in ms, or {@link SessionPlayer#UNKNOWN_TIME} if
* unknown or not connected
*/
public long getBufferedPosition() {
return isConnected() ? getImpl().getBufferedPosition() : UNKNOWN_TIME;
}
/**
* Get the current playback info for this session.
* If it is not connected yet, it returns {@code null}.
*
* @return the current playback info or null
*/
@Nullable
public PlaybackInfo getPlaybackInfo() {
return isConnected() ? getImpl().getPlaybackInfo() : null;
}
/**
* Requests that the connected {@link MediaSession} rates 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 MediaMetadata#getRating(String)} with the key
* {@link MediaMetadata#METADATA_KEY_USER_RATING}.
* <p>
* If the user rating was {@code null}, the media item does not accept setting user rating.
*
* @param mediaId the non-empty media id
* @param rating the rating to set
*/
@NonNull
public ListenableFuture<SessionResult> setRating(@NonNull String mediaId,
@NonNull Rating rating) {
if (mediaId == null) {
throw new NullPointerException("mediaId shouldn't be null");
} else if (TextUtils.isEmpty(mediaId)) {
throw new IllegalArgumentException("mediaId shouldn't be empty");
}
if (rating == null) {
throw new NullPointerException("rating shouldn't be null");
}
if (isConnected()) {
return getImpl().setRating(mediaId, rating);
}
return createDisconnectedFuture();
}
/**
* Sends a custom command to the session
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat},
* {@link SessionResult#getResultCode()} will return the custom result code from the
* {@link ResultReceiver#onReceiveResult(int, Bundle)} instead of the standard result codes
* defined in the {@link SessionResult}.
* <p>
* A command is not accepted if it is not a custom command.
*
* @param command custom command
* @param args optional argument
*/
@NonNull
public ListenableFuture<SessionResult> sendCustomCommand(@NonNull SessionCommand command,
@Nullable Bundle args) {
if (command == null) {
throw new NullPointerException("command shouldn't be null");
}
if (command.getCommandCode() != SessionCommand.COMMAND_CODE_CUSTOM) {
throw new IllegalArgumentException("command should be a custom command");
}
if (isConnected()) {
return getImpl().sendCustomCommand(command, args);
}
return createDisconnectedFuture();
}
/**
* Gets the playlist of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}. It can be {@code null} if the playlist hasn't been set or it's reset
* by {@link #setMediaItem}.
* <p>
* This list may differ from the list that was specified with
* {@link #setPlaylist(List, MediaMetadata)} depending on the {@link SessionPlayer}
* implementation.
*
* @return playlist, or {@code null} if the playlist hasn't been set or the controller isn't
* connected
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST
*/
@Nullable
public List<MediaItem> getPlaylist() {
return isConnected() ? getImpl().getPlaylist() : null;
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* sets the playlist with the list of media IDs. Use this, {@link #setMediaUri}, or
* {@link #setMediaItem} to specify which items to play.
* <p>
* All media IDs in the list shouldn't be an empty string.
* <p>
* This can be called multiple times in any states other than
* {@link SessionPlayer#PLAYER_STATE_ERROR}. This would override previous call of this,
* {@link #setMediaItem}, or {@link #setMediaUri}.
* <p>
* The {@link ControllerCallback#onPlaylistChanged} and/or
* {@link ControllerCallback#onCurrentMediaItemChanged} would be called when it's completed.
* The current item would be the first item in the playlist.
*
* @param list list of media id. Shouldn't contain an empty id
* @param metadata metadata of the playlist
* @see #setMediaItem
* @see #setMediaUri
* @see ControllerCallback#onCurrentMediaItemChanged
* @see ControllerCallback#onPlaylistChanged
* @see MediaMetadata#METADATA_KEY_MEDIA_ID
* @throws IllegalArgumentException if the list is {@code null} or contains any empty string.
*/
@NonNull
public ListenableFuture<SessionResult> setPlaylist(@NonNull List<String> list,
@Nullable MediaMetadata metadata) {
if (list == null) {
throw new NullPointerException("list shouldn't be null");
}
for (int i = 0; i < list.size(); i++) {
if (TextUtils.isEmpty(list.get(i))) {
throw new IllegalArgumentException("list shouldn't contain empty id, index=" + i);
}
}
if (isConnected()) {
return getImpl().setPlaylist(list, metadata);
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* sets a {@link MediaItem} for playback. Use this, {@link #setMediaUri}, or
* {@link #setPlaylist} to specify which items to play.
* If you want to change current item in the playlist, use one of {@link #skipToPlaylistItem},
* {@link #skipToNextPlaylistItem}, or {@link #skipToPreviousPlaylistItem} instead of this
* method.
* <p>
* This can be called multiple times in any states other than
* {@link SessionPlayer#PLAYER_STATE_ERROR}. This would override previous call of this,
* {@link #setMediaUri}, or {@link #setPlaylist}.
* <p>
* The {@link ControllerCallback#onPlaylistChanged} and/or
* {@link ControllerCallback#onCurrentMediaItemChanged} would be called when it's completed.
* <p>
* On success, a {@link SessionResult} would be returned with {@code item} set.
*
* @param mediaId the non-empty media id of the item to play
* @see #setMediaUri
* @see #setPlaylist
* @see ControllerCallback#onCurrentMediaItemChanged
* @see ControllerCallback#onPlaylistChanged
*/
@NonNull
public ListenableFuture<SessionResult> setMediaItem(@NonNull String mediaId) {
if (TextUtils.isEmpty(mediaId)) {
throw new IllegalArgumentException("mediaId shouldn't be empty");
}
if (isConnected()) {
return getImpl().setMediaItem(mediaId);
}
return createDisconnectedFuture();
}
/**
* Requests that the connected {@link MediaSession} sets a specific {@link Uri} for playback.
* Use this, {@link #setMediaItem}, or {@link #setPlaylist} to specify which items to play.
* <p>
* This can be called multiple times in any states other than
* {@link SessionPlayer#PLAYER_STATE_ERROR}. This would override previous call of this,
* {@link #setMediaItem}, or {@link #setPlaylist}.
* <p>
* The {@link ControllerCallback#onPlaylistChanged} and/or
* {@link ControllerCallback#onCurrentMediaItemChanged} would be called when it's completed.
* <p>
* On success, a {@link SessionResult} would be returned with {@code item} set.
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat}, this call will be grouped
* together with later {@link #prepare} or {@link #play}, depending on the Uri pattern as
* follows:
* <table>
* <tr>
* <th align="left">Uri patterns</th><th>Following API calls</th><th>Method</th>
* </tr><tr>
* <td rowspan="2">{@code androidx://media2-session/setMediaUri?uri=[uri]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr><tr>
* <td rowspan="2">{@code androidx://media2-session/setMediaUri?id=[mediaId]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
* </tr><tr>
* <td rowspan="2">{@code androidx://media2-session/setMediaUri?query=[query]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
* </tr><tr>
* <td rowspan="2">Does not match with any pattern above</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr></table>
* <p>
* Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's
* handled together with {@link #prepare} or {@link #play}. If this API is called multiple times
* without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned
* for previous calls.
*
* @param uri the Uri of the item to play
* @see #setMediaItem
* @see #setPlaylist
* @see ControllerCallback#onCurrentMediaItemChanged
* @see ControllerCallback#onPlaylistChanged
* @see MediaConstants#MEDIA_URI_AUTHORITY
* @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID
* @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID
* @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_SEARCH
* @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_SEARCH
* @see MediaConstants#MEDIA_URI_PATH_SET_MEDIA_URI
* @see MediaConstants#MEDIA_URI_QUERY_ID
* @see MediaConstants#MEDIA_URI_QUERY_QUERY
* @see MediaConstants#MEDIA_URI_QUERY_URI
*/
@NonNull
public ListenableFuture<SessionResult> setMediaUri(@NonNull Uri uri, @Nullable Bundle extras) {
if (uri == null) {
throw new NullPointerException("mediaUri shouldn't be null");
}
if (isConnected()) {
return getImpl().setMediaUri(uri, extras);
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* updates the playlist metadata while keeping the playlist as-is.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @param metadata metadata of the playlist
* @see ControllerCallback#onPlaylistMetadataChanged(MediaController, MediaMetadata)
*/
@NonNull
public ListenableFuture<SessionResult> updatePlaylistMetadata(
@Nullable MediaMetadata metadata) {
if (isConnected()) {
return getImpl().updatePlaylistMetadata(metadata);
}
return createDisconnectedFuture();
}
/**
* Gets the playlist metadata of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}.
*
* @return metadata of the playlist, or null if none is set or the controller is not
* connected
* @see ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)
* @see ControllerCallback#onPlaylistMetadataChanged(MediaController, MediaMetadata)
*/
@Nullable
public MediaMetadata getPlaylistMetadata() {
return isConnected() ? getImpl().getPlaylistMetadata() : null;
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* adds the media item to the playlist at the index with the media
* ID. Index equals to or greater than the current playlist size
* (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of the playlist.
* <p>
* If index is less than or equal to the current index of the playlist,
* the current index of the playlist will be increased correspondingly.
* <p>
* On success, a {@link SessionResult} would be returned with {@code item} added.
*
* @param index the index you want to add
* @param mediaId the non-empty media id of the new item
* @see ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)
* @see MediaMetadata#METADATA_KEY_MEDIA_ID
*/
@NonNull
public ListenableFuture<SessionResult> addPlaylistItem(@IntRange(from = 0) int index,
@NonNull String mediaId) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
if (TextUtils.isEmpty(mediaId)) {
throw new IllegalArgumentException("mediaId shouldn't be empty");
}
if (isConnected()) {
return getImpl().addPlaylistItem(index, mediaId);
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* removes the media item at index in the playlist.
* <p>
* On success, a {@link SessionResult} would be returned with {@code item} removed.
*
* @param index the media item you want to add
* @see ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)
*/
@NonNull
public ListenableFuture<SessionResult> removePlaylistItem(@IntRange(from = 0) int index) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
if (isConnected()) {
return getImpl().removePlaylistItem(index);
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* replaces the media item at index in the playlist with the media ID.
* <p>
* On success, a {@link SessionResult} would be returned with {@code item} set.
*
* @param index the index of the item to replace
* @param mediaId the non-empty media id of the new item
* @see MediaMetadata#METADATA_KEY_MEDIA_ID
* @see ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)
*/
@NonNull
public ListenableFuture<SessionResult> replacePlaylistItem(@IntRange(from = 0) int index,
@NonNull String mediaId) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
if (TextUtils.isEmpty(mediaId)) {
throw new IllegalArgumentException("mediaId shouldn't be empty");
}
if (isConnected()) {
return getImpl().replacePlaylistItem(index, mediaId);
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* moves the media item at {@code fromIdx} to {@code toIdx} in the playlist.
* <p>
* On success, a {@link SessionResult} would be returned with {@code item} set.
*
* @param fromIndex the media item's initial index in the playlist
* @param toIndex the media item's target index in the playlist
* @see ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)
*/
@NonNull
public ListenableFuture<SessionResult> movePlaylistItem(@IntRange(from = 0) int fromIndex,
@IntRange(from = 0) int toIndex) {
if (fromIndex < 0 || toIndex < 0) {
throw new IllegalArgumentException("indexes shouldn't be negative");
}
if (isConnected()) {
return getImpl().movePlaylistItem(fromIndex, toIndex);
}
return createDisconnectedFuture();
}
/**
* Gets the current media item of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}. This can be currently playing or would be played with later
* {@link #play}. This value may be updated when
* {@link ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)} or
* {@link ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)} is
* called.
*
* @return the current media item. Can be {@code null} only when media item or playlist hasn't
* been set or the controller is not connected.
* @see #setMediaItem
* @see #setPlaylist
*/
@Nullable
public MediaItem getCurrentMediaItem() {
return isConnected() ? getImpl().getCurrentMediaItem() : null;
}
/**
* Gets the current item index in the playlist of the {@link SessionPlayer} associated with
* the connected {@link MediaSession}. The value would be updated when
* {@link ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)} or
* {@link ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)} is called.
*
* @return the index of current item in playlist, or {@link SessionPlayer#INVALID_ITEM_INDEX}
* if current media item does not exist or playlist hasn't been set
*/
public int getCurrentMediaItemIndex() {
return isConnected() ? getImpl().getCurrentMediaItemIndex() : INVALID_ITEM_INDEX;
}
/**
* Gets the previous item index in the playlist of the {@link SessionPlayer} associated with
* the connected {@link MediaSession}. This value would be updated when
* {@link ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)} or
* {@link ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)} is called.
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat}, this will always return
* {@link SessionPlayer#INVALID_ITEM_INDEX}.
*
* @return the index of previous item in playlist, or {@link SessionPlayer#INVALID_ITEM_INDEX}
* if previous media item does not exist or playlist hasn't been set
*/
public int getPreviousMediaItemIndex() {
return isConnected() ? getImpl().getPreviousMediaItemIndex() : INVALID_ITEM_INDEX;
}
/**
* Gets the next item index in the playlist of the {@link SessionPlayer} associated with
* the connected {@link MediaSession}. This value would be updated when
* {@link ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)} or
* {@link ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)} is called.
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat}, this will always return
* {@link SessionPlayer#INVALID_ITEM_INDEX}..
*
* @return the index of next item in playlist, or {@link SessionPlayer#INVALID_ITEM_INDEX}
* if next media item does not exist or playlist hasn't been set
*/
public int getNextMediaItemIndex() {
return isConnected() ? getImpl().getNextMediaItemIndex() : INVALID_ITEM_INDEX;
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* skips to the previous item in the playlist.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)
*/
@NonNull
public ListenableFuture<SessionResult> skipToPreviousPlaylistItem() {
if (isConnected()) {
return getImpl().skipToPreviousItem();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* skips to the next item in the playlist.
* <p>
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)
*/
@NonNull
public ListenableFuture<SessionResult> skipToNextPlaylistItem() {
if (isConnected()) {
return getImpl().skipToNextItem();
}
return createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* skips to the item in the playlist at the index.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @param index The index of the item you want to play in the playlist
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)
*/
@NonNull
public ListenableFuture<SessionResult> skipToPlaylistItem(@IntRange(from = 0) int index) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
if (isConnected()) {
return getImpl().skipToPlaylistItem(index);
}
return createDisconnectedFuture();
}
/**
* Gets the repeat mode of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}. If it is not connected yet, it returns
* {@link SessionPlayer#REPEAT_MODE_NONE}.
*
* @return repeat mode
* @see SessionPlayer#REPEAT_MODE_NONE
* @see SessionPlayer#REPEAT_MODE_ONE
* @see SessionPlayer#REPEAT_MODE_ALL
* @see SessionPlayer#REPEAT_MODE_GROUP
*/
@RepeatMode
public int getRepeatMode() {
return isConnected() ? getImpl().getRepeatMode() : REPEAT_MODE_NONE;
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* sets the repeat mode.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @param repeatMode repeat mode
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see SessionPlayer#REPEAT_MODE_NONE
* @see SessionPlayer#REPEAT_MODE_ONE
* @see SessionPlayer#REPEAT_MODE_ALL
* @see SessionPlayer#REPEAT_MODE_GROUP
*/
@NonNull
public ListenableFuture<SessionResult> setRepeatMode(@RepeatMode int repeatMode) {
if (isConnected()) {
return getImpl().setRepeatMode(repeatMode);
}
return createDisconnectedFuture();
}
/**
* Gets the shuffle mode of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}. If it is not connected yet, it returns
* {@link SessionPlayer#SHUFFLE_MODE_NONE}.
*
* @return the shuffle mode
* @see SessionPlayer#SHUFFLE_MODE_NONE
* @see SessionPlayer#SHUFFLE_MODE_ALL
* @see SessionPlayer#SHUFFLE_MODE_GROUP
*/
@ShuffleMode
public int getShuffleMode() {
return isConnected() ? getImpl().getShuffleMode() : SHUFFLE_MODE_NONE;
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* sets the shuffle mode.
* <p>
* On success, a {@link SessionResult} would be returned with the current media item when the
* command completed.
*
* @param shuffleMode the shuffle mode
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see SessionPlayer#SHUFFLE_MODE_NONE
* @see SessionPlayer#SHUFFLE_MODE_ALL
* @see SessionPlayer#SHUFFLE_MODE_GROUP
*/
@NonNull
public ListenableFuture<SessionResult> setShuffleMode(@ShuffleMode int shuffleMode) {
if (isConnected()) {
return getImpl().setShuffleMode(shuffleMode);
}
return createDisconnectedFuture();
}
/**
* Gets the video size of the {@link SessionPlayer} associated with the connected
* {@link MediaSession}. If it is not connected yet, it returns {@code new VideoSize(0, 0)}.
*
* @return the size of the video. The width and height of size could be 0 if there is no video
* or the size has not been determined yet.
* @see ControllerCallback#onVideoSizeChanged(MediaController, VideoSize)
*/
@NonNull
public VideoSize getVideoSize() {
return isConnected() ? getImpl().getVideoSize() : new VideoSize(0, 0);
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* sets the {@link Surface} to be used as the sink for the video portion of the media.
* <p>
* A null surface will reset any Surface and result in only the audio track being played.
* <p>
* On success, a {@link SessionResult} is returned with the current media item when the command
* completed.
*
* @param surface the {@link Surface} to be used for the video portion of the media
* @return a {@link ListenableFuture} which represents the pending completion of the command
*/
@NonNull
public ListenableFuture<SessionResult> setSurface(@Nullable Surface surface) {
if (isConnected()) {
return getImpl().setSurface(surface);
}
return createDisconnectedFuture();
}
/**
* Gets the full list of selected and unselected tracks that the media contains of the
* {@link SessionPlayer} associated with the connected {@link MediaSession}. The order of
* the list is irrelevant as different players expose tracks in different ways, but the tracks
* will generally be ordered based on track type.
* <p>
* The types of tracks supported may vary based on player implementation.
*
* @return list of tracks. The total number of tracks is the size of the list. If empty,
* an empty list would be returned.
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
*/
@NonNull
public List<TrackInfo> getTracks() {
return isConnected() ? getImpl().getTracks() : Collections.emptyList();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* selects the {@link TrackInfo} for the current media item.
* <p>
* Generally one track will be selected for each track type.
* <p>
* The types of tracks supported may vary based on players.
* <p>
* Note: {@link #getTracks()} returns the list of tracks that can be selected, but the
* list may be invalidated when
* {@link ControllerCallback#onTracksChanged(MediaController, List)} is called.
*
* @param trackInfo track to be selected
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
* @see ControllerCallback#onTrackSelected(MediaController, TrackInfo)
*/
@NonNull
public ListenableFuture<SessionResult> selectTrack(@NonNull TrackInfo trackInfo) {
if (trackInfo == null) {
throw new NullPointerException("TrackInfo shouldn't be null");
}
return isConnected() ? getImpl().selectTrack(trackInfo) : createDisconnectedFuture();
}
/**
* Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession}
* deselects the {@link TrackInfo} for the current media item.
* <p>
* Generally, a track should already be selected in order to be deselected and audio and video
* tracks should not be deselected.
* <p>
* The types of tracks supported may vary based on players.
* <p>
* Note: {@link #getSelectedTrack(int)} returns the currently selected track per track type that
* can be deselected, but the list may be invalidated when
* {@link ControllerCallback#onTracksChanged(MediaController, List)} is called.
*
* @param trackInfo track to be deselected
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
* @see ControllerCallback#onTrackDeselected(MediaController, TrackInfo)
*/
@NonNull
public ListenableFuture<SessionResult> deselectTrack(@NonNull TrackInfo trackInfo) {
if (trackInfo == null) {
throw new NullPointerException("TrackInfo shouldn't be null");
}
return isConnected() ? getImpl().deselectTrack(trackInfo) : createDisconnectedFuture();
}
/**
* Gets the currently selected track for the given track type of the {@link SessionPlayer}
* associated with the connected {@link MediaSession}. If it is not connected yet, it returns
* {@code null}.
* <p>
* The returned value can be outdated after
* {@link ControllerCallback#onTracksChanged(MediaController, List)},
* {@link ControllerCallback#onTrackSelected(MediaController, TrackInfo)},
* or {@link ControllerCallback#onTrackDeselected(MediaController, TrackInfo)} is called.
*
* @param trackType type of selected track
* @return selected track info
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
*/
@Nullable
public TrackInfo getSelectedTrack(@TrackInfo.MediaTrackType int trackType) {
return isConnected() ? getImpl().getSelectedTrack(trackType) : null;
}
/**
* Sets the time diff forcefully when calculating current position.
* @param timeDiff {@code null} for reset
*
*/
@RestrictTo(LIBRARY)
public void setTimeDiff(Long timeDiff) {
mTimeDiff = timeDiff;
}
/**
* Registers an extra {@link ControllerCallback}.
* @param executor a callback executor
* @param callback a ControllerCallback
* @see #unregisterExtraCallback(ControllerCallback)
*
*/
@RestrictTo(LIBRARY_GROUP)
public void registerExtraCallback(@NonNull /*@CallbackExecutor*/ Executor executor,
@NonNull ControllerCallback callback) {
if (executor == null) {
throw new NullPointerException("executor shouldn't be null");
}
if (callback == null) {
throw new NullPointerException("callback shouldn't be null");
}
boolean found = false;
synchronized (mLock) {
for (Pair<ControllerCallback, Executor> pair : mExtraControllerCallbacks) {
if (pair.first == callback) {
found = true;
break;
}
}
if (!found) {
mExtraControllerCallbacks.add(new Pair<>(callback, executor));
}
}
if (found) {
Log.w(TAG, "registerExtraCallback: the callback already exists");
}
}
/**
* Unregisters an {@link ControllerCallback} that has been registered by
* {@link #registerExtraCallback(Executor, ControllerCallback)}.
* The callback passed to {@link Builder#setControllerCallback(Executor, ControllerCallback)}
* can not be unregistered by this method.
* @param callback a ControllerCallback
* @see #registerExtraCallback(Executor, ControllerCallback)
*
*/
@RestrictTo(LIBRARY_GROUP)
public void unregisterExtraCallback(@NonNull ControllerCallback callback) {
if (callback == null) {
throw new NullPointerException("callback shouldn't be null");
}
boolean found = false;
synchronized (mLock) {
for (int i = mExtraControllerCallbacks.size() - 1; i >= 0; i--) {
if (mExtraControllerCallbacks.get(i).first == callback) {
found = true;
mExtraControllerCallbacks.remove(i);
break;
}
}
}
if (!found) {
Log.w(TAG, "unregisterExtraCallback: no such callback found");
}
}
@RestrictTo(LIBRARY)
@NonNull
public List<Pair<ControllerCallback, Executor>> getExtraControllerCallbacks() {
List<Pair<ControllerCallback, Executor>> extraCallbacks;
synchronized (mLock) {
extraCallbacks = new ArrayList<>(mExtraControllerCallbacks);
}
return extraCallbacks;
}
/**
* Gets the cached allowed commands from {@link ControllerCallback#onAllowedCommandsChanged}.
* If it is not connected yet, it returns {@code null}.
*
* @return the allowed commands
*/
@Nullable
public SessionCommandGroup getAllowedCommands() {
if (!isConnected()) {
return null;
}
return getImpl().getAllowedCommands();
}
private static ListenableFuture<SessionResult> createDisconnectedFuture() {
return SessionResult.createFutureWithResult(
SessionResult.RESULT_ERROR_SESSION_DISCONNECTED);
}
void notifyPrimaryControllerCallback(
@NonNull final ControllerCallbackRunnable callbackRunnable) {
if (mPrimaryCallback != null && mPrimaryCallbackExecutor != null) {
mPrimaryCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
callbackRunnable.run(mPrimaryCallback);
}
});
}
}
@RestrictTo(LIBRARY)
public void notifyAllControllerCallbacks(
@NonNull final ControllerCallbackRunnable callbackRunnable) {
notifyPrimaryControllerCallback(callbackRunnable);
for (Pair<ControllerCallback, Executor> pair : getExtraControllerCallbacks()) {
final ControllerCallback callback = pair.first;
final Executor executor = pair.second;
if (callback == null) {
Log.e(TAG, "notifyAllControllerCallbacks: mExtraControllerCallbacks contains a "
+ "null ControllerCallback! Ignoring.");
continue;
}
if (executor == null) {
Log.e(TAG, "notifyAllControllerCallbacks: mExtraControllerCallbacks contains a "
+ "null Executor! Ignoring.");
continue;
}
executor.execute(new Runnable() {
@Override
public void run() {
callbackRunnable.run(callback);
}
});
}
}
@RestrictTo(LIBRARY)
public interface ControllerCallbackRunnable {
/**
* Runs the {@link ControllerCallback}.
*
* @param callback the callback
*/
void run(@NonNull ControllerCallback callback);
}
interface MediaControllerImpl extends Closeable {
@Nullable SessionToken getConnectedToken();
boolean isConnected();
ListenableFuture<SessionResult> play();
ListenableFuture<SessionResult> pause();
ListenableFuture<SessionResult> prepare();
ListenableFuture<SessionResult> fastForward();
ListenableFuture<SessionResult> rewind();
ListenableFuture<SessionResult> seekTo(long pos);
ListenableFuture<SessionResult> skipForward();
ListenableFuture<SessionResult> skipBackward();
ListenableFuture<SessionResult> setVolumeTo(int value, @VolumeFlags int flags);
ListenableFuture<SessionResult> adjustVolume(@VolumeDirection int direction,
@VolumeFlags int flags);
@Nullable
PendingIntent getSessionActivity();
int getPlayerState();
long getDuration();
long getCurrentPosition();
float getPlaybackSpeed();
ListenableFuture<SessionResult> setPlaybackSpeed(float speed);
@SessionPlayer.BuffState
int getBufferingState();
long getBufferedPosition();
@Nullable
PlaybackInfo getPlaybackInfo();
ListenableFuture<SessionResult> setRating(@NonNull String mediaId,
@NonNull Rating rating);
ListenableFuture<SessionResult> sendCustomCommand(@NonNull SessionCommand command,
@Nullable Bundle args);
@Nullable
List<MediaItem> getPlaylist();
ListenableFuture<SessionResult> setPlaylist(@NonNull List<String> list,
@Nullable MediaMetadata metadata);
ListenableFuture<SessionResult> setMediaItem(@NonNull String mediaId);
ListenableFuture<SessionResult> setMediaUri(@NonNull Uri uri, @Nullable Bundle extras);
ListenableFuture<SessionResult> updatePlaylistMetadata(
@Nullable MediaMetadata metadata);
@Nullable MediaMetadata getPlaylistMetadata();
ListenableFuture<SessionResult> addPlaylistItem(int index, @NonNull String mediaId);
ListenableFuture<SessionResult> removePlaylistItem(int index);
ListenableFuture<SessionResult> replacePlaylistItem(int index,
@NonNull String mediaId);
ListenableFuture<SessionResult> movePlaylistItem(int fromIndex, int toIndex);
MediaItem getCurrentMediaItem();
int getCurrentMediaItemIndex();
int getPreviousMediaItemIndex();
int getNextMediaItemIndex();
ListenableFuture<SessionResult> skipToPreviousItem();
ListenableFuture<SessionResult> skipToNextItem();
ListenableFuture<SessionResult> skipToPlaylistItem(int index);
@RepeatMode
int getRepeatMode();
ListenableFuture<SessionResult> setRepeatMode(@RepeatMode int repeatMode);
@ShuffleMode
int getShuffleMode();
ListenableFuture<SessionResult> setShuffleMode(@ShuffleMode int shuffleMode);
@NonNull
VideoSize getVideoSize();
ListenableFuture<SessionResult> setSurface(@Nullable Surface surface);
@NonNull
List<TrackInfo> getTracks();
ListenableFuture<SessionResult> selectTrack(TrackInfo trackInfo);
ListenableFuture<SessionResult> deselectTrack(TrackInfo trackInfo);
@Nullable
TrackInfo getSelectedTrack(@TrackInfo.MediaTrackType int trackType);
@Nullable
SessionCommandGroup getAllowedCommands();
// Internally used methods
@NonNull
Context getContext();
@Nullable
MediaBrowserCompat getBrowserCompat();
}
/**
* Builder for {@link MediaController}.
*
* <p>To set the token of the session for the controller to connect to, one of the {@link
* #setSessionToken(SessionToken)} or {@link #setSessionCompatToken(MediaSessionCompat.Token)}
* should be called. Otherwise, the {@link #build()} will throw an {@link
* IllegalArgumentException}.
*
* <p>Any incoming event from the {@link MediaSession} will be handled on the callback executor.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
public static final class Builder
extends BuilderBase<MediaController, Builder, ControllerCallback> {
public Builder(@NonNull Context context) {
super(context);
}
@Override
@NonNull
public Builder setSessionToken(@NonNull SessionToken token) {
return super.setSessionToken(token);
}
@Override
@NonNull
public Builder setSessionCompatToken(@NonNull MediaSessionCompat.Token compatToken) {
return super.setSessionCompatToken(compatToken);
}
@Override
@NonNull
public Builder setConnectionHints(@NonNull Bundle connectionHints) {
return super.setConnectionHints(connectionHints);
}
@Override
@NonNull
public Builder setControllerCallback(@NonNull Executor executor,
@NonNull ControllerCallback callback) {
return super.setControllerCallback(executor, callback);
}
/**
* Builds a {@link MediaController}.
*
* @throws IllegalArgumentException if both {@link SessionToken} and
* {@link MediaSessionCompat.Token} are not set.
* @return a new controller
*/
@Override
@NonNull
public MediaController build() {
if (mToken == null && mCompatToken == null) {
throw new IllegalArgumentException("token and compat token shouldn't be both null");
}
if (mToken != null) {
return new MediaController(mContext, mToken, mConnectionHints,
mCallbackExecutor, mCallback);
} else {
return new MediaController(mContext, mCompatToken, mConnectionHints,
mCallbackExecutor, mCallback);
}
}
}
/**
* Base builder class for MediaController and its subclass. Any change in this class should be
* also applied to the subclasses {@link MediaController.Builder} and
* {@link MediaBrowser.Builder}.
* <p>
* APIs here should be package private, but should have documentations for developers.
* Otherwise, javadoc will generate documentation with the generic types such as follows.
* <pre>U extends BuilderBase<T, U, C> setControllerCallback(Executor executor,
* C callback)</pre>
* <p>
* This class is hidden to prevent from generating test stub, which fails with
* 'unexpected bound' because it tries to auto generate stub class as follows.
* <pre>abstract static class BuilderBase<
* T extends androidx.media2.MediaController,
* U extends androidx.media2.MediaController.BuilderBase<
* T, U, C extends androidx.media2.MediaController.ControllerCallback>, C></pre>
*/
@RestrictTo(LIBRARY)
abstract static class BuilderBase<T extends MediaController, U extends BuilderBase<T, U, C>,
C extends ControllerCallback> {
final Context mContext;
SessionToken mToken;
MediaSessionCompat.Token mCompatToken;
Bundle mConnectionHints;
Executor mCallbackExecutor;
ControllerCallback mCallback;
/**
* Creates a builder for {@link MediaController}.
*
* @param context context
*/
BuilderBase(@NonNull Context context) {
if (context == null) {
throw new NullPointerException("context shouldn't be null");
}
mContext = context;
}
/**
* Sets the {@link SessionToken} for the controller to connect to.
* <p>
* When this method is called, the {@link MediaSessionCompat.Token} which was set by calling
* {@link #setSessionCompatToken} is removed.
* <p>
* Detailed behavior of the {@link MediaController} differs according to the type of the
* token as follows.
* <p>
* <ol>
* <li>Connected to a {@link SessionToken#TYPE_SESSION} token
* <p>
* The controller connects to the specified session directly. It's recommended when you're
* sure which session to control, or a you've got token directly from the session app.
* <p>
* This can be used only when the session for the token is running. Once the session is
* closed, the token becomes unusable.
* </li>
* <li>Connected to a {@link SessionToken#TYPE_SESSION_SERVICE} or
* {@link SessionToken#TYPE_LIBRARY_SERVICE}
* <p>
* The controller connects to the session provided by the
* {@link MediaSessionService#onGetSession(ControllerInfo)}.
* It's up to the service's decision which session would be returned for the connection.
* Use the {@link #getConnectedSessionToken()} to know the connected session.
* <p>
* This can be used regardless of the session app is running or not. The controller would
* bind to the service while connected to wake up and keep the service process running.
* </li>
* </ol>
*
* @param token token to connect to
* @return the Builder to allow chaining
* @see MediaSessionService#onGetSession(ControllerInfo)
* @see #getConnectedSessionToken()
* @see #setConnectionHints(Bundle)
*/
@NonNull
@SuppressWarnings("unchecked")
U setSessionToken(@NonNull SessionToken token) {
if (token == null) {
throw new NullPointerException("token shouldn't be null");
}
mToken = token;
mCompatToken = null;
return (U) this;
}
/**
* Sets the {@link MediaSessionCompat.Token} for the controller to connect to.
* <p>
* When this method is called, the {@link SessionToken} which was set by calling
* {@link #setSessionToken(SessionToken)} is removed.
*
* @param compatToken token to connect to
* @return the Builder to allow chaining
*/
@NonNull
@SuppressWarnings("unchecked")
U setSessionCompatToken(@NonNull MediaSessionCompat.Token compatToken) {
if (compatToken == null) {
throw new NullPointerException("compatToken shouldn't be null");
}
mCompatToken = compatToken;
mToken = null;
return (U) this;
}
/**
* Sets the connection hints for the controller.
* <p>
* {@code connectionHints} is a session-specific argument to send to the session when
* connecting. The contents of this bundle may affect the connection result.
* <p>
* The hints specified here are only used when when connecting to the {@link MediaSession}.
* They will be ignored when connecting to {@link MediaSessionCompat}.
*
* @param connectionHints a bundle which contains the connection hints
* @return the Builder to allow chaining
* @throws IllegalArgumentException if the bundle contains any non-framework Parcelable
* objects.
*/
@NonNull
@SuppressWarnings("unchecked")
public U setConnectionHints(@NonNull Bundle connectionHints) {
if (connectionHints == null) {
throw new NullPointerException("connectionHints shouldn't be null");
}
if (MediaUtils.doesBundleHaveCustomParcelable(connectionHints)) {
throw new IllegalArgumentException(
"connectionHints shouldn't contain any custom parcelables");
}
mConnectionHints = new Bundle(connectionHints);
return (U) this;
}
/**
* Sets the callback for the controller and its executor.
*
* @param executor callback executor
* @param callback controller callback.
* @return the Builder to allow chaining
*/
@NonNull
@SuppressWarnings("unchecked")
U setControllerCallback(@NonNull Executor executor, @NonNull C callback) {
if (executor == null) {
throw new NullPointerException("executor shouldn't be null");
}
if (callback == null) {
throw new NullPointerException("callback shouldn't be null");
}
mCallbackExecutor = executor;
mCallback = callback;
return (U) this;
}
@NonNull
abstract T build();
}
/**
* Interface for listening to change in activeness of the {@link MediaSession}. It's active if
* and only if it has set a player.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
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 MediaController controller,
@NonNull SessionCommandGroup 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 MediaController controller) {}
/**
* Called when the session set the custom layout through the
* {@link MediaSession#setCustomLayout(MediaSession.ControllerInfo, List)}.
* <p>
* Can be called before {@link #onConnected(MediaController, SessionCommandGroup)}
* is called.
* <p>
* Default implementation returns {@link SessionResult#RESULT_ERROR_NOT_SUPPORTED}.
*
* @param controller the controller for this event
* @param layout
*/
@SessionResult.ResultCode
public int onSetCustomLayout(
@NonNull MediaController controller, @NonNull List<CommandButton> layout) {
return SessionResult.RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when the session has changed anything related with the {@link PlaybackInfo}.
* <p>
* Interoperability: When connected to
* {@link android.support.v4.media.session.MediaSessionCompat}, this may be called when the
* session changes playback info by calling
* {@link android.support.v4.media.session.MediaSessionCompat#setPlaybackToLocal(int)} or
* {@link android.support.v4.media.session.MediaSessionCompat#setPlaybackToRemote(
* VolumeProviderCompat)}}. Specifically:
* <ul>
* <li> Prior to API 21, this will always be called whenever any of those two methods is
* called.
* <li> From API 21 to 22, this is called only when the playback type is changed from local
* to remote (i.e. not from remote to local).
* <li> From API 23, this is called only when the playback type is changed.
* </ul>
*
* @param controller the controller for this event
* @param info new playback info
*/
public void onPlaybackInfoChanged(@NonNull MediaController 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 MediaController controller,
@NonNull SessionCommandGroup commands) {}
/**
* Called when the session sent a custom command. Returns a {@link SessionResult} for
* session to get notification back. If the {@code null} is returned,
* {@link SessionResult#RESULT_ERROR_UNKNOWN} will be returned.
* <p>
* Default implementation returns {@link SessionResult#RESULT_ERROR_NOT_SUPPORTED}.
*
* @param controller the controller for this event
* @param command
* @param args
* @return result of handling custom command
*/
@NonNull
public SessionResult onCustomCommand(@NonNull MediaController controller,
@NonNull SessionCommand command, @Nullable Bundle args) {
return new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED);
}
/**
* Called when the player state is changed.
*
* @param controller the controller for this event
* @param state the new player state
*/
public void onPlayerStateChanged(@NonNull MediaController controller,
@SessionPlayer.PlayerState int state) {}
/**
* Called when playback speed is changed.
*
* @param controller the controller for this event
* @param speed speed
*/
public void onPlaybackSpeedChanged(@NonNull MediaController controller,
float speed) {}
/**
* Called to report buffering events for a media item.
* <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 MediaController controller,
@NonNull MediaItem item, @SessionPlayer.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 MediaController controller, long position) {}
/**
* Called when the current item is changed. It's also called after
* {@link #setPlaylist} or {@link #setMediaItem}.
* Also called when {@link MediaItem#setMetadata(MediaMetadata)} is called on the current
* media item.
* <p>
* When it's called, you should invalidate previous playback information and wait for later
* callbacks. Also, current, previous, and next media item indices may need to be updated.
*
* @param controller the controller for this event
* @param item new current media item
* @see #getPlaylist()
* @see #getPlaylistMetadata()
*/
public void onCurrentMediaItemChanged(@NonNull MediaController controller,
@Nullable MediaItem item) {}
/**
* Called when a playlist is changed. It's also called after {@link #setPlaylist} or
* {@link #setMediaItem}.
* Also called when {@link MediaItem#setMetadata(MediaMetadata)} is called on a media item
* that is contained in the current playlist.
* <p>
* When it's called, current, previous, and next media item indices may need to be updated.
*
* @param controller the controller for this event
* @param list new playlist
* @param metadata new metadata
* @see #getPlaylist()
* @see #getPlaylistMetadata()
*/
public void onPlaylistChanged(@NonNull MediaController controller,
@Nullable List<MediaItem> list, @Nullable MediaMetadata metadata) {}
/**
* Called when a playlist metadata is changed.
*
* @param controller the controller for this event
* @param metadata new metadata
*/
public void onPlaylistMetadataChanged(@NonNull MediaController controller,
@Nullable MediaMetadata metadata) {}
/**
* Called when the shuffle mode is changed.
*
* @param controller the controller for this event
* @param shuffleMode repeat mode
* @see SessionPlayer#SHUFFLE_MODE_NONE
* @see SessionPlayer#SHUFFLE_MODE_ALL
* @see SessionPlayer#SHUFFLE_MODE_GROUP
*/
public void onShuffleModeChanged(@NonNull MediaController controller,
@SessionPlayer.ShuffleMode int shuffleMode) {}
/**
* Called when the repeat mode is changed.
*
* @param controller the controller for this event
* @param repeatMode repeat mode
* @see SessionPlayer#REPEAT_MODE_NONE
* @see SessionPlayer#REPEAT_MODE_ONE
* @see SessionPlayer#REPEAT_MODE_ALL
* @see SessionPlayer#REPEAT_MODE_GROUP
*/
public void onRepeatModeChanged(@NonNull MediaController controller,
@SessionPlayer.RepeatMode int repeatMode) {}
/**
* Called when the playback is completed.
*
* @param controller the controller for this event
*/
public void onPlaybackCompleted(@NonNull MediaController controller) {}
/**
* @deprecated Use {@link #onVideoSizeChanged(MediaController, VideoSize)} instead.
*/
@RestrictTo(LIBRARY)
@Deprecated
public void onVideoSizeChanged(@NonNull MediaController controller, @NonNull MediaItem item,
@NonNull VideoSize videoSize) {}
/**
* Called when video size is changed.
*
* @param controller the controller for this event
* @param videoSize the size of video
*/
public void onVideoSizeChanged(@NonNull MediaController controller,
@NonNull VideoSize videoSize) {}
/**
* Called when the tracks of the current media item is changed such as
* 1) when tracks of a media item become available,
* 2) when new tracks are found during playback, or
* 3) when the current media item is changed.
* <p>
* When it's called, you should invalidate previous track information and use the new
* tracks to call {@link #selectTrack(TrackInfo)} or
* {@link #deselectTrack(TrackInfo)}.
* <p>
* The types of tracks supported may vary based on player implementation.
*
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
*
* @param controller the controller for this event
* @param tracks the list of tracks. It can be empty.
*/
public void onTracksChanged(@NonNull MediaController controller,
@NonNull List<TrackInfo> tracks) {}
/**
* Called when a track is selected.
* <p>
* The types of tracks supported may vary based on player implementation, but generally
* one track will be selected for each track type.
*
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
*
* @param controller the controller for this event
* @param trackInfo the selected track
*/
public void onTrackSelected(@NonNull MediaController controller,
@NonNull TrackInfo trackInfo) {}
/**
* Called when a track is deselected.
* <p>
* The types of tracks supported may vary based on player implementation, but generally
* a track should already be selected in order to be deselected and audio and video tracks
* should not be deselected.
*
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
*
* @param controller the controller for this event
* @param trackInfo the deselected track
*/
public void onTrackDeselected(@NonNull MediaController controller,
@NonNull TrackInfo trackInfo) {}
/**
* Called when the subtitle track has new subtitle data available.
* @param controller the controller for this event
* @param item the MediaItem of this media item
* @param track the track that has the subtitle data
* @param data the subtitle data
*/
public void onSubtitleData(@NonNull MediaController controller, @NonNull MediaItem item,
@NonNull TrackInfo track, @NonNull SubtitleData data) {}
}
/**
* Holds information about the way volume is handled for this session.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
// The same as MediaController.PlaybackInfo
@Deprecated
@VersionedParcelize
public static final class PlaybackInfo implements VersionedParcelable {
@ParcelField(1)
int mPlaybackType;
@ParcelField(2)
int mControlType;
@ParcelField(3)
int mMaxVolume;
@ParcelField(4)
int mCurrentVolume;
@ParcelField(5)
AudioAttributesCompat mAudioAttrsCompat;
// WARNING: Adding a new ParcelField may break old library users (b/152830728)
/**
* 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;
/**
* Used for VersionedParcelable
*/
PlaybackInfo() {
}
PlaybackInfo(int playbackType, AudioAttributesCompat attrs, int controlType, int max,
int current) {
mPlaybackType = playbackType;
mAudioAttrsCompat = attrs;
mControlType = controlType;
mMaxVolume = max;
mCurrentVolume = current;
}
/**
* Gets 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;
}
/**
* Gets 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
*/
@Nullable
public AudioAttributesCompat getAudioAttributes() {
return mAudioAttrsCompat;
}
/**
* Gets 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;
}
/**
* Gets the maximum volume that may be set for this session.
* <p>
* This is only meaningful when the playback type is {@link #PLAYBACK_TYPE_REMOTE}.
*
* @return the maximum allowed volume where this session is playing
*/
public int getMaxVolume() {
return mMaxVolume;
}
/**
* Gets the current volume for this session.
* <p>
* This is only meaningful when the playback type is {@link #PLAYBACK_TYPE_REMOTE}.
*
* @return the current volume where this session is playing
*/
public int getCurrentVolume() {
return mCurrentVolume;
}
@Override
public int hashCode() {
return ObjectsCompat.hash(
mPlaybackType, mControlType, mMaxVolume, mCurrentVolume, mAudioAttrsCompat);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof PlaybackInfo)) {
return false;
}
PlaybackInfo other = (PlaybackInfo) obj;
return mPlaybackType == other.mPlaybackType
&& mControlType == other.mControlType
&& mMaxVolume == other.mMaxVolume
&& mCurrentVolume == other.mCurrentVolume
&& ObjectsCompat.equals(mAudioAttrsCompat, other.mAudioAttrsCompat);
}
static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributesCompat attrs,
int controlType, int max, int current) {
return new PlaybackInfo(playbackType, attrs, controlType, max, current);
}
}
}