/* * 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: * *
* *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. * *
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. * *
The app targeting API level 30 or higher must include a {@code The detailed behavior of the {@link MediaController} differs depending on the type of the
* token as follows.
*
* 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:
*
* 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 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 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 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.
*
* 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 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 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 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 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:
*
* 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 Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items.
*/
@Override
public void addMediaItems(List Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items.
*/
@Override
public void addMediaItems(int index, List 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{@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}.
*
*
*
*
* @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.
*
* {@code
* MediaController.Builder builder = ...;
* ListenableFuture
*
*
*
*
*
* Uri patterns Following API calls Method
*
* {@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}
*