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