/* * 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.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.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 AutoCloseable { 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) -> { synchronized (mLock) { if (!mClosed) { mImpl = createImpl(context, sessionToken, connectionHints); } else { 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. *

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

* This would transfer the player state from {@link SessionPlayer#PLAYER_STATE_PLAYING} to * {@link SessionPlayer#PLAYER_STATE_PAUSED}. */ @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. *

* This would transfer the player state from {@link SessionPlayer#PLAYER_STATE_IDLE} to * {@link SessionPlayer#PLAYER_STATE_PAUSED}. *

* Playback can be started without this. But this provides finer grained control of playback * start. See {@link #play} for details. * * @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. * * @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. * * @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. * * * @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. * * @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} * moves to a new location in the media stream. * * @param pos position to move to, in milliseconds */ @NonNull public ListenableFuture seekTo(long pos) { if (isConnected()) { return getImpl().seekTo(pos); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * starts playback for a specific media id. * * @param mediaId the non-empty media id * @param extras optional extras that can include extra information about the media item * to be played * @hide */ @RestrictTo(LIBRARY) @NonNull public ListenableFuture playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) { if (TextUtils.isEmpty(mediaId)) { throw new IllegalArgumentException("mediaId shouldn't be empty"); } if (isConnected()) { return getImpl().playFromMediaId(mediaId, extras); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * starts playback for a specific search query. * * @param query the non-empty search query * @param extras optional extras that can include extra information about the query * @hide */ @RestrictTo(LIBRARY) @NonNull public ListenableFuture playFromSearch(@NonNull String query, @Nullable Bundle extras) { if (TextUtils.isEmpty(query)) { throw new IllegalArgumentException("query shouldn't be empty"); } if (isConnected()) { return getImpl().playFromSearch(query, extras); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * starts playback for a specific {@link Uri}. * * @param uri the URI of the requested media * @param extras optional extras that can include extra information about the media item * to be played * @hide */ @RestrictTo(LIBRARY) @NonNull public ListenableFuture playFromUri(@NonNull Uri uri, @Nullable Bundle extras) { if (uri == null) { throw new NullPointerException("uri shouldn't be null"); } if (isConnected()) { return getImpl().playFromUri(uri, extras); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * prepares a media item with the media id for playback. * In other words, other sessions can continue to play during the preparation of this session. * This method can be used to speed up the start of the playback. * Once the prepare is done, the session will change its playback state to * {@link SessionPlayer#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called to start * playback. If the prepare is not needed, {@link #playFromMediaId} can be directly called * without this method. * * @param mediaId the non-empty media id * @param extras optional extras that can include extra information about the media item * to be prepared * @hide */ @RestrictTo(LIBRARY) @NonNull public ListenableFuture prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) { if (TextUtils.isEmpty(mediaId)) { throw new IllegalArgumentException("mediaId shouldn't be empty"); } if (isConnected()) { return getImpl().prepareFromMediaId(mediaId, extras); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * prepares a media item with the specific search query for playback. * In other words, other sessions can continue to play during the preparation of this session. * This method can be used to speed up the start of the playback. * Once the prepare is done, the session will change its playback state to * {@link SessionPlayer#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called to start * playback. If the prepare is not needed, {@link #playFromSearch} can be directly called * without this method. * * @param query the non-empty search query * @param extras optional extras that can include extra information about the query * @hide */ @RestrictTo(LIBRARY) @NonNull public ListenableFuture prepareFromSearch(@NonNull String query, @Nullable Bundle extras) { if (TextUtils.isEmpty(query)) { throw new IllegalArgumentException("query shouldn't be empty"); } if (isConnected()) { return getImpl().prepareFromSearch(query, extras); } return createDisconnectedFuture(); } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * prepares a media item with the specific {@link Uri} for playback. * In other words, other sessions can continue to play during the preparation of this session. * This method can be used to speed up the start of the playback. * Once the prepare is done, the session will change its playback state to * {@link SessionPlayer#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called to start * playback. If the prepare is not needed, {@link #playFromUri} can be directly called * without this method. * * @param uri the URI of the requested media * @param extras optional extras that can include extra information about the media item * to be prepared * @hide */ @RestrictTo(LIBRARY) @NonNull public ListenableFuture prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) { if (uri == null) { throw new NullPointerException("uri shouldn't be null"); } if (isConnected()) { return getImpl().prepareFromUri(uri, extras); } 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. * * @see #getPlaybackInfo() * @param value the value to set it to, between 0 and the reported max * @param flags flags from {@link AudioManager} to include with the volume request for local * playback */ @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. * * @see #getPlaybackInfo() * @param direction The direction to adjust the volume in. * @param flags flags from {@link AudioManager} to include with the volume request for local * playback */ @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 lastly cached player state from * {@link ControllerCallback#onPlayerStateChanged(MediaController, int)}. * If it is not connected yet, it returns {@link SessionPlayer#PLAYER_STATE_IDLE}. * * @return player state */ 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. * * @return the duration in ms, or {@link SessionPlayer#UNKNOWN_TIME} */ public long getDuration() { return isConnected() ? getImpl().getDuration() : UNKNOWN_TIME; } /** * Gets the current playback position. *

* This returns the calculated value of the position, based on the difference between the * update time and current time. * * @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 lastly cached playback speed from * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController, float)}. * * @return speed the lastly cached 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. A value of {@code 1.0f} is the default playback value, and a * negative value indicates reverse playback. {@code 0.0f} is not allowed. * * @throws IllegalArgumentException if the {@code speed} is equal to zero. */ @NonNull public ListenableFuture setPlaybackSpeed(float speed) { if (speed == 0.0f) { throw new IllegalArgumentException("speed must not be zero"); } if (isConnected()) { return getImpl().setPlaybackSpeed(speed); } return createDisconnectedFuture(); } /** * Gets the current buffering state of the {@link SessionPlayer} associated with the connected * {@link MediaSession}. * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already * buffered. * * @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 lastly cached buffered position from the session when * {@link ControllerCallback#onBufferingStateChanged(MediaController, MediaItem, int)} is * called. * * @return buffering position in millis, 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(); } /** * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}. 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. Use media items returned here for other playlist agent APIs such as * {@link SessionPlayer#skipToPlaylistItem}. * * @return playlist, or {@code null} if the playlist hasn't been set, controller isn't * connected, or it doesn't have enough permission * @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 or * {@link #setMediaItem} to specify which items to play. *

* All media IDs in the list shouldn't be an empty string. *

* The {@link ControllerCallback#onPlaylistChanged} and * {@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 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 with the media ID. Use this 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. *

* The {@link ControllerCallback#onPlaylistChanged} and * {@link ControllerCallback#onCurrentMediaItemChanged} would be called when it's completed. * The current item would be the item given here. * * @param mediaId the non-empty media id of the item to play * @see #setPlaylist * @see ControllerCallback#onCurrentMediaItemChanged * @see ControllerCallback#onPlaylistChanged * @see MediaMetadata#METADATA_KEY_MEDIA_ID */ @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 {@link SessionPlayer} associated with the connected {@link MediaSession} * updates the playlist metadata * * @param metadata metadata of the playlist */ @NonNull public ListenableFuture updatePlaylistMetadata( @Nullable MediaMetadata metadata) { if (isConnected()) { return getImpl().updatePlaylistMetadata(metadata); } return createDisconnectedFuture(); } /** * Gets the lastly cached playlist metadata either from * {@link ControllerCallback#onPlaylistMetadataChanged} or * {@link ControllerCallback#onPlaylistChanged}. * * @return metadata of the playlist, or null if none is set or the controller is not * connected */ @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. *

