/* * 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.media3.session; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotEmpty; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.postOrRun; import android.app.PendingIntent; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroupArray; import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; import org.checkerframework.checker.initialization.qual.Initialized; /** * A controller that interacts with a {@link MediaSession}, a {@link MediaSessionService} hosting a * {@link MediaSession}, or a {@link MediaLibraryService} hosting a {@link * MediaLibraryService.MediaLibrarySession}. The {@link MediaSession} typically resides in a remote * process like another app but may be in the same process as this controller. It implements {@link * Player} and the player commands are sent to the underlying {@link Player} of the connected {@link * MediaSession}. It also has session-specific commands that can be handled by {@link * MediaSession.SessionCallback}. * *

Topics covered here: * *

    *
  1. Controller Lifecycle *
  2. Threading Model *
  3. Package Visibility Filter *
* *

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 you're done, use {@link #releaseFuture(Future)} or {@link #release()} to clean up * resources. This also helps session service to be destroyed when there's no controller associated * with it. * *

Threading Model

* *

Methods of this class should be called from the application thread associated with the {@link * #getApplicationLooper() application looper}. Otherwise, {@link IllegalStateException} will be * thrown. Also, the methods of {@link Player.Listener} and {@link Listener} will be called from the * application thread. * *

Package Visibility Filter

* *

The app targeting API level 30 or higher must include a {@code } element in their * manifest to connect to a service component of another app like {@link MediaSessionService}, * {@link MediaLibraryService}, or {@link androidx.media.MediaBrowserServiceCompat}). See the * following example and this * guide for more information. * *

{@code
 * 
 * 
 *   
 * 
 * 
 *   
 * 
 * 
 *   
 * 
 * 
 * 
 * }
*/ public class MediaController implements Player { private static final String TAG = "MediaController"; private static final String WRONG_THREAD_ERROR_MESSAGE = "MediaController method is called from a wrong thread." + " See javadoc of MediaController for details."; /** A builder for {@link MediaController}. */ public static final class Builder { private final Context context; private final SessionToken token; private Bundle connectionHints; private Listener listener; private Looper applicationLooper; /** * Creates a builder for {@link MediaController}. * *

The detailed behavior of the {@link MediaController} differs depending on the type of the * token as follows. * *

    *
  1. {@link SessionToken#TYPE_SESSION}: The controller connects to the specified session * directly. It's recommended when you're sure which session to control, or you've got a * 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. {@link SessionToken#TYPE_SESSION_SERVICE} or {@link SessionToken#TYPE_LIBRARY_SERVICE}: * The controller connects to the session provided by the {@link * MediaSessionService#onGetSession(MediaSession.ControllerInfo)} or {@link * MediaLibraryService#onGetSession(MediaSession.ControllerInfo)}. It's up to the service * to decide which session should be returned for the connection. Use the {@link * #getConnectedToken()} to know the connected session. This can be used regardless of * whether the session app is running or not. The controller will bind to the service as * long as it's connected to wake up and keep the service process running. *
* * @param context The context. * @param token The token to connect to. */ public Builder(Context context, SessionToken token) { this.context = checkNotNull(context); this.token = checkNotNull(token); connectionHints = Bundle.EMPTY; listener = new Listener() {}; applicationLooper = Util.getCurrentOrMainLooper(); } /** * Sets connection hints for the controller. * *

The hints are session-specific arguments sent to the session when connecting. The contents * of this bundle may affect the connection result. * *

The hints are only used when connecting to the {@link MediaSession}. They will be ignored * when connecting to {@link MediaSessionCompat}. * * @param connectionHints A bundle containing the connection hints. * @return The builder to allow chaining. */ public Builder setConnectionHints(Bundle connectionHints) { this.connectionHints = new Bundle(checkNotNull(connectionHints)); return this; } /** * Sets a listener for the controller. * * @param listener The listener. * @return The builder to allow chaining. */ public Builder setListener(Listener listener) { this.listener = checkNotNull(listener); return this; } /** * Sets a {@link Looper} that must be used for all calls to the {@link Player} methods and that * is used to call {@link Player.Listener} methods on. The {@link Looper#myLooper()} current * looper} at that time this builder is created will be used if not specified. The {@link * Looper#getMainLooper() main looper} will be used if the current looper doesn't exist. * * @param looper The looper. * @return The builder to allow chaining. */ public Builder setApplicationLooper(Looper looper) { applicationLooper = checkNotNull(looper); return this; } /** * Builds a {@link MediaController} asynchronously. * *

The controller instance can be obtained like the following example: * *

{@code
     * MediaController.Builder builder = ...;
     * ListenableFuture future = builder.buildAsync();
     * future.addListener(() -> {
     *   try {
     *     MediaController controller = future.get();
     *     // The session accepted the connection.
     *   } catch (ExecutionException e) {
     *     if (e.getCause() instanceof SecurityException) {
     *       // The session rejected the connection.
     *     }
     *   }
     * }, ContextCompat.getMainExecutor());
     * }
* *

The future must be kept by callers until the future is complete to get the controller * instance. Otherwise, the future might be garbage collected and the listener added by {@link * ListenableFuture#addListener(Runnable, Executor)} would never be called. * * @return A future of the controller instance. */ public ListenableFuture buildAsync() { MediaControllerHolder holder = new MediaControllerHolder<>(applicationLooper); MediaController controller = new MediaController(context, token, connectionHints, listener, applicationLooper, holder); postOrRun(new Handler(applicationLooper), () -> holder.setController(controller)); return holder; } } /** * A listener for events and incoming commands from {@link MediaSession}. * *

The methods will be called from the application thread associated with the {@link * #getApplicationLooper() application looper} of the controller. */ public interface Listener { /** * Called when the controller is disconnected from the session. The controller becomes * unavailable afterwards and this listener won't be called anymore. * *

It will be also called after the {@link #release()}, so you can put clean up code here. * You don't need to call {@link #release()} after this. * * @param controller The controller. */ default void onDisconnected(MediaController controller) {} /** * Called when the session sets the custom layout through {@link MediaSession#setCustomLayout}. * *

Return a {@link ListenableFuture} to reply with a {@link SessionResult} to the session * asynchronously. You can also return a {@link SessionResult} directly by using Guava's {@link * Futures#immediateFuture(Object)}. * *

The default implementation returns a {@link ListenableFuture} of {@link * SessionResult#RESULT_ERROR_NOT_SUPPORTED}. * * @param controller The controller. * @param layout The ordered list of {@link CommandButton}. * @return The result of handling the custom layout. */ default ListenableFuture onSetCustomLayout( MediaController controller, List layout) { return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)); } /** * Called when the available session commands are changed by session. * * @param controller The controller. * @param commands The new available session commands. */ default void onAvailableSessionCommandsChanged( MediaController controller, SessionCommands commands) {} /** * Called when the session sends a custom command through {@link * MediaSession#sendCustomCommand}. * *

Return a {@link ListenableFuture} to reply with a {@link SessionResult} to the session * asynchronously. You can also return a {@link SessionResult} directly by using Guava's {@link * Futures#immediateFuture(Object)}. * *

The default implementation returns {@link ListenableFuture} of {@link * SessionResult#RESULT_ERROR_NOT_SUPPORTED}. * * @param controller The controller. * @param command The custom command. * @param args The additional arguments. May be empty. * @return The result of handling the custom command. */ default ListenableFuture onCustomCommand( MediaController controller, SessionCommand command, Bundle args) { return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)); } } /* package */ interface ConnectionCallback { void onAccepted(); void onRejected(); } private final Timeline.Window window; private boolean released; /* package */ final MediaControllerImpl impl; /* package */ final Listener listener; /* package */ final Handler applicationHandler; @VisibleForTesting long timeDiffMs; private boolean connectionNotified; /* package */ final ConnectionCallback connectionCallback; /** Creates a {@link MediaController} from the {@link SessionToken}. */ /* package */ MediaController( Context context, SessionToken token, Bundle connectionHints, Listener listener, Looper applicationLooper, ConnectionCallback connectionCallback) { checkNotNull(context, "context must not be null"); checkNotNull(token, "token must not be null"); // Initialize default values. window = new Timeline.Window(); timeDiffMs = C.TIME_UNSET; // Initialize members with params. this.listener = listener; applicationHandler = new Handler(applicationLooper); this.connectionCallback = connectionCallback; @SuppressWarnings("nullness:assignment") @Initialized MediaController thisRef = this; impl = thisRef.createImpl(context, thisRef, token, connectionHints); } /* package */ MediaControllerImpl createImpl( Context context, MediaController thisRef, SessionToken token, Bundle connectionHints) { if (token.isLegacySession()) { return new MediaControllerImplLegacy(context, thisRef, token); } else { return new MediaControllerImplBase(context, thisRef, token, connectionHints); } } @Override public void stop() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring stop()."); return; } impl.stop(); } @UnstableApi @Deprecated @Override public void stop(boolean reset) { throw new UnsupportedOperationException(); } /** * Releases the connection between {@link MediaController} and {@link MediaSession}. This method * must be called when the controller is no longer required. The controller must not be used after * calling this method. * *

This method does not call {@link Player#release()} of the underlying player in the session. */ @Override public void release() { verifyApplicationThread(); if (released) { return; } released = true; try { impl.release(); } catch (Exception e) { // Should not be here. Log.d(TAG, "Exception while releasing impl", e); } if (connectionNotified) { notifyControllerListener(listener -> listener.onDisconnected(this)); } else { connectionNotified = true; connectionCallback.onRejected(); } } /** * Releases the future controller returned by {@link Builder#buildAsync()}. It makes sure that the * controller is released by canceling the future if the future is not yet done. */ public static void releaseFuture(Future controllerFuture) { if (!controllerFuture.isDone()) { controllerFuture.cancel(/* mayInterruptIfRunning= */ true); return; } MediaController controller; try { controller = controllerFuture.get(); } catch (CancellationException | ExecutionException | InterruptedException e) { return; } controller.release(); } /** * Returns the {@link SessionToken} of the connected session, or {@code null} if it is not * connected. * *

This may differ from the {@link SessionToken} from the constructor. For example, if the * controller is created with the token for {@link MediaSessionService}, this will return a token * for the {@link MediaSession} in the service. */ @Nullable public SessionToken getConnectedToken() { return isConnected() ? impl.getConnectedToken() : null; } /** Returns whether this controller is connected to a {@link MediaSession} or not. */ public boolean isConnected() { return impl.isConnected(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with * previously called {@link #setMediaUri}. See {@link #setMediaUri} for details. */ @Override public void play() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring play()."); return; } impl.play(); } @Override public void pause() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring pause()."); return; } impl.pause(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with * previously called {@link #setMediaUri}. See {@link #setMediaUri} for details. */ @Override public void prepare() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring prepare()."); return; } impl.prepare(); } @Override public void seekToDefaultPosition() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekTo()."); return; } impl.seekToDefaultPosition(); } @Override public void seekToDefaultPosition(int windowIndex) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekTo()."); return; } impl.seekToDefaultPosition(windowIndex); } @Override public void seekTo(long positionMs) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekTo()."); return; } impl.seekTo(positionMs); } @Override public void seekTo(int windowIndex, long positionMs) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekTo()."); return; } impl.seekTo(windowIndex, positionMs); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link MediaSessionCompat}, it returns {code 0}. */ @Override public long getSeekBackIncrement() { verifyApplicationThread(); return isConnected() ? impl.getSeekBackIncrement() : 0; } /** * {@inheritDoc} * *

Interoperability: When connected to {@link MediaSessionCompat}, it calls {@link * MediaControllerCompat.TransportControls#rewind()}. */ @Override public void seekBack() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekBack()."); return; } impl.seekBack(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link MediaSessionCompat}, it returns {code 0}. */ @Override public long getSeekForwardIncrement() { verifyApplicationThread(); return isConnected() ? impl.getSeekForwardIncrement() : 0; } /** * {@inheritDoc} * *

Interoperability: When connected to {@link MediaSessionCompat}, it calls {@link * MediaControllerCompat.TransportControls#fastForward()}. */ @Override public void seekForward() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekForward()."); return; } impl.seekForward(); } /** Returns an intent for launching UI associated with the session if exists, or {@code null}. */ @Nullable public PendingIntent getSessionActivity() { return isConnected() ? impl.getSessionActivity() : null; } @Override @Nullable public PlaybackException getPlayerError() { verifyApplicationThread(); return isConnected() ? impl.getPlayerError() : null; } @Override public void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThread(); if (isConnected()) { impl.setPlayWhenReady(playWhenReady); } } @Override public boolean getPlayWhenReady() { verifyApplicationThread(); return isConnected() && impl.getPlayWhenReady(); } @Override @PlaybackSuppressionReason public int getPlaybackSuppressionReason() { verifyApplicationThread(); return isConnected() ? impl.getPlaybackSuppressionReason() : Player.PLAYBACK_SUPPRESSION_REASON_NONE; } @Override @State public int getPlaybackState() { verifyApplicationThread(); return isConnected() ? impl.getPlaybackState() : Player.STATE_IDLE; } @Override public boolean isPlaying() { verifyApplicationThread(); return isConnected() && impl.isPlaying(); } @Override public boolean isLoading() { verifyApplicationThread(); return isConnected() && impl.isLoading(); } @Override public long getDuration() { verifyApplicationThread(); return isConnected() ? impl.getDuration() : C.TIME_UNSET; } @Override public long getCurrentPosition() { verifyApplicationThread(); return isConnected() ? impl.getCurrentPosition() : 0; } @Override public long getBufferedPosition() { verifyApplicationThread(); return isConnected() ? impl.getBufferedPosition() : 0; } @Override public int getBufferedPercentage() { verifyApplicationThread(); return isConnected() ? impl.getBufferedPercentage() : 0; } @Override public long getTotalBufferedDuration() { verifyApplicationThread(); return isConnected() ? impl.getTotalBufferedDuration() : 0; } @Override public long getCurrentLiveOffset() { verifyApplicationThread(); return isConnected() ? impl.getCurrentLiveOffset() : C.TIME_UNSET; } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it's the same as {@link #getDuration()} * to match the behavior with {@link #getContentPosition()} and {@link * #getContentBufferedPosition()}. */ @Override public long getContentDuration() { verifyApplicationThread(); return isConnected() ? impl.getContentDuration() : C.TIME_UNSET; } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it's the same as {@link * #getCurrentPosition()} because content position isn't available. */ @Override public long getContentPosition() { verifyApplicationThread(); return isConnected() ? impl.getContentPosition() : 0; } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it's the same as {@link * #getBufferedPosition()} because content buffered position isn't available. */ @Override public long getContentBufferedPosition() { verifyApplicationThread(); return isConnected() ? impl.getContentBufferedPosition() : 0; } @Override public boolean isPlayingAd() { verifyApplicationThread(); return isConnected() && impl.isPlayingAd(); } @Override public int getCurrentAdGroupIndex() { verifyApplicationThread(); return isConnected() ? impl.getCurrentAdGroupIndex() : C.INDEX_UNSET; } @Override public int getCurrentAdIndexInAdGroup() { verifyApplicationThread(); return isConnected() ? impl.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { verifyApplicationThread(); checkNotNull(playbackParameters, "playbackParameters must not be null"); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setPlaybackParameters()."); return; } impl.setPlaybackParameters(playbackParameters); } @Override public void setPlaybackSpeed(float speed) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setPlaybackSpeed()."); return; } impl.setPlaybackSpeed(speed); } @Override public PlaybackParameters getPlaybackParameters() { verifyApplicationThread(); return isConnected() ? impl.getPlaybackParameters() : PlaybackParameters.DEFAULT; } @Override public AudioAttributes getAudioAttributes() { verifyApplicationThread(); if (!isConnected()) { return AudioAttributes.DEFAULT; } return impl.getAudioAttributes(); } /** * 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#userRating}. * *

If the user rating was {@code null}, the media item does not accept setting user rating. * * @param mediaId The non-empty {@link MediaItem#mediaId}. * @param rating The rating to set. * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending * completion. */ public ListenableFuture setRating(String mediaId, Rating rating) { verifyApplicationThread(); checkNotNull(mediaId, "mediaId must not be null"); checkNotEmpty(mediaId, "mediaId must not be empty"); checkNotNull(rating, "rating must not be null"); if (isConnected()) { return impl.setRating(mediaId, rating); } return createDisconnectedFuture(); } /** * Requests that the connected {@link MediaSession} rates the current media item. 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#userRating}. * *

If the user rating was {@code null}, the media item does not accept setting user rating. * * @param rating The rating to set. * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending * completion. */ public ListenableFuture setRating(Rating rating) { verifyApplicationThread(); checkNotNull(rating, "rating must not be null"); if (isConnected()) { return impl.setRating(rating); } return createDisconnectedFuture(); } /** * Sends a custom command to the session. * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, {@link SessionResult#resultCode} will * return the custom result code from the {@code android.os.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 The custom command. * @param args The additional arguments. May be empty. * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending * completion. */ public ListenableFuture sendCustomCommand(SessionCommand command, Bundle args) { verifyApplicationThread(); checkNotNull(command, "command must not be null"); checkArgument( command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM, "command must be a custom command"); if (isConnected()) { return impl.sendCustomCommand(command, args); } return createDisconnectedFuture(); } /** Returns {@code null}. */ @UnstableApi @Override @Nullable public Object getCurrentManifest() { return null; } /** * {@inheritDoc} * *

Caveat: Some methods of the {@link Timeline} such as {@link Timeline#getPeriodByUid(Object, * Timeline.Period)}, {@link Timeline#getIndexOfPeriod(Object)}, and {@link * Timeline#getUidOfPeriod(int)} will throw {@link UnsupportedOperationException} because of the * limitation of restoring the instance sent from session as described in {@link * Timeline#CREATOR}. */ @Override public Timeline getCurrentTimeline() { verifyApplicationThread(); return isConnected() ? impl.getCurrentTimeline() : Timeline.EMPTY; } @Override public void setMediaItem(MediaItem mediaItem) { verifyApplicationThread(); checkNotNull(mediaItem, "mediaItems must not be null"); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setMediaItem()."); return; } impl.setMediaItem(mediaItem); } @Override public void setMediaItem(MediaItem mediaItem, long startPositionMs) { verifyApplicationThread(); checkNotNull(mediaItem, "mediaItems must not be null"); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setMediaItem()."); return; } impl.setMediaItem(mediaItem, startPositionMs); } @Override public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { verifyApplicationThread(); checkNotNull(mediaItem, "mediaItems must not be null"); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setMediaItems()."); return; } impl.setMediaItem(mediaItem, resetPosition); } @Override public void setMediaItems(List mediaItems) { verifyApplicationThread(); checkNotNull(mediaItems, "mediaItems must not be null"); for (int i = 0; i < mediaItems.size(); i++) { checkArgument(mediaItems.get(i) != null, "items must not contain null, index=" + i); } if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setMediaItems()."); return; } impl.setMediaItems(mediaItems); } @Override public void setMediaItems(List mediaItems, boolean resetPosition) { verifyApplicationThread(); checkNotNull(mediaItems, "mediaItems must not be null"); for (int i = 0; i < mediaItems.size(); i++) { checkArgument(mediaItems.get(i) != null, "items must not contain null, index=" + i); } if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setMediaItems()."); return; } impl.setMediaItems(mediaItems, resetPosition); } @Override public void setMediaItems( List mediaItems, int startWindowIndex, long startPositionMs) { verifyApplicationThread(); checkNotNull(mediaItems, "mediaItems must not be null"); for (int i = 0; i < mediaItems.size(); i++) { checkArgument(mediaItems.get(i) != null, "items must not contain null, index=" + i); } if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setMediaItems()."); return; } impl.setMediaItems(mediaItems, startWindowIndex, startPositionMs); } /** * Requests that the connected {@link MediaSession} sets a specific {@link Uri} for playback. Use * this, or {@link #setMediaItems} to specify which item(s) to play. * *

This can be called multiple times in any states. This would override previous call of this, * or {@link #setMediaItems}. * *

The {@link Player.Listener#onTimelineChanged} and/or {@link * Player.Listener#onMediaItemTransition} would be called when it's completed. * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this call will be grouped together with * later {@link #prepare} or {@link #play}, depending on the uri pattern as follows: * * * * * * * * * * * * * * * * * * * * * * * * * *
Uri patterns and following API calls for MediaControllerCompat methods
Uri patternsFollowing API callsMethod
{@code androidx://media3-session/setMediaUri?uri=[uri]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} *
{@link #play}{@link MediaControllerCompat.TransportControls#playFromUri playFromUri} *
{@code androidx://media3-session/setMediaUri?id=[mediaId]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} *
{@link #play}{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId} *
{@code androidx://media3-session/setMediaUri?query=[query]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} *
{@link #play}{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch} *
Does not match with any pattern above{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} *
{@link #play}{@link MediaControllerCompat.TransportControls#playFromUri playFromUri} *
* *

Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's * handled together with {@link #prepare} or {@link #play}. If this API is called multiple times * without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned for * previous calls. * * @param uri The uri of the item(s) to play. * @param extras A {@link Bundle} to send extra information. May be empty. * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending * completion. * @see MediaConstants#MEDIA_URI_AUTHORITY * @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID * @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID * @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_SEARCH * @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_SEARCH * @see MediaConstants#MEDIA_URI_PATH_SET_MEDIA_URI * @see MediaConstants#MEDIA_URI_QUERY_ID * @see MediaConstants#MEDIA_URI_QUERY_QUERY * @see MediaConstants#MEDIA_URI_QUERY_URI */ public ListenableFuture setMediaUri(Uri uri, Bundle extras) { verifyApplicationThread(); checkNotNull(uri); checkNotNull(extras); if (isConnected()) { return impl.setMediaUri(uri, extras); } return createDisconnectedFuture(); } @Override public void setPlaylistMetadata(MediaMetadata playlistMetadata) { verifyApplicationThread(); checkNotNull(playlistMetadata, "playlistMetadata must not be null"); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setPlaylistMetadata()."); return; } impl.setPlaylistMetadata(playlistMetadata); } @Override public MediaMetadata getPlaylistMetadata() { verifyApplicationThread(); return isConnected() ? impl.getPlaylistMetadata() : MediaMetadata.EMPTY; } @Override public void addMediaItem(MediaItem mediaItem) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring addMediaItem()."); return; } impl.addMediaItem(mediaItem); } @Override public void addMediaItem(int index, MediaItem mediaItem) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring addMediaItem()."); return; } impl.addMediaItem(index, mediaItem); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items. */ @Override public void addMediaItems(List mediaItems) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring addMediaItems()."); return; } impl.addMediaItems(mediaItems); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items. */ @Override public void addMediaItems(int index, List mediaItems) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring addMediaItems()."); return; } impl.addMediaItems(index, mediaItems); } @Override public void removeMediaItem(int index) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring removeMediaItem()."); return; } impl.removeMediaItem(index); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically remove items. */ @Override public void removeMediaItems(int fromIndex, int toIndex) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring removeMediaItems()."); return; } impl.removeMediaItems(fromIndex, toIndex); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically clear items. */ @Override public void clearMediaItems() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring clearMediaItems()."); return; } impl.clearMediaItems(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically move items. */ @Override public void moveMediaItem(int currentIndex, int newIndex) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring moveMediaItem()."); return; } impl.moveMediaItem(currentIndex, newIndex); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically move items. */ @Override public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring moveMediaItems()."); return; } impl.moveMediaItems(fromIndex, toIndex, newIndex); } @UnstableApi @Deprecated @Override public boolean isCurrentWindowDynamic() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return isCurrentMediaItemDynamic(); } @Override public boolean isCurrentMediaItemDynamic() { verifyApplicationThread(); Timeline timeline = getCurrentTimeline(); return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic; } @UnstableApi @Deprecated @Override public boolean isCurrentWindowLive() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return isCurrentMediaItemLive(); } @Override public boolean isCurrentMediaItemLive() { verifyApplicationThread(); Timeline timeline = getCurrentTimeline(); return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isLive(); } @UnstableApi @Deprecated @Override public boolean isCurrentWindowSeekable() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return isCurrentMediaItemSeekable(); } @Override public boolean isCurrentMediaItemSeekable() { verifyApplicationThread(); Timeline timeline = getCurrentTimeline(); return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isSeekable; } /** * {@inheritDoc} * *

The MediaController returns {@code false}. */ @Override public boolean canAdvertiseSession() { return false; } @Override @Nullable public MediaItem getCurrentMediaItem() { Timeline timeline = getCurrentTimeline(); return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).mediaItem; } @Override public int getMediaItemCount() { return getCurrentTimeline().getWindowCount(); } @Override public MediaItem getMediaItemAt(int index) { return getCurrentTimeline().getWindow(index, window).mediaItem; } @Override public int getCurrentPeriodIndex() { verifyApplicationThread(); return isConnected() ? impl.getCurrentPeriodIndex() : C.INDEX_UNSET; } @UnstableApi @Deprecated @Override public int getCurrentWindowIndex() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return getCurrentMediaItemIndex(); } @Override public int getCurrentMediaItemIndex() { verifyApplicationThread(); return isConnected() ? impl.getCurrentWindowIndex() : C.INDEX_UNSET; } @UnstableApi @Deprecated @Override public int getPreviousWindowIndex() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return getPreviousMediaItemIndex(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this will always return {@link * C#INDEX_UNSET} even when {@link #hasPreviousWindow()} is {@code true}. */ @Override public int getPreviousMediaItemIndex() { verifyApplicationThread(); return isConnected() ? impl.getPreviousWindowIndex() : C.INDEX_UNSET; } @UnstableApi @Deprecated @Override public int getNextWindowIndex() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return getNextMediaItemIndex(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, this will always return {@link * C#INDEX_UNSET} even when {@link #hasNextWindow()} is {@code true}. */ @Override public int getNextMediaItemIndex() { verifyApplicationThread(); return isConnected() ? impl.getNextWindowIndex() : C.INDEX_UNSET; } @UnstableApi @Deprecated @Override public boolean hasPrevious() { throw new UnsupportedOperationException(); } @UnstableApi @Deprecated @Override public boolean hasNext() { throw new UnsupportedOperationException(); } @UnstableApi @Deprecated @Override public boolean hasPreviousWindow() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return hasPreviousMediaItem(); } @UnstableApi @Deprecated @Override public boolean hasNextWindow() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. return hasNextMediaItem(); } @Override public boolean hasPreviousMediaItem() { verifyApplicationThread(); return isConnected() && impl.hasPreviousWindow(); } @Override public boolean hasNextMediaItem() { verifyApplicationThread(); return isConnected() && impl.hasNextWindow(); } @UnstableApi @Deprecated @Override public void previous() { throw new UnsupportedOperationException(); } @UnstableApi @Deprecated @Override public void next() { throw new UnsupportedOperationException(); } @UnstableApi @Deprecated @Override public void seekToPreviousWindow() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. seekToPreviousMediaItem(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it's the same as {@link #seekToPrevious}. */ @Override public void seekToPreviousMediaItem() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekToPreviousMediaItem()."); return; } impl.seekToPreviousWindow(); } @UnstableApi @Deprecated @Override public void seekToNextWindow() { // TODO(b/202157117): Throw UnsupportedOperationException when all callers are migrated. seekToNextMediaItem(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it's the same as {@link #seekToNext}. */ @Override public void seekToNextMediaItem() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekToNextMediaItem()."); return; } impl.seekToNextWindow(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it wouldn't update current window index * immediately because previous window index is unknown. */ @Override public void seekToPrevious() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekToPrevious()."); return; } impl.seekToPrevious(); } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it always returns {@code 0}. */ @Override public long getMaxSeekToPreviousPosition() { verifyApplicationThread(); return isConnected() ? impl.getMaxSeekToPreviousPosition() : 0L; } /** * {@inheritDoc} * *

Interoperability: When connected to {@link * android.support.v4.media.session.MediaSessionCompat}, it wouldn't update current window index * immediately because previous window index is unknown. */ @Override public void seekToNext() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring seekToNext()."); return; } impl.seekToNext(); } @Override @RepeatMode public int getRepeatMode() { verifyApplicationThread(); return isConnected() ? impl.getRepeatMode() : Player.REPEAT_MODE_OFF; } @Override public void setRepeatMode(@RepeatMode int repeatMode) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setRepeatMode()."); return; } impl.setRepeatMode(repeatMode); } @Override public boolean getShuffleModeEnabled() { verifyApplicationThread(); return isConnected() && impl.getShuffleModeEnabled(); } @Override public void setShuffleModeEnabled(boolean shuffleModeEnabled) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setShuffleMode()."); return; } impl.setShuffleModeEnabled(shuffleModeEnabled); } @Override public VideoSize getVideoSize() { verifyApplicationThread(); return isConnected() ? impl.getVideoSize() : VideoSize.UNKNOWN; } @Override public void clearVideoSurface() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring clearVideoSurface()."); return; } impl.clearVideoSurface(); } @Override public void clearVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring clearVideoSurface()."); return; } impl.clearVideoSurface(surface); } @Override public void setVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setVideoSurface()."); return; } impl.setVideoSurface(surface); } @Override public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setVideoSurfaceHolder()."); return; } impl.setVideoSurfaceHolder(surfaceHolder); } @Override public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring clearVideoSurfaceHolder()."); return; } impl.clearVideoSurfaceHolder(surfaceHolder); } @Override public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setVideoSurfaceView()."); return; } impl.setVideoSurfaceView(surfaceView); } @Override public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring clearVideoSurfaceView()."); return; } impl.clearVideoSurfaceView(surfaceView); } @Override public void setVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setVideoTextureView()."); return; } impl.setVideoTextureView(textureView); } @Override public void clearVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring clearVideoTextureView()."); return; } impl.clearVideoTextureView(textureView); } @Override public List getCurrentCues() { verifyApplicationThread(); return isConnected() ? impl.getCurrentCues() : ImmutableList.of(); } @Override @FloatRange(from = 0, to = 1) public float getVolume() { verifyApplicationThread(); return isConnected() ? impl.getVolume() : 1; } @Override public void setVolume(@FloatRange(from = 0, to = 1) float volume) { verifyApplicationThread(); checkArgument(volume >= 0 && volume <= 1, "volume must be between 0 and 1"); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setVolume()."); return; } impl.setVolume(volume); } @Override public DeviceInfo getDeviceInfo() { verifyApplicationThread(); if (!isConnected()) { return DeviceInfo.UNKNOWN; } return impl.getDeviceInfo(); } @Override public int getDeviceVolume() { verifyApplicationThread(); if (!isConnected()) { return 0; } return impl.getDeviceVolume(); } @Override public boolean isDeviceMuted() { verifyApplicationThread(); if (!isConnected()) { return false; } return impl.isDeviceMuted(); } @Override public void setDeviceVolume(int volume) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setDeviceVolume()."); return; } impl.setDeviceVolume(volume); } @Override public void increaseDeviceVolume() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring increaseDeviceVolume()."); return; } impl.increaseDeviceVolume(); } @Override public void decreaseDeviceVolume() { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring decreaseDeviceVolume()."); return; } impl.decreaseDeviceVolume(); } @Override public void setDeviceMuted(boolean muted) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setDeviceMuted()."); return; } impl.setDeviceMuted(muted); } @Override public MediaMetadata getMediaMetadata() { verifyApplicationThread(); return isConnected() ? impl.getMediaMetadata() : MediaMetadata.EMPTY; } /** Returns {@link TrackGroupArray#EMPTY}. */ @UnstableApi @Override public TrackGroupArray getCurrentTrackGroups() { return TrackGroupArray.EMPTY; } /** Returns an empty {@link TrackSelectionArray}. */ @UnstableApi @Override public TrackSelectionArray getCurrentTrackSelections() { return new TrackSelectionArray(); } @Override public TracksInfo getCurrentTracksInfo() { return TracksInfo.EMPTY; // TODO(b/178486745) } @Override public TrackSelectionParameters getTrackSelectionParameters() { verifyApplicationThread(); if (!isConnected()) { return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT; } return impl.getTrackSelectionParameters(); } @Override public void setTrackSelectionParameters(TrackSelectionParameters parameters) { verifyApplicationThread(); if (!isConnected()) { Log.w(TAG, "The controller is not connected. Ignoring setTrackSelectionParameters()."); } impl.setTrackSelectionParameters(parameters); } @Override public Looper getApplicationLooper() { return applicationHandler.getLooper(); } /** * Sets the time diff forcefully when calculating current position. * * @param timeDiffMs {@code C.TIME_UNSET} for reset. */ /* package */ void setTimeDiffMs(long timeDiffMs) { verifyApplicationThread(); this.timeDiffMs = timeDiffMs; } @Override public void addListener(Player.Listener listener) { checkNotNull(listener, "listener must not be null"); impl.addListener(listener); } @Override public void removeListener(Player.Listener listener) { checkNotNull(listener, "listener must not be null"); impl.removeListener(listener); } @Override public boolean isCommandAvailable(@Command int command) { return getAvailableCommands().contains(command); } @Override public Commands getAvailableCommands() { verifyApplicationThread(); if (!isConnected()) { return Commands.EMPTY; } return impl.getAvailableCommands(); } /** * Returns whether the {@link SessionCommand.CommandCode} is available. The {@code * sessionCommandCode} must not be {@link SessionCommand#COMMAND_CODE_CUSTOM}. Use {@link * #isSessionCommandAvailable(SessionCommand)} for custom commands. */ public boolean isSessionCommandAvailable(@SessionCommand.CommandCode int sessionCommandCode) { return getAvailableSessionCommands().contains(sessionCommandCode); } /** Returns whether the {@link SessionCommand} is available. */ public boolean isSessionCommandAvailable(SessionCommand sessionCommand) { return getAvailableSessionCommands().contains(sessionCommand); } /** * Returns the current available session commands from {@link * Listener#onAvailableSessionCommandsChanged(MediaController, SessionCommands)}, or {@link * SessionCommands#EMPTY} if it is not connected. * * @return The available session commands. */ public SessionCommands getAvailableSessionCommands() { verifyApplicationThread(); if (!isConnected()) { return SessionCommands.EMPTY; } return impl.getAvailableSessionCommands(); } private static ListenableFuture createDisconnectedFuture() { return Futures.immediateFuture( new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); } /* package */ void runOnApplicationLooper(Runnable runnable) { postOrRun(applicationHandler, runnable); } /* package */ void notifyControllerListener(Consumer listenerConsumer) { checkState(Looper.myLooper() == getApplicationLooper()); listenerConsumer.accept(listener); } /* package */ void notifyAccepted() { checkState(Looper.myLooper() == getApplicationLooper()); checkState(!connectionNotified); connectionNotified = true; connectionCallback.onAccepted(); } private void verifyApplicationThread() { checkState(Looper.myLooper() == getApplicationLooper(), WRONG_THREAD_ERROR_MESSAGE); } // TODO(b/202157117): Rename Window references to MediaItem. interface MediaControllerImpl { void addListener(Player.Listener listener); void removeListener(Player.Listener listener); @Nullable SessionToken getConnectedToken(); boolean isConnected(); void play(); void pause(); void setPlayWhenReady(boolean playWhenReady); void prepare(); void stop(); void release(); void seekToDefaultPosition(); void seekToDefaultPosition(int windowIndex); void seekTo(long positionMs); void seekTo(int windowIndex, long positionMs); long getSeekBackIncrement(); void seekBack(); long getSeekForwardIncrement(); void seekForward(); @Nullable PendingIntent getSessionActivity(); @Nullable PlaybackException getPlayerError(); long getDuration(); long getCurrentPosition(); long getBufferedPosition(); int getBufferedPercentage(); long getTotalBufferedDuration(); long getCurrentLiveOffset(); long getContentDuration(); long getContentPosition(); long getContentBufferedPosition(); boolean isPlayingAd(); int getCurrentAdGroupIndex(); int getCurrentAdIndexInAdGroup(); void setPlaybackParameters(PlaybackParameters playbackParameters); void setPlaybackSpeed(float speed); PlaybackParameters getPlaybackParameters(); AudioAttributes getAudioAttributes(); ListenableFuture setRating(String mediaId, Rating rating); ListenableFuture setRating(Rating rating); ListenableFuture sendCustomCommand(SessionCommand command, Bundle args); Timeline getCurrentTimeline(); void setMediaItem(MediaItem mediaItem); void setMediaItem(MediaItem mediaItem, long startPositionMs); void setMediaItem(MediaItem mediaItem, boolean resetPosition); void setMediaItems(List mediaItems); void setMediaItems(List mediaItems, boolean resetPosition); void setMediaItems(List mediaItems, int startWindowIndex, long startPositionMs); ListenableFuture setMediaUri(Uri uri, Bundle extras); void setPlaylistMetadata(MediaMetadata playlistMetadata); MediaMetadata getPlaylistMetadata(); void addMediaItem(MediaItem mediaItem); void addMediaItem(int index, MediaItem mediaItem); void addMediaItems(List mediaItems); void addMediaItems(int index, List mediaItems); void removeMediaItem(int index); void removeMediaItems(int fromIndex, int toIndex); void clearMediaItems(); void moveMediaItem(int currentIndex, int newIndex); void moveMediaItems(int fromIndex, int toIndex, int newIndex); int getCurrentPeriodIndex(); int getCurrentWindowIndex(); int getPreviousWindowIndex(); int getNextWindowIndex(); boolean hasPreviousWindow(); boolean hasNextWindow(); void seekToPreviousWindow(); void seekToNextWindow(); void seekToPrevious(); long getMaxSeekToPreviousPosition(); void seekToNext(); @RepeatMode int getRepeatMode(); void setRepeatMode(@RepeatMode int repeatMode); boolean getShuffleModeEnabled(); void setShuffleModeEnabled(boolean shuffleModeEnabled); VideoSize getVideoSize(); void clearVideoSurface(); void clearVideoSurface(@Nullable Surface surface); void setVideoSurface(@Nullable Surface surface); void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); void setVideoSurfaceView(@Nullable SurfaceView surfaceView); void clearVideoSurfaceView(@Nullable SurfaceView surfaceView); void setVideoTextureView(@Nullable TextureView textureView); void clearVideoTextureView(@Nullable TextureView textureView); List getCurrentCues(); float getVolume(); void setVolume(float volume); DeviceInfo getDeviceInfo(); int getDeviceVolume(); boolean isDeviceMuted(); void setDeviceVolume(int volume); void increaseDeviceVolume(); void decreaseDeviceVolume(); void setDeviceMuted(boolean muted); boolean getPlayWhenReady(); @PlaybackSuppressionReason int getPlaybackSuppressionReason(); @State int getPlaybackState(); boolean isPlaying(); boolean isLoading(); MediaMetadata getMediaMetadata(); Commands getAvailableCommands(); TrackSelectionParameters getTrackSelectionParameters(); void setTrackSelectionParameters(TrackSelectionParameters parameters); SessionCommands getAvailableSessionCommands(); // Internally used methods Context getContext(); @Nullable MediaBrowserCompat getBrowserCompat(); } }