/* * 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.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: *

    *
  1. Controller Lifecycle *
  2. Controlling the {@link MediaSession} in the same * process *
*

Controller Lifecycle

*

* 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. *

* *

Controlling the MediaSession in the same process

* 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. *

*

 * {@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. * * @see MediaSession * @see MediaSessionService */ public class MediaController implements Closeable { private static final String TAG = "MediaController"; /** * @hide */ @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 {} /** * @hide */ @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> 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}. *

* 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. * * @return a {@link ListenableFuture} representing the pending completion of the command * @see #prepare */ @NonNull public ListenableFuture play() { if (isConnected()) { return getImpl().play(); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * pauses playback. *

* 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 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}. *

* 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. * * @return a {@link ListenableFuture} representing the pending completion of the command * @see #play */ @NonNull public ListenableFuture prepare() { if (isConnected()) { return getImpl().prepare(); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link 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 {@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 fastForward() { if (isConnected()) { return getImpl().fastForward(); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link 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 {@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 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. *

* 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 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. *

* 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 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. *

* 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 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}. *

* 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 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}. *

* 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 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}. *

* 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 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}. *

* 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 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 *

* 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 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}. *

* 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 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. *

* 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 setPlaylist(@NonNull List 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. *

* 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 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. *

* 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. * * @param uri the Uri of the item to play * @see #setMediaItem * @see #setPlaylist * @see ControllerCallback#onCurrentMediaItemChanged * @see ControllerCallback#onPlaylistChanged */ @NonNull public ListenableFuture 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

*

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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. *

* 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 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}. *

* 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 * * @hide */ @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) * * @hide */ @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 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) * * @hide */ @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"); } } /** @hide */ @RestrictTo(LIBRARY) @NonNull public List> getExtraControllerCallbacks() { List> 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 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); } }); } } /** @hide */ @RestrictTo(LIBRARY) public void notifyAllControllerCallbacks( @NonNull final ControllerCallbackRunnable callbackRunnable) { notifyPrimaryControllerCallback(callbackRunnable); for (Pair 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); } }); } } /** @hide */ @RestrictTo(LIBRARY) public interface ControllerCallbackRunnable { void run(@NonNull ControllerCallback callback); } interface MediaControllerImpl extends Closeable { @Nullable SessionToken getConnectedToken(); boolean isConnected(); ListenableFuture play(); ListenableFuture pause(); ListenableFuture prepare(); ListenableFuture fastForward(); ListenableFuture rewind(); ListenableFuture seekTo(long pos); ListenableFuture skipForward(); ListenableFuture skipBackward(); ListenableFuture setVolumeTo(int value, @VolumeFlags int flags); ListenableFuture adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags); @Nullable PendingIntent getSessionActivity(); int getPlayerState(); long getDuration(); long getCurrentPosition(); float getPlaybackSpeed(); ListenableFuture setPlaybackSpeed(float speed); @SessionPlayer.BuffState int getBufferingState(); long getBufferedPosition(); @Nullable PlaybackInfo getPlaybackInfo(); ListenableFuture setRating(@NonNull String mediaId, @NonNull Rating rating); ListenableFuture sendCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args); @Nullable List getPlaylist(); ListenableFuture setPlaylist(@NonNull List list, @Nullable MediaMetadata metadata); ListenableFuture setMediaItem(@NonNull String mediaId); ListenableFuture setMediaUri(@NonNull Uri uri, @Nullable Bundle extras); ListenableFuture updatePlaylistMetadata( @Nullable MediaMetadata metadata); @Nullable MediaMetadata getPlaylistMetadata(); ListenableFuture addPlaylistItem(int index, @NonNull String mediaId); ListenableFuture removePlaylistItem(int index); ListenableFuture replacePlaylistItem(int index, @NonNull String mediaId); ListenableFuture movePlaylistItem(int fromIndex, int toIndex); MediaItem getCurrentMediaItem(); int getCurrentMediaItemIndex(); int getPreviousMediaItemIndex(); int getNextMediaItemIndex(); ListenableFuture skipToPreviousItem(); ListenableFuture skipToNextItem(); ListenableFuture skipToPlaylistItem(int index); @RepeatMode int getRepeatMode(); ListenableFuture setRepeatMode(@RepeatMode int repeatMode); @ShuffleMode int getShuffleMode(); ListenableFuture setShuffleMode(@ShuffleMode int shuffleMode); @NonNull VideoSize getVideoSize(); ListenableFuture setSurface(@Nullable Surface surface); @NonNull List getTracks(); ListenableFuture selectTrack(TrackInfo trackInfo); ListenableFuture 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}. *

* 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. */ public static final class Builder extends BuilderBase { 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}. *

* APIs here should be package private, but should have documentations for developers. * Otherwise, javadoc will generate documentation with the generic types such as follows. *

U extends BuilderBase setControllerCallback(Executor executor,
     * C callback)
*

* 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. *

abstract static class BuilderBase<
     *      T extends androidx.media2.MediaController,
     *      U extends androidx.media2.MediaController.BuilderBase<
     *              T, U, C extends androidx.media2.MediaController.ControllerCallback>, C>
* @hide */ @RestrictTo(LIBRARY) abstract static class BuilderBase, 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. *

* 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. *

*

    *
  1. Connected to a {@link SessionToken#TYPE_SESSION} token *

    * 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. *

  2. *
  3. Connected to a {@link SessionToken#TYPE_SESSION_SERVICE} or * {@link SessionToken#TYPE_LIBRARY_SERVICE} *

    * 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. *

  4. *
* * @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. *

* 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. */ 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 layout) { return SessionResult.RESULT_ERROR_NOT_SUPPORTED; } /** * Called when the session has changed anything related with the {@link PlaybackInfo}. *

* 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: *

* * @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. *

* 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 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. * @hide */ @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. *

* 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 tracks) {} /** * Called when a track is selected. *

* 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. */ // The same as MediaController.PlaybackInfo @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: *

* * @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. *

* 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); } } }