* This will not change the currently playing media item. * If index is less than or equal to the current index of the playlist, * the current index of the playlist will be incremented correspondingly. * * @param index the index you want to add * @param mediaId the non-empty media id of the new item * @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. *

* If the item is the currently playing item of the playlist, current playback * will be stopped and playback moves to next source in the list. * * @param index the media item you want to add */ @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. * * @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 */ @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(); } /** * Gets the lastly cached current item from * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)}. * * @return the currently playing item, or null if unknown or not connected * @see #setMediaItem * @see #setPlaylist */ @Nullable public MediaItem getCurrentMediaItem() { return isConnected() ? getImpl().getCurrentMediaItem() : null; } /** * Gets the current item index in the playlist. The returned value can be outdated after * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController, MediaItem)} or * {@link ControllerCallback#onPlaylistChanged(MediaController, List, MediaMetadata)} is called. * * @return the index of current item in playlist, or -1 if current media item does not exist or * playlist hasn't been set. */ public int getCurrentMediaItemIndex() { return isConnected() ? getImpl().getCurrentMediaItemIndex() : -1; } /** * Gets the previous item index in the playlist. The returned value can be outdated after * {@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 * {@code -1}. * * @return the index of previous item in playlist, or -1 if previous media item does not exist * or playlist hasn't been set. */ public int getPreviousMediaItemIndex() { return isConnected() ? getImpl().getPreviousMediaItemIndex() : -1; } /** * Gets the next item index in the playlist. The returned value can be outdated after * {@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 * {@code -1}. * * @return the index of next item in playlist, or -1 if next media item does not exist or * playlist hasn't been set. */ public int getNextMediaItemIndex() { return isConnected() ? getImpl().getNextMediaItemIndex() : -1; } /** * Requests that the {@link SessionPlayer} associated with the connected {@link MediaSession} * skips to the previous item in the playlist. *

* This calls {@link SessionPlayer#skipToPreviousPlaylistItem()}. */ @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. *

