/*
* 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.annotation.VisibleForTesting.NONE;
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.IntRange;
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}.
*
* <p>Topics covered here:
*
* <ol>
* <li><a href="#ControllerLifeCycle">Controller Lifecycle</a>
* <li><a href="#ThreadingModel">Threading Model</a>
* <li><a href="#PackageVisibilityFilter">Package Visibility Filter</a>
* </ol>
*
* <h2 id="ControllerLifeCycle">Controller Lifecycle</h2>
*
* <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 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.
*
* <h2 id="ThreadingModel">Threading Model</h2>
*
* <p>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.
*
* <h2 id="PackageVisibilityFilter">Package Visibility Filter</h2>
*
* <p>The app targeting API level 30 or higher must include a {@code <queries>} 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 <a href="//developer.android.com/training/package-visibility">this
* guide</a> for more information.
*
* <pre>{@code
* <!-- As intent actions -->
* <intent>
* <action android:name="androidx.media3.session.MediaSessionService" />
* </intent>
* <intent>
* <action android:name="androidx.media3.session.MediaLibraryService" />
* </intent>
* <intent>
* <action android:name="android.media.browse.MediaBrowserService" />
* </intent>
* <!-- Or, as a package name -->
* <package android:name="package_name_of_the_other_app" />
* }</pre>
*/
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}.
*
* <p>The detailed behavior of the {@link MediaController} differs depending on the type of the
* token as follows.
*
* <ol>
* <li>{@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.
* <li>{@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.
* </ol>
*
* @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.
*
* <p>The hints are session-specific arguments sent to the session when connecting. The contents
* of this bundle may affect the connection result.
*
* <p>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.
*
* <p>The controller instance can be obtained like the following example:
*
* <pre>{@code
* MediaController.Builder builder = ...;
* ListenableFuture<MediaController> 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());
* }</pre>
*
* <p>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<MediaController> buildAsync() {
MediaControllerHolder<MediaController> 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}.
*
* <p>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.
*
* <p>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}.
*
* <p>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)}.
*
* <p>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<SessionResult> onSetCustomLayout(
MediaController controller, List<CommandButton> 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}.
*
* <p>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)}.
*
* <p>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<SessionResult> 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;
private 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);
impl.connect();
}
/* 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.
*
* <p>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<? extends MediaController> 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.
*
* <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 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}
*
* <p>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}
*
* <p>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 mediaItemIndex) {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring seekTo().");
return;
}
impl.seekToDefaultPosition(mediaItemIndex);
}
@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 mediaItemIndex, long positionMs) {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring seekTo().");
return;
}
impl.seekTo(mediaItemIndex, positionMs);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link MediaSessionCompat}, it returns {code 0}.
*/
@Override
public long getSeekBackIncrement() {
verifyApplicationThread();
return isConnected() ? impl.getSeekBackIncrement() : 0;
}
/**
* {@inheritDoc}
*
* <p>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}
*
* <p>Interoperability: When connected to {@link MediaSessionCompat}, it returns {code 0}.
*/
@Override
public long getSeekForwardIncrement() {
verifyApplicationThread();
return isConnected() ? impl.getSeekForwardIncrement() : 0;
}
/**
* {@inheritDoc}
*
* <p>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
public @PlaybackSuppressionReason int getPlaybackSuppressionReason() {
verifyApplicationThread();
return isConnected()
? impl.getPlaybackSuppressionReason()
: Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
@Override
public @State 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
@IntRange(from = 0, to = 100)
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}
*
* <p>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}
*
* <p>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}
*
* <p>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}.
*
* <p>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<SessionResult> 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}.
*
* <p>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<SessionResult> 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.
*
* <p>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}.
*
* <p>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<SessionResult> 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}
*
* <p>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<MediaItem> 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<MediaItem> 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<MediaItem> mediaItems, int startIndex, 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, startIndex, 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.
*
* <p>This can be called multiple times in any states. This would override previous call of this,
* or {@link #setMediaItems}.
*
* <p>The {@link Player.Listener#onTimelineChanged} and/or {@link
* Player.Listener#onMediaItemTransition} would be called when it's completed.
*
* <p>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:
*
* <table>
* <caption>Uri patterns and following API calls for MediaControllerCompat methods</caption>
* <tr>
* <th>Uri patterns</th><th>Following API calls</th><th>Method</th>
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?uri=[uri]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?id=[mediaId]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?query=[query]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
* </tr><tr>
* <td rowspan="2">Does not match with any pattern above</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr></table>
*
* <p>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<SessionResult> 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}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items.
*/
@Override
public void addMediaItems(List<MediaItem> mediaItems) {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring addMediaItems().");
return;
}
impl.addMediaItems(mediaItems);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items.
*/
@Override
public void addMediaItems(int index, List<MediaItem> 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}
*
* <p>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}
*
* <p>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}
*
* <p>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}
*
* <p>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() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentMediaItemDynamic() {
verifyApplicationThread();
Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic;
}
@UnstableApi
@Deprecated
@Override
public boolean isCurrentWindowLive() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentMediaItemLive() {
verifyApplicationThread();
Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isLive();
}
@UnstableApi
@Deprecated
@Override
public boolean isCurrentWindowSeekable() {
throw new UnsupportedOperationException();
}
@Override
public boolean isCurrentMediaItemSeekable() {
verifyApplicationThread();
Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isSeekable;
}
/**
* {@inheritDoc}
*
* <p>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(getCurrentMediaItemIndex(), 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() {
throw new UnsupportedOperationException();
}
@Override
public int getCurrentMediaItemIndex() {
verifyApplicationThread();
return isConnected() ? impl.getCurrentMediaItemIndex() : C.INDEX_UNSET;
}
@UnstableApi
@Deprecated
@Override
public int getPreviousWindowIndex() {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this will always return {@link
* C#INDEX_UNSET} even when {@link #hasPreviousMediaItem()} is {@code true}.
*/
@Override
public int getPreviousMediaItemIndex() {
verifyApplicationThread();
return isConnected() ? impl.getPreviousMediaItemIndex() : C.INDEX_UNSET;
}
@UnstableApi
@Deprecated
@Override
public int getNextWindowIndex() {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this will always return {@link
* C#INDEX_UNSET} even when {@link #hasNextMediaItem()} is {@code true}.
*/
@Override
public int getNextMediaItemIndex() {
verifyApplicationThread();
return isConnected() ? impl.getNextMediaItemIndex() : 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() {
throw new UnsupportedOperationException();
}
@UnstableApi
@Deprecated
@Override
public boolean hasNextWindow() {
throw new UnsupportedOperationException();
}
@Override
public boolean hasPreviousMediaItem() {
verifyApplicationThread();
return isConnected() && impl.hasPreviousMediaItem();
}
@Override
public boolean hasNextMediaItem() {
verifyApplicationThread();
return isConnected() && impl.hasNextMediaItem();
}
@UnstableApi
@Deprecated
@Override
public void previous() {
throw new UnsupportedOperationException();
}
@UnstableApi
@Deprecated
@Override
public void next() {
throw new UnsupportedOperationException();
}
@UnstableApi
@Deprecated
@Override
public void seekToPreviousWindow() {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* <p>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.seekToPreviousMediaItem();
}
@UnstableApi
@Deprecated
@Override
public void seekToNextWindow() {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*
* <p>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.seekToNextMediaItem();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, it won't update the current media item
* index immediately because the previous media item index is unknown.
*/
@Override
public void seekToPrevious() {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring seekToPrevious().");
return;
}
impl.seekToPrevious();
}
/**
* {@inheritDoc}
*
* <p>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}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, it won't update the current media item
* index immediately because the previous media item index is unknown.
*/
@Override
public void seekToNext() {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring seekToNext().");
return;
}
impl.seekToNext();
}
@Override
public @RepeatMode 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<Cue> 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
@IntRange(from = 0)
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(@IntRange(from = 0) 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();
}
/**
* Gets the optional time diff (in milliseconds) used for calculating the current position, or
* {@link C#TIME_UNSET} if no diff should be applied.
*/
/* package */ long getTimeDiffMs() {
return timeDiffMs;
}
/**
* Sets the time diff (in milliseconds) used when calculating the current position.
*
* @param timeDiffMs {@link C#TIME_UNSET} for reset.
*/
@VisibleForTesting(otherwise = NONE)
/* 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<SessionResult> createDisconnectedFuture() {
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
}
/* package */ void runOnApplicationLooper(Runnable runnable) {
postOrRun(applicationHandler, runnable);
}
/* package */ void notifyControllerListener(Consumer<Listener> 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);
}
interface MediaControllerImpl {
void connect();
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 mediaItemIndex);
void seekTo(long positionMs);
void seekTo(int mediaItemIndex, 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<SessionResult> setRating(String mediaId, Rating rating);
ListenableFuture<SessionResult> setRating(Rating rating);
ListenableFuture<SessionResult> 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<MediaItem> mediaItems);
void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition);
void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs);
ListenableFuture<SessionResult> setMediaUri(Uri uri, Bundle extras);
void setPlaylistMetadata(MediaMetadata playlistMetadata);
MediaMetadata getPlaylistMetadata();
void addMediaItem(MediaItem mediaItem);
void addMediaItem(int index, MediaItem mediaItem);
void addMediaItems(List<MediaItem> mediaItems);
void addMediaItems(int index, List<MediaItem> 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 getCurrentMediaItemIndex();
int getPreviousMediaItemIndex();
int getNextMediaItemIndex();
boolean hasPreviousMediaItem();
boolean hasNextMediaItem();
void seekToPreviousMediaItem();
void seekToNextMediaItem();
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<Cue> 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();
}
}