/* * 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. * *
MediaController objects are thread-safe. * *
Topics covered here: * *
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. * *
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. * *
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. * *
When the connected session is closed, the controller will receive {@link * ControllerCallback#onDisconnected(MediaController)}. * *
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. * *
* *
{@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(); * }* * 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. * *
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. * *
The app targeting API level 30 or higher must include a {@code
* 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.
*
* 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.
*
* 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
* 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
* 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}.
*
* Playback can be started without this. But this provides finer grained control of playback
* start. See {@link #play} for details.
*
* 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
* 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
* 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
* 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
* 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
* 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.
*
* 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
* 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}.
*
* 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
* The command will be ignored if the session does not support
* {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
* {@link 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 {@link AudioManager}.
*
* 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
* 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.
*
* 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.
*
* 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.
*
* 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
* 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}.
*
* 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
* 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}.
*
* A command is not accepted if it is not a custom command.
*
* @param command custom command
* @param args optional argument
*/
@NonNull
public ListenableFuture
* 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
* All media IDs in the list shouldn't be an empty string.
*
* 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}.
*
* 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
* 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}.
*
* The {@link ControllerCallback#onPlaylistChanged} and/or
* {@link ControllerCallback#onCurrentMediaItemChanged} would be called when it's completed.
*
* 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
* 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}.
*
* The {@link ControllerCallback#onPlaylistChanged} and/or
* {@link ControllerCallback#onCurrentMediaItemChanged} would be called when it's completed.
*
* On success, a {@link SessionResult} would be returned with {@code item} set.
*
* 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:
*
* 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
* 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
* 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 {@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
* 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
* 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
* 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
* 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.
*
* 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.
*
* 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
*
* 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
* 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
* 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
* 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
* A null surface will reset any Surface and result in only the audio track being played.
*
* 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
* 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
* Generally one track will be selected for each track type.
*
* The types of tracks supported may vary based on players.
*
* 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
* 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: {@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
* 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 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}.
*
* Any incoming event from the {@link MediaSession} will be handled on the callback executor.
*
* @deprecated androidx.media2 is deprecated. Please migrate to androidx.media3.
*/
@Deprecated
public static final class Builder
extends BuilderBase
* APIs here should be package private, but should have documentations for developers.
* Otherwise, javadoc will generate documentation with the generic types such as follows.
*
* 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.
*
* When this method is called, the {@link MediaSessionCompat.Token} which was set by calling
* {@link #setSessionCompatToken} is removed.
*
* Detailed behavior of the {@link MediaController} differs according to the type of the
* token as follows.
*
*
* 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.
*
* This can be used only when the session for the token is running. Once the session is
* closed, the token becomes unusable.
*
* 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.
*
* 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.
*
* 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.
*
* {@code connectionHints} is a session-specific argument to send to the session when
* connecting. The contents of this bundle may affect the connection result.
*
* 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 androidx.media3.
*/
@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.
*
* 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)}.
*
* Can be called before {@link #onConnected(MediaController, SessionCommandGroup)}
* is called.
*
* 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
* 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:
*
* 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.
*
* 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.
*
* 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.
*
* 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
* When it's called, you should invalidate previous track information and use the new
* tracks to call {@link #selectTrack(TrackInfo)} or
* {@link #deselectTrack(TrackInfo)}.
*
* 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
* 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.
*
* 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 androidx.media3.
*/
// 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:
*
* 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.
*
* 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);
}
}
}
{@code
*
*
*
* @see MediaSession
* @see MediaSessionService
* @deprecated androidx.media2 is deprecated. Please migrate to androidx.media3.
*/
@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
*
*
* Uri patterns Following API calls Method
*
* {@code androidx://media2-session/setMediaUri?uri=[uri]}
* {@link #prepare}
* {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
*
* {@link #play}
* {@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
*
* {@code androidx://media2-session/setMediaUri?id=[mediaId]}
* {@link #prepare}
* {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
*
* {@link #play}
* {@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
*
* {@code androidx://media2-session/setMediaUri?query=[query]}
* {@link #prepare}
* {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
*
* {@link #play}
* {@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
*
* Does not match with any pattern above
* {@link #prepare}
* {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
*
* {@link #play}
* {@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* U extends BuilderBase
* abstract static class BuilderBase<
* T extends androidx.media2.MediaController,
* U extends androidx.media2.MediaController.BuilderBase<
* T, U, C extends androidx.media2.MediaController.ControllerCallback>, C>
*/
@RestrictTo(LIBRARY)
abstract static class BuilderBase
*
*
* @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.
*
*
*
* @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.
*
*
*
* @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:
*
*
*
* @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.
*