* This calls {@link SessionPlayer#skipToNextPlaylistItem()}. */ @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. *

* This calls {@link SessionPlayer#skipToPlaylistItem(int)}. * * @param index The index of the item you want to play in the playlist */ @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 cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}. * 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. * * @param repeatMode repeat mode * @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 cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}. * 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. * * @param shuffleMode The shuffle mode * @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 cached video size from the {@link ControllerCallback#onVideoSizeChanged}. * If it is not connected yet, it returns {@code new VideoSize(0, 0)}. * * @return The video size * * @hide */ @RestrictTo(LIBRARY_GROUP) @NonNull public VideoSize getVideoSize() { return isConnected() ? getImpl().getVideoSize() : new VideoSize(0, 0); } /** * Sets the {@link Surface} to be used as the sink for the video portion of the media. *

* This makes the currently connected {@link MediaSession} instance call * {@link SessionPlayer#setSurface(Surface)} on the {@link SessionPlayer} * instance that is associated with it via {@link MediaSession#updatePlayer(SessionPlayer)} * * @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. * * @hide */ @RestrictTo(LIBRARY_GROUP) @NonNull public ListenableFuture setSurface(@Nullable Surface surface) { if (isConnected()) { return getImpl().setSurface(surface); } return createDisconnectedFuture(); } /** * Gets the cached track info list from the * {@link ControllerCallback#onTrackInfoChanged(MediaController, List)}. * The types of tracks supported may vary based on player implementation. * If it is not connected yet, it returns null. * * @return List of tracks. The total number of tracks is the size of the list. If empty, * the implementation should return a empty list instead of {@code null}. * * @hide */ @RestrictTo(LIBRARY_GROUP) @NonNull public List getTrackInfo() { return isConnected() ? getImpl().getTrackInfo() : Collections.emptyList(); } /** * Selects the {@link TrackInfo} for the current media item. * The types of tracks supported may vary based on player implementation. * * @param trackInfo track to be selected. * * @hide */ @RestrictTo(LIBRARY_GROUP) @NonNull public ListenableFuture selectTrack(@NonNull TrackInfo trackInfo) { if (trackInfo == null) { throw new NullPointerException("TrackInfo shouldn't be null"); } return isConnected() ? getImpl().selectTrack(trackInfo) : createDisconnectedFuture(); } /** * Deselects the {@link TrackInfo} for the current media item. * The types of tracks supported may vary based on player implementation. * * @param trackInfo track to be deselected. * * @hide */ @RestrictTo(LIBRARY_GROUP) @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. The return value is an element in * the list returned by {@link #getTrackInfo()} and supported track types may vary based on the * player implementation. * * The returned value can be outdated after * {@link ControllerCallback#onTrackInfoChanged(MediaController, List)}, * {@link ControllerCallback#onTrackSelected(MediaController, TrackInfo)}, * or {@link ControllerCallback#onTrackDeselected(MediaController, TrackInfo)} is called. * * If it is not connected yet, it returns null. * * @param trackType type of selected track * @return selected track info * * @hide */ @RestrictTo(LIBRARY_GROUP) @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 * * @hide */ @RestrictTo(LIBRARY_GROUP) @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 AutoCloseable { @Nullable SessionToken getConnectedToken(); boolean isConnected(); ListenableFuture play(); ListenableFuture pause(); ListenableFuture prepare(); ListenableFuture fastForward(); ListenableFuture rewind(); ListenableFuture seekTo(long pos); ListenableFuture skipForward(); ListenableFuture skipBackward(); ListenableFuture playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras); ListenableFuture playFromSearch(@NonNull String query, @Nullable Bundle extras); ListenableFuture playFromUri(@NonNull Uri uri, @Nullable Bundle extras); ListenableFuture prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras); ListenableFuture prepareFromSearch(@NonNull String query, @Nullable Bundle extras); ListenableFuture prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras); 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 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); 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 getTrackInfo(); 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 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") public 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") public 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") public 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) {} /** * Called when video size is changed. * * @param controller the controller for this event * @param item the media item for which the video size changed * @param videoSize the size of video * * @hide */ @RestrictTo(LIBRARY_GROUP) public void onVideoSizeChanged(@NonNull MediaController controller, @NonNull MediaItem item, @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 trackInfos the list of track infos * * @hide */ @RestrictTo(LIBRARY_GROUP) public void onTrackInfoChanged(@NonNull MediaController controller, @NonNull List trackInfos) {} /** * 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 * @hide */ @RestrictTo(LIBRARY_GROUP) 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 * @hide */ @RestrictTo(LIBRARY_GROUP) 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 * @hide */ @RestrictTo(LIBRARY_GROUP) 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; /** * 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); } } }