/* * Copyright 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media2.common; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.media.MediaFormat; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.util.Log; import android.view.Surface; import androidx.annotation.CallSuper; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.concurrent.futures.ResolvableFuture; import androidx.core.util.Pair; import androidx.media.AudioAttributesCompat; import androidx.versionedparcelable.CustomVersionedParcelable; import androidx.versionedparcelable.NonParcelField; import androidx.versionedparcelable.ParcelField; import androidx.versionedparcelable.VersionedParcelize; import com.google.common.util.concurrent.ListenableFuture; import java.io.Closeable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; /** * Base interface for all media players that want media session. *

* APIs that return {@link ListenableFuture} should be the asynchronous calls and shouldn't block * the calling thread. This guarantees the APIs are safe to be called on the main thread. * *

Topics covered here are: *

    *
  1. Best practices *
  2. Player states *
  3. Invalid method calls *
* *

Best practices

* * Here are best practices when implementing/using SessionPlayer: * * * *

Player states

* The playback control of audio/video files is managed as a state machine. The SessionPlayer * defines four states: *
    *
  1. {@link #PLAYER_STATE_IDLE}: Initial state after the instantiation. *

    * While in this state, you should call {@link #setMediaItem(MediaItem)} or * {@link #setPlaylist(List, MediaMetadata)}. Check returned {@link ListenableFuture} for * potential error. *

    * Calling {@link #prepare()} transfers this object to {@link #PLAYER_STATE_PAUSED}. * *

  2. {@link #PLAYER_STATE_PAUSED}: State when the audio/video playback is paused. *

    * Call {@link #play()} to resume or start playback from the position where it paused. * *

  3. {@link #PLAYER_STATE_PLAYING}: State when the player plays the media item. *

    * In this state, {@link PlayerCallback#onBufferingStateChanged( * SessionPlayer, MediaItem, int)} will be called regularly to tell the buffering status. *

    * Playback state would remain {@link #PLAYER_STATE_PLAYING} when the currently playing * media item is changed. *

    * When the playback reaches the end of stream, the behavior depends on repeat mode, set by * {@link #setRepeatMode(int)}. If the repeat mode was set to {@link #REPEAT_MODE_NONE}, * the player will transfer to the {@link #PLAYER_STATE_PAUSED}. Otherwise, the * SessionPlayer object remains in the {@link #PLAYER_STATE_PLAYING} and playback will be * ongoing. * *

  4. {@link #PLAYER_STATE_ERROR}: State when the playback failed and player cannot be * recovered by itself. *

    * In general, playback might fail due to various reasons such as unsupported audio/video * format, poorly interleaved audio/video, resolution too high, streaming timeout, and * others. In addition, due to programming errors, a playback control operation might be * performed from an invalid state. In these cases the player * may transition to this state. *

* Subclasses may have extra methods to reset the player state to {@link #PLAYER_STATE_IDLE} from * other states. Take a look at documentations of specific subclass that you're interested in. *

* *

Invalid method calls

* The only method you safely call from the {@link #PLAYER_STATE_ERROR} is {@link #close()}. * Any other methods might return meaningless data. *

* Subclasses of the SessionPlayer may have extra methods that are safe to be called in the error * state and/or provide a method to recover from the error state. Take a look at documentations of * specific subclass that you're interested in. *

* Most methods can be called from any non-Error state. In case they're called in invalid state, * the implementation should ignore and would return {@link PlayerResult} with * {@link PlayerResult#RESULT_ERROR_INVALID_STATE}. The following table lists the methods that * aren't guaranteed to successfully running if they're called from the associated invalid states. *

* * * * * * * *
Method Name Invalid States
setAudioAttributes {Paused, Playing}
prepare {Paused, Playing}
play {Idle}
pause {Idle}
seekTo {Idle}
*/ // Previously MediaSessionCompat.Callback. // Players can extend this directly (e.g. MediaPlayer) or create wrapper and control underlying // player. // Preferably it can be interface, but API guideline requires to use abstract class. public abstract class SessionPlayer implements Closeable { private static final String TAG = "SessionPlayer"; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({ PLAYER_STATE_IDLE, PLAYER_STATE_PAUSED, PLAYER_STATE_PLAYING, PLAYER_STATE_ERROR}) @Retention(RetentionPolicy.SOURCE) public @interface PlayerState { } /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({ BUFFERING_STATE_UNKNOWN, BUFFERING_STATE_BUFFERING_AND_PLAYABLE, BUFFERING_STATE_BUFFERING_AND_STARVED, BUFFERING_STATE_COMPLETE}) @Retention(RetentionPolicy.SOURCE) public @interface BuffState { } /** * State when the player is idle, and needs configuration to start playback. */ public static final int PLAYER_STATE_IDLE = 0; /** * State when the player's playback is paused */ public static final int PLAYER_STATE_PAUSED = 1; /** * State when the player's playback is ongoing */ public static final int PLAYER_STATE_PLAYING = 2; /** * State when the player is in error state and cannot be recovered self. */ public static final int PLAYER_STATE_ERROR = 3; /** * Buffering state is unknown. */ public static final int BUFFERING_STATE_UNKNOWN = 0; /** * Buffering state indicating the player is buffering but enough has been buffered * for this player to be able to play the content. * See {@link #getBufferedPosition()} for how far is buffered already. */ public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1; /** * Buffering state indicating the player is buffering, but the player is currently starved * for data, and cannot play. */ public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2; /** * Buffering state indicating the player is done buffering, and the remainder of the content is * available for playback. */ public static final int BUFFERING_STATE_COMPLETE = 3; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL, REPEAT_MODE_GROUP}) @Retention(RetentionPolicy.SOURCE) public @interface RepeatMode { } /** * Playback will be stopped at the end of the playing media list. */ public static final int REPEAT_MODE_NONE = 0; /** * Playback of the current playing media item will be repeated. */ public static final int REPEAT_MODE_ONE = 1; /** * Playing media list will be repeated. */ public static final int REPEAT_MODE_ALL = 2; /** * Playback of the playing media group will be repeated. * A group is a logical block of media items which is specified in the section 5.7 of the * Bluetooth AVRCP 1.6. An example of a group is the playlist. */ public static final int REPEAT_MODE_GROUP = 3; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP}) @Retention(RetentionPolicy.SOURCE) public @interface ShuffleMode { } /** * Media list will be played in order. */ public static final int SHUFFLE_MODE_NONE = 0; /** * Media list will be played in shuffled order. */ public static final int SHUFFLE_MODE_ALL = 1; /** * Media group will be played in shuffled order. * A group is a logical block of media items which is specified in the section 5.7 of the * Bluetooth AVRCP 1.6. An example of a group is the playlist. */ public static final int SHUFFLE_MODE_GROUP = 2; /** * Value indicating the time is unknown */ public static final long UNKNOWN_TIME = Long.MIN_VALUE; /** * Media item index is invalid. This value will be returned when the corresponding media item * does not exist. */ public static final int INVALID_ITEM_INDEX = -1; private final Object mLock = new Object(); @GuardedBy("mLock") private final List> mCallbacks = new ArrayList<>(); /** * Starts or resumes playback. *

* On success, this transfers the player state to {@link #PLAYER_STATE_PLAYING} and * a {@link PlayerResult} should be returned with the current media item when the command * was completed. If it is called in {@link #PLAYER_STATE_IDLE} or {@link #PLAYER_STATE_ERROR}, * it should be ignored and a {@link PlayerResult} should be returned with * {@link PlayerResult#RESULT_ERROR_INVALID_STATE}. * * @return a {@link ListenableFuture} representing the pending completion of the command */ @NonNull public abstract ListenableFuture play(); /** * Pauses playback. *

* On success, this transfers the player state to {@link #PLAYER_STATE_PAUSED} and * a {@link PlayerResult} should be returned with the current media item when the command * was completed. If it is called in {@link #PLAYER_STATE_IDLE} or {@link #PLAYER_STATE_ERROR}, * it should be ignored and a {@link PlayerResult} should be returned with * {@link PlayerResult#RESULT_ERROR_INVALID_STATE}. * * @return a {@link ListenableFuture} representing the pending completion of the command */ @NonNull public abstract ListenableFuture pause(); /** * Prepares the media items for playback. During this time, the player may allocate resources * required to play, such as audio and video decoders. Before calling this API, set media * item(s) through either {@link #setMediaItem} or {@link #setPlaylist}. *

* On success, this transfers the player state from {@link #PLAYER_STATE_IDLE} to * {@link #PLAYER_STATE_PAUSED} and a {@link PlayerResult} should be returned with the prepared * media item when the command completed. If it's not called in {@link #PLAYER_STATE_IDLE}, * it should be ignored and {@link PlayerResult} should be returned with * {@link PlayerResult#RESULT_ERROR_INVALID_STATE}. * * @return a {@link ListenableFuture} representing the pending completion of the command */ @NonNull public abstract ListenableFuture prepare(); /** * Seeks to the specified position. *

* The position is the relative position based on the {@link MediaItem#getStartPosition()}. So * calling {@link #seekTo(long)} with {@code 0} means the seek to the start position. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. If it's called in {@link #PLAYER_STATE_IDLE}, it is ignored and * a {@link PlayerResult} should be returned with * {@link PlayerResult#RESULT_ERROR_INVALID_STATE}. * * @return a {@link ListenableFuture} representing the pending completion of the command * @param position the new playback position in ms. The value should be in the range of start * and end positions defined in {@link MediaItem}. */ @NonNull public abstract ListenableFuture seekTo(long position); /** * Sets the playback speed. The default playback speed is {@code 1.0f}, and negative values * indicate reverse playback and {@code 0.0f} is not allowed. *

* The supported playback speed range depends on the underlying player implementation, so it is * recommended to query the actual speed of the player via {@link #getPlaybackSpeed()} after the * operation completes. In particular, please note that player implementations may not support * reverse playback. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @param playbackSpeed the requested playback speed * @return a {@link ListenableFuture} representing the pending completion of the command * @see #getPlaybackSpeed() * @see PlayerCallback#onPlaybackSpeedChanged(SessionPlayer, float) */ @NonNull public abstract ListenableFuture setPlaybackSpeed(float playbackSpeed); /** * Sets the {@link AudioAttributesCompat} to be used during the playback of the media. *

* You must call this method in {@link #PLAYER_STATE_IDLE} in order for the audio attributes to * become effective thereafter. Otherwise, the call would be ignored and {@link PlayerResult} * should be returned with {@link PlayerResult#RESULT_ERROR_INVALID_STATE}. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @param attributes non-null AudioAttributes. */ @NonNull public abstract ListenableFuture setAudioAttributes( @NonNull AudioAttributesCompat attributes); /** * Gets the current player state. * * @return the current player state * @see PlayerCallback#onPlayerStateChanged(SessionPlayer, int) * @see #PLAYER_STATE_IDLE * @see #PLAYER_STATE_PAUSED * @see #PLAYER_STATE_PLAYING * @see #PLAYER_STATE_ERROR */ @PlayerState public abstract int getPlayerState(); /** * Gets the current playback position. *

* The position is the relative position based on the {@link MediaItem#getStartPosition()}. * So the position {@code 0} means the start position of the {@link MediaItem}. * * @return the current playback position in ms, or {@link #UNKNOWN_TIME} if unknown */ public abstract long getCurrentPosition(); /** * Gets the duration of the current media item, or {@link #UNKNOWN_TIME} if unknown. If the * current {@link MediaItem} has either start or end position, then duration would be adjusted * accordingly instead of returning the whole size of the {@link MediaItem}. * * @return the duration in ms, or {@link #UNKNOWN_TIME} if unknown */ public abstract long getDuration(); /** * Gets the position for how much has been buffered, or {@link #UNKNOWN_TIME} if unknown. *

* The position is the relative position based on the {@link MediaItem#getStartPosition()}. * So the position {@code 0} means the start position of the {@link MediaItem}. * * @return the buffered position in ms, or {@link #UNKNOWN_TIME} if unknown */ public abstract long getBufferedPosition(); /** * Returns the current buffering state of the player. *

* During the buffering, see {@link #getBufferedPosition()} for the quantifying the amount * already buffered. * * @return the buffering state, or {@link #BUFFERING_STATE_UNKNOWN} if unknown * @see #getBufferedPosition() */ @BuffState public abstract int getBufferingState(); /** * Gets the actual playback speed to be used by the player when playing. A value of {@code 1.0f} * is the default playback value, and a negative value indicates reverse playback. *

* Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}. * * @return the actual playback speed */ public abstract float getPlaybackSpeed(); /** * Gets the size of the video. * * @return the size of the video. The width and height of size could be 0 if there is no video * or the size has not been determined yet. * @see PlayerCallback#onVideoSizeChanged(SessionPlayer, VideoSize) */ @NonNull public VideoSize getVideoSize() { throw new UnsupportedOperationException("getVideoSize is not implemented"); } /** * Sets the {@link Surface} to be used as the sink for the video portion of the media. *

* A null surface will reset any Surface and result in only the audio track being played. *

* On success, a {@link SessionPlayer.PlayerResult} is returned with * the current media item when the command completed. * * @param surface the {@link Surface} to be used for the video portion of the media * @return a {@link ListenableFuture} which represents the pending completion of the command */ @NonNull public ListenableFuture setSurface(@Nullable Surface surface) { throw new UnsupportedOperationException("setSurface is not implemented"); } /** * Sets a list of {@link MediaItem} with metadata. Use this or {@link #setMediaItem} to specify * which items to play. *

* This can be called multiple times in any states other than {@link #PLAYER_STATE_ERROR}. This * would override previous {@link #setMediaItem} or {@link #setPlaylist} calls. *

* Ensure uniqueness of each {@link MediaItem} in the playlist so the session can uniquely * identity individual items. All {@link MediaItem}s shouldn't be {@code null} as well. *

* It's recommended to fill {@link MediaMetadata} in each {@link MediaItem} especially for the * duration information with the key {@link MediaMetadata#METADATA_KEY_DURATION}. Without the * duration information in the metadata, session will do extra work to get the duration and send * it to the controller. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistChanged} and {@link PlayerCallback#onCurrentMediaItemChanged} * when it's completed. The current media item would be the first item in the playlist. *

* The implementation must close the {@link ParcelFileDescriptor} in the {@link FileMediaItem} * when a media item in the playlist is a {@link FileMediaItem}. *

* On success, a {@link PlayerResult} should be returned with the first media item of the * playlist when the command completed. * * @param list a list of {@link MediaItem} objects to set as a play list * @throws IllegalArgumentException if the given list is {@code null} or empty, or has * duplicated media items. * @return a {@link ListenableFuture} which represents the pending completion of the command * @see #setMediaItem * @see PlayerCallback#onPlaylistChanged * @see PlayerCallback#onCurrentMediaItemChanged */ @NonNull public abstract ListenableFuture setPlaylist( @NonNull List list, @Nullable MediaMetadata metadata); /** * Gets the {@link AudioAttributesCompat} that media player has. */ @Nullable public abstract AudioAttributesCompat getAudioAttributes(); /** * Sets a {@link MediaItem} for playback. Use this or {@link #setPlaylist} to specify which * items to play. If you want to change current item in the playlist, use one of * {@link #skipToPlaylistItem}, {@link #skipToNextPlaylistItem}, or * {@link #skipToPreviousPlaylistItem} instead of this method. *

* This can be called multiple times in any states other than {@link #PLAYER_STATE_ERROR}. This * would override previous {@link #setMediaItem} or {@link #setPlaylist} calls. *

* It's recommended to fill {@link MediaMetadata} in {@link MediaItem} especially for the * duration information with the key {@link MediaMetadata#METADATA_KEY_DURATION}. Without the * duration information in the metadata, session will do extra work to get the duration and send * it to the controller. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistChanged} and {@link PlayerCallback#onCurrentMediaItemChanged} * when it's completed. The current item would be the item given here. *

* The implementation must close the {@link ParcelFileDescriptor} in the {@link FileMediaItem} * if the given media item is a {@link FileMediaItem}. *

* On success, a {@link PlayerResult} should be returned with {@code item} set. * * @param item the descriptor of media item you want to play * @return a {@link ListenableFuture} which represents the pending completion of the command * @see #setPlaylist * @see PlayerCallback#onPlaylistChanged * @see PlayerCallback#onCurrentMediaItemChanged * @throws IllegalArgumentException if the given item is {@code null}. */ @NonNull public abstract ListenableFuture setMediaItem( @NonNull MediaItem item); /** * Adds the media item to the playlist at the index. Index equals to or greater than * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of * the playlist. *

* If index is less than or equal to the current index of the playlist, * the current index of the playlist should be increased correspondingly. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} when it's * completed. *

* The implementation must close the {@link ParcelFileDescriptor} in the {@link FileMediaItem} * if the given media item is a {@link FileMediaItem}. *

* On success, a {@link PlayerResult} should be returned with {@code item} added. * * @param index the index of the item you want to add in the playlist * @param item the media item you want to add * @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata) */ @NonNull public abstract ListenableFuture addPlaylistItem(int index, @NonNull MediaItem item); /** * Removes the media item from the playlist *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} when it's * completed. *

* On success, a {@link PlayerResult} should be returned with {@code item} removed. *

* If the last item is removed, the player should be moved to {@link #PLAYER_STATE_IDLE}. * * @param index the index of the item you want to remove in the playlist * @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata) */ @NonNull public abstract ListenableFuture removePlaylistItem( @IntRange(from = 0) int index); /** * Replaces the media item at index in the playlist. This can be also used to update metadata of * an item. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} when it's * completed. *

* The implementation must close the {@link ParcelFileDescriptor} in the {@link FileMediaItem} * if the given media item is a {@link FileMediaItem}. *

* On success, a {@link PlayerResult} should be returned with {@code item} set. * * @param index the index of the item to replace in the playlist * @param item the new item * @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata) */ @NonNull public abstract ListenableFuture replacePlaylistItem(int index, @NonNull MediaItem item); /** * Moves the media item at {@code fromIdx} to {@code toIdx} in the playlist. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} when it's * completed. *

* On success, a {@link PlayerResult} should be returned with {@code item} set. * * @param fromIndex the media item's initial index in the playlist * @param toIndex the media item's target index in the playlist * @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata) */ @NonNull public ListenableFuture movePlaylistItem( @IntRange(from = 0) int fromIndex, @IntRange(from = 0) int toIndex) { throw new UnsupportedOperationException("movePlaylistItem is not implemented"); } /** * Skips to the previous item in the playlist. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} when it's * completed. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @return a {@link ListenableFuture} representing the pending completion of the command * @see PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem) */ @NonNull public abstract ListenableFuture skipToPreviousPlaylistItem(); /** * Skips to the next item in the playlist. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} when it's * completed. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @return a {@link ListenableFuture} representing the pending completion of the command * @see PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem) */ @NonNull public abstract ListenableFuture skipToNextPlaylistItem(); /** * Skips to the item in the playlist at the index. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} when it's * completed. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @param index the index of the item you want to play in the playlist * @see PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem) */ @NonNull public abstract ListenableFuture skipToPlaylistItem( @IntRange(from = 0) int index); /** * Updates the playlist metadata while keeping the playlist as-is. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onPlaylistMetadataChanged(SessionPlayer, MediaMetadata)} when it's * completed. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @param metadata metadata of the playlist * @see PlayerCallback#onPlaylistMetadataChanged(SessionPlayer, MediaMetadata) */ @NonNull public abstract ListenableFuture updatePlaylistMetadata( @Nullable MediaMetadata metadata); /** * Sets the repeat mode. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onRepeatModeChanged(SessionPlayer, int)} when it's completed. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @param repeatMode repeat mode * @see #REPEAT_MODE_NONE * @see #REPEAT_MODE_ONE * @see #REPEAT_MODE_ALL * @see #REPEAT_MODE_GROUP * @see PlayerCallback#onRepeatModeChanged(SessionPlayer, int) */ @NonNull public abstract ListenableFuture setRepeatMode( @RepeatMode int repeatMode); /** * Sets the shuffle mode. *

* The implementation must notify registered callbacks with * {@link PlayerCallback#onShuffleModeChanged(SessionPlayer, int)} when it's completed. *

* On success, a {@link PlayerResult} should be returned with the current media item when the * command completed. * * @param shuffleMode the shuffle mode * @return a {@link ListenableFuture} representing the pending completion of the command * @see #SHUFFLE_MODE_NONE * @see #SHUFFLE_MODE_ALL * @see #SHUFFLE_MODE_GROUP * @see PlayerCallback#onShuffleModeChanged(SessionPlayer, int) */ @NonNull public abstract ListenableFuture setShuffleMode( @ShuffleMode int shuffleMode); /** * Gets the playlist. It can be {@code null} if the playlist hasn't been set or it's reset by * {@link #setMediaItem}. * * @return playlist, or {@code null} * @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata) */ @Nullable public abstract List getPlaylist(); /** * Gets the playlist metadata. * * @return metadata metadata of the playlist, or null if none is set * @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata) * @see PlayerCallback#onPlaylistMetadataChanged(SessionPlayer, MediaMetadata) */ @Nullable public abstract MediaMetadata getPlaylistMetadata(); /** * Gets the repeat mode. * * @return repeat mode * @see #REPEAT_MODE_NONE * @see #REPEAT_MODE_ONE * @see #REPEAT_MODE_ALL * @see #REPEAT_MODE_GROUP * @see PlayerCallback#onRepeatModeChanged(SessionPlayer, int) */ @RepeatMode public abstract int getRepeatMode(); /** * Gets the shuffle mode. * * @return the shuffle mode * @see #SHUFFLE_MODE_NONE * @see #SHUFFLE_MODE_ALL * @see #SHUFFLE_MODE_GROUP * @see PlayerCallback#onShuffleModeChanged(SessionPlayer, int) */ @ShuffleMode public abstract int getShuffleMode(); /** * Gets the current media item, which is currently playing or would be played with later * {@link #play}. This value may be updated when * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is * called. * * @return the current media item. Can be {@code null} only when the player is in * {@link #PLAYER_STATE_IDLE} and a media item or playlist hasn't been set. * @see #setMediaItem * @see #setPlaylist */ @Nullable public abstract MediaItem getCurrentMediaItem(); /** * Gets the index of current media item in playlist. This value should be updated when * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is called. * * @return the index of current media item. Can be {@link #INVALID_ITEM_INDEX} when current * media item is null or not in the playlist, and when the playlist hasn't been set. */ @IntRange(from = INVALID_ITEM_INDEX) public abstract int getCurrentMediaItemIndex(); /** * Gets the previous item index in the playlist. This value should be updated when * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is called. * * @return the index of previous media item. Can be {@link #INVALID_ITEM_INDEX} only when * previous media item does not exist or playlist hasn't been set. */ @IntRange(from = INVALID_ITEM_INDEX) public abstract int getPreviousMediaItemIndex(); /** * Gets the next item index in the playlist. This value should be updated when * {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or * {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is called. * * @return the index of next media item. Can be {@link #INVALID_ITEM_INDEX} only when next media * item does not exist or playlist hasn't been set. */ @IntRange(from = INVALID_ITEM_INDEX) public abstract int getNextMediaItemIndex(); // Listeners / Callback related // Intentionally final not to allow developers to change the behavior /** * Register {@link PlayerCallback} to listen changes. * * @param executor a callback Executor * @param callback a PlayerCallback * @throws IllegalArgumentException if executor or callback is {@code null}. */ public final void registerPlayerCallback( @NonNull /*@CallbackExecutor*/ Executor executor, @NonNull PlayerCallback callback) { if (executor == null) { throw new NullPointerException("executor shouldn't be null"); } if (callback == null) { throw new NullPointerException("callback shouldn't be null"); } synchronized (mLock) { for (Pair pair : mCallbacks) { if (pair.first == callback && pair.second != null) { Log.w(TAG, "callback is already added. Ignoring."); return; } } mCallbacks.add(new Pair<>(callback, executor)); } } /** * Unregister the previously registered {@link PlayerCallback}. * * @param callback the callback to be removed * @throws IllegalArgumentException if the callback is {@code null}. */ public final void unregisterPlayerCallback(@NonNull PlayerCallback callback) { if (callback == null) { throw new NullPointerException("callback shouldn't be null"); } synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { if (mCallbacks.get(i).first == callback) { mCallbacks.remove(i); } } } } /** * Gets the callbacks with executors for subclasses to notify player events. * * @return map of callbacks and its executors */ @NonNull protected final List> getCallbacks() { List> list = new ArrayList<>(); synchronized (mLock) { list.addAll(mCallbacks); } return list; } /** * Gets the full list of selected and unselected tracks that the media contains. The order of * the list is irrelevant as different players expose tracks in different ways, but the tracks * will generally be ordered based on track type. *

* The types of tracks supported may vary based on player implementation. * * @return list of tracks. The total number of tracks is the size of the list. If empty, * the implementation should return an empty list instead of {@code null}. * @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO * @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO * @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE * @see TrackInfo#MEDIA_TRACK_TYPE_METADATA */ @NonNull public List getTracks() { throw new UnsupportedOperationException("getTracks is not implemented"); } /** * Selects the {@link TrackInfo} for the current media item. *

* Generally one track will be selected for each track type. *

* The types of tracks supported may vary based on player implementation. *

* Note: {@link #getTracks()} returns the list of tracks that can be selected, but the * list may be invalidated when {@link PlayerCallback#onTracksChanged(SessionPlayer, List)} * is called. * * @param trackInfo track to be selected * @return a {@link ListenableFuture} representing the pending completion of the command * @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO * @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO * @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE * @see TrackInfo#MEDIA_TRACK_TYPE_METADATA * @see PlayerCallback#onTrackSelected(SessionPlayer, TrackInfo) */ @NonNull public ListenableFuture selectTrack(@NonNull TrackInfo trackInfo) { throw new UnsupportedOperationException("selectTrack is not implemented"); } /** * Deselects the {@link TrackInfo} for the current media item. *

* Generally, a track should already be selected in order to be deselected, and audio and video * tracks should not be deselected. *

* The types of tracks supported may vary based on player implementation. *

* Note: {@link #getSelectedTrack(int)} returns the currently selected track per track type that * can be deselected, but the list may be invalidated when * {@link PlayerCallback#onTracksChanged(SessionPlayer, List)} is called. * * @param trackInfo track to be deselected * @return a {@link ListenableFuture} which represents the pending completion of the command * @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO * @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO * @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE * @see TrackInfo#MEDIA_TRACK_TYPE_METADATA * @see PlayerCallback#onTrackDeselected(SessionPlayer, TrackInfo) */ @NonNull public ListenableFuture deselectTrack(@NonNull TrackInfo trackInfo) { throw new UnsupportedOperationException("deselectTrack is not implemented"); } /** * Gets currently selected track's {@link TrackInfo} for the given track type. *

* The returned value can be outdated after * {@link PlayerCallback#onTracksChanged(SessionPlayer, List)}, * {@link PlayerCallback#onTrackSelected(SessionPlayer, TrackInfo)}, * or {@link PlayerCallback#onTrackDeselected(SessionPlayer, TrackInfo)} is called. * * @param trackType type of selected track * @return selected track info * @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO * @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO * @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE * @see TrackInfo#MEDIA_TRACK_TYPE_METADATA */ @Nullable public TrackInfo getSelectedTrack(@TrackInfo.MediaTrackType int trackType) { throw new UnsupportedOperationException( "getSelectedTrack is not implemented"); } /** * Removes all existing references to callbacks and executors. * * Note: Sub classes of {@link SessionPlayer} that override this API should call this super * method. */ @CallSuper @Override public void close() { synchronized (mLock) { mCallbacks.clear(); } } /** * Class for the player to return each audio/video/subtitle track's metadata. * * Note: TrackInfo holds a MediaFormat instance, but only the following key-values will be * supported when sending it over different processes: *

* * @see #getTracks */ @VersionedParcelize(isCustom = true) public static class TrackInfo extends CustomVersionedParcelable { public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; public static final int MEDIA_TRACK_TYPE_VIDEO = 1; public static final int MEDIA_TRACK_TYPE_AUDIO = 2; public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; public static final int MEDIA_TRACK_TYPE_METADATA = 5; private static final String KEY_IS_FORMAT_NULL = "androidx.media2.common.SessionPlayer.TrackInfo.KEY_IS_FORMAT_NULL"; private static final String KEY_IS_SELECTABLE = "androidx.media2.common.SessionPlayer.TrackInfo.KEY_IS_SELECTABLE"; /** * @hide */ @IntDef(flag = false, /*prefix = "MEDIA_TRACK_TYPE",*/ value = { MEDIA_TRACK_TYPE_UNKNOWN, MEDIA_TRACK_TYPE_VIDEO, MEDIA_TRACK_TYPE_AUDIO, MEDIA_TRACK_TYPE_SUBTITLE, MEDIA_TRACK_TYPE_METADATA, }) @Retention(RetentionPolicy.SOURCE) @RestrictTo(LIBRARY_GROUP) public @interface MediaTrackType {} @ParcelField(1) int mId; // Removed @ParcelField(2) @ParcelField(3) int mTrackType; // Parceled via mParcelableExtras. @NonParcelField @Nullable MediaFormat mFormat; // Parceled via mParcelableExtras. @NonParcelField boolean mIsSelectable; // For extra information containing MediaFormat and isSelectable data. Should only be used // by onPreParceling() and onPostParceling(). @ParcelField(4) Bundle mParcelableExtras; @NonParcelField private final Object mLock = new Object(); // WARNING: Adding a new ParcelField may break old library users (b/152830728) /** * Used for VersionedParcelable */ TrackInfo() { // no-op } /** * Constructor to create a TrackInfo instance. * * Note: The default value for {@link #isSelectable()} is false. * * @param id id of track unique across {@link MediaItem}s * @param type type of track. Can be video, audio or subtitle * @param format format of track */ public TrackInfo(int id, int type, @Nullable MediaFormat format) { this(id, type, format, /* isSelectable= */ false); } /** * Constructor to create a TrackInfo instance. * * @param id id of track unique across {@link MediaItem}s * @param type type of track. Can be video, audio or subtitle * @param format format of track * @param isSelectable whether track can be selected via * {@link SessionPlayer#selectTrack(TrackInfo)}. */ public TrackInfo(int id, int type, @Nullable MediaFormat format, boolean isSelectable) { mId = id; mTrackType = type; mFormat = format; mIsSelectable = isSelectable; } /** * Gets the track type. * @return MediaTrackType which indicates if the track is video, audio or subtitle */ @MediaTrackType public int getTrackType() { return mTrackType; } /** * Gets the language code of the track. * @return {@link Locale} which includes the language information */ @NonNull public Locale getLanguage() { String language = mFormat != null ? mFormat.getString(MediaFormat.KEY_LANGUAGE) : null; if (language == null) { language = "und"; } return new Locale(language); } /** * Gets the {@link MediaFormat} of the track. If the format is * unknown or could not be determined, null is returned. */ @Nullable public MediaFormat getFormat() { return mFormat; } /** * Gets the id of the track. * The id is used by {@link #selectTrack(TrackInfo)} and {@link #deselectTrack(TrackInfo)} * to identify the track to be (de)selected. * So, it's highly recommended to ensure that the id of each track is unique across * {@link MediaItem}s to avoid potential mis-selection when a stale {@link TrackInfo} is * used. * * @return id of the track */ public int getId() { return mId; } /** * Whether the current track can be selected via {@link #selectTrack(TrackInfo)} or not. * * @return true if the current track can be selected; false if otherwise. */ public boolean isSelectable() { return mIsSelectable; } @Override @NonNull public String toString() { StringBuilder out = new StringBuilder(128); out.append(getClass().getName()); out.append('#').append(mId); out.append('{'); switch (mTrackType) { case MEDIA_TRACK_TYPE_VIDEO: out.append("VIDEO"); break; case MEDIA_TRACK_TYPE_AUDIO: out.append("AUDIO"); break; case MEDIA_TRACK_TYPE_SUBTITLE: out.append("SUBTITLE"); break; case MEDIA_TRACK_TYPE_METADATA: out.append("METADATA"); break; default: out.append("UNKNOWN"); break; } out.append(", ").append(mFormat); out.append(", isSelectable=").append(mIsSelectable); out.append("}"); return out.toString(); } @Override public int hashCode() { return mId; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (!(obj instanceof TrackInfo)) { return false; } TrackInfo other = (TrackInfo) obj; return mId == other.mId; } /** * @hide * @param isStream */ @RestrictTo(LIBRARY) @Override public void onPreParceling(boolean isStream) { synchronized (mLock) { mParcelableExtras = new Bundle(); mParcelableExtras.putBoolean(KEY_IS_FORMAT_NULL, mFormat == null); if (mFormat != null) { putStringValueToBundle(MediaFormat.KEY_LANGUAGE, mFormat, mParcelableExtras); putStringValueToBundle(MediaFormat.KEY_MIME, mFormat, mParcelableExtras); putIntValueToBundle(MediaFormat.KEY_IS_FORCED_SUBTITLE, mFormat, mParcelableExtras); putIntValueToBundle(MediaFormat.KEY_IS_AUTOSELECT, mFormat, mParcelableExtras); putIntValueToBundle(MediaFormat.KEY_IS_DEFAULT, mFormat, mParcelableExtras); } mParcelableExtras.putBoolean(KEY_IS_SELECTABLE, mIsSelectable); } } /** * @hide */ @RestrictTo(LIBRARY) @Override public void onPostParceling() { if (mParcelableExtras != null && !mParcelableExtras.getBoolean(KEY_IS_FORMAT_NULL)) { mFormat = new MediaFormat(); setStringValueToMediaFormat(MediaFormat.KEY_LANGUAGE, mFormat, mParcelableExtras); setStringValueToMediaFormat(MediaFormat.KEY_MIME, mFormat, mParcelableExtras); setIntValueToMediaFormat(MediaFormat.KEY_IS_FORCED_SUBTITLE, mFormat, mParcelableExtras); setIntValueToMediaFormat(MediaFormat.KEY_IS_AUTOSELECT, mFormat, mParcelableExtras); setIntValueToMediaFormat(MediaFormat.KEY_IS_DEFAULT, mFormat, mParcelableExtras); } if (mParcelableExtras == null || !mParcelableExtras.containsKey(KEY_IS_SELECTABLE)) { mIsSelectable = mTrackType != MEDIA_TRACK_TYPE_VIDEO; } else { mIsSelectable = mParcelableExtras.getBoolean(KEY_IS_SELECTABLE); } } private static void putIntValueToBundle( String intValueKey, MediaFormat mediaFormat, Bundle bundle) { if (mediaFormat.containsKey(intValueKey)) { bundle.putInt(intValueKey, mediaFormat.getInteger(intValueKey)); } } private static void putStringValueToBundle( String stringValueKey, MediaFormat mediaFormat, Bundle bundle) { if (mediaFormat.containsKey(stringValueKey)) { bundle.putString(stringValueKey, mediaFormat.getString(stringValueKey)); } } private static void setIntValueToMediaFormat( String intValueKey, MediaFormat mediaFormat, Bundle bundle) { if (bundle.containsKey(intValueKey)) { mediaFormat.setInteger(intValueKey, bundle.getInt(intValueKey)); } } private static void setStringValueToMediaFormat( String stringValueKey, MediaFormat mediaFormat, Bundle bundle) { if (bundle.containsKey(stringValueKey)) { mediaFormat.setString(stringValueKey, bundle.getString(stringValueKey)); } } } /** * A callback class to receive notifications for events on the session player. See * {@link #registerPlayerCallback(Executor, PlayerCallback)} to register this callback. */ public abstract static class PlayerCallback { /** * Called when the state of the player has changed. * * @param player the player whose state has changed * @param playerState the new state of the player * @see #getPlayerState() */ public void onPlayerStateChanged(@NonNull SessionPlayer player, @PlayerState int playerState) { } /** * Called when a buffering events for a media item happened. * * @param player the player that is buffering * @param item the media item for which buffering is happening * @param buffState the new buffering state * @see #getBufferingState() */ public void onBufferingStateChanged(@NonNull SessionPlayer player, @Nullable MediaItem item, @BuffState int buffState) { } /** * Called when the playback speed has changed. * * @param player the player that has changed the playback speed * @param playbackSpeed the new playback speed * @see #getPlaybackSpeed() */ public void onPlaybackSpeedChanged(@NonNull SessionPlayer player, float playbackSpeed) { } /** * Called when {@link #seekTo(long)} is completed. * * @param player the player that has completed seeking * @param position the previous seeking request * @see #getCurrentPosition() */ public void onSeekCompleted(@NonNull SessionPlayer player, long position) { } /** * Called when a playlist is changed. It's also called after {@link #setPlaylist} or * {@link #setMediaItem}. * * @param player the player that has changed the playlist and playlist metadata * @param list new playlist * @param metadata new metadata * @see #getPlaylist() * @see #getPlaylistMetadata() */ public void onPlaylistChanged(@NonNull SessionPlayer player, @Nullable List list, @Nullable MediaMetadata metadata) { } /** * Called when a playlist metadata is changed. * * @param player the player that has changed the playlist metadata * @param metadata new metadata * @see #getPlaylistMetadata() */ public void onPlaylistMetadataChanged(@NonNull SessionPlayer player, @Nullable MediaMetadata metadata) { } /** * Called when the shuffle mode is changed. *

* {@link SessionPlayer#getPreviousMediaItemIndex()} and * {@link SessionPlayer#getNextMediaItemIndex()} values can be outdated when this callback * is called if the current media item is the first or last item in the playlist. * * @param player playlist agent for this event * @param shuffleMode shuffle mode * @see #SHUFFLE_MODE_NONE * @see #SHUFFLE_MODE_ALL * @see #SHUFFLE_MODE_GROUP * @see #getShuffleMode() */ public void onShuffleModeChanged(@NonNull SessionPlayer player, @ShuffleMode int shuffleMode) { } /** * Called when the repeat mode is changed. *

* {@link SessionPlayer#getPreviousMediaItemIndex()} and * {@link SessionPlayer#getNextMediaItemIndex()} values can be outdated when this callback * is called if the current media item is the first or last item in the playlist. * * @param player player for this event * @param repeatMode repeat mode * @see #REPEAT_MODE_NONE * @see #REPEAT_MODE_ONE * @see #REPEAT_MODE_ALL * @see #REPEAT_MODE_GROUP * @see #getRepeatMode() */ public void onRepeatModeChanged(@NonNull SessionPlayer player, @RepeatMode int repeatMode) { } /** * Called when the player's current media item has changed. Generally called after a new * media item is set through {@link #setPlaylist} or {@link #setMediaItem}, or after * skipping to a different item in a given playlist. * * @param player the player whose media item changed * @param item the new current media item. This can be {@code null} when the state of * the player becomes {@link #PLAYER_STATE_IDLE}. * @see #getCurrentMediaItem() */ public void onCurrentMediaItemChanged(@NonNull SessionPlayer player, @Nullable MediaItem item) { } /** * Called when the player finished playing. Playback state would be also set * {@link #PLAYER_STATE_PAUSED} with it. *

* This will be called only when the repeat mode is set to {@link #REPEAT_MODE_NONE}. * * @param player the player whose playback is completed * @see #REPEAT_MODE_NONE */ public void onPlaybackCompleted(@NonNull SessionPlayer player) { } /** * Called when the player's current audio attributes are changed. * * @param player the player whose audio attributes are changed * @param attributes the new current audio attributes * @see #getAudioAttributes() */ public void onAudioAttributesChanged(@NonNull SessionPlayer player, @Nullable AudioAttributesCompat attributes) { } /** * Called to indicate the video size *

* The video size (width and height) could be 0 if there was no video, * no display surface was set, or the value was not determined yet. *

* This callback is generally called when player updates video size, but will also be * called when {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} * is called. * * @param player the player associated with this callback * @param size the size of the video * @see #getVideoSize() */ public void onVideoSizeChanged(@NonNull SessionPlayer player, @NonNull VideoSize size) { } /** * Called when the player's subtitle track has new subtitle data available. * @param player the player that reports the new subtitle data * @param item the MediaItem of this media item * @param track the track that has the subtitle data * @param data the subtitle data */ public void onSubtitleData(@NonNull SessionPlayer player, @NonNull MediaItem item, @NonNull TrackInfo track, @NonNull SubtitleData data) { } /** * Called when the tracks of the current media item is changed such as * 1) when tracks of a media item become available, * 2) when new tracks are found during playback, or * 3) when the current media item is changed. *

* When it's called, you should invalidate previous track information and use the new * tracks to call {@link #selectTrack(TrackInfo)} or * {@link #deselectTrack(TrackInfo)}. * * @param player the player associated with this callback * @param tracks the list of tracks. It can be empty * @see #getTracks() */ public void onTracksChanged(@NonNull SessionPlayer player, @NonNull List tracks) { } /** * Called when a track is selected. * * @param player the player associated with this callback * @param trackInfo the selected track * @see #selectTrack(TrackInfo) */ public void onTrackSelected(@NonNull SessionPlayer player, @NonNull TrackInfo trackInfo) { } /** * Called when a track is deselected. *

* This callback will generally be called only after calling * {@link #deselectTrack(TrackInfo)}. * * @param player the player associated with this callback * @param trackInfo the deselected track * @see #deselectTrack(TrackInfo) */ public void onTrackDeselected(@NonNull SessionPlayer player, @NonNull TrackInfo trackInfo) { } } /** * Result class of the asynchronous APIs. *

* Subclass may extend this class for providing more result and/or custom result code. For the * custom result code, follow the convention below to avoid potential code duplication. *

*

*/ public static class PlayerResult implements BaseResult { /** * @hide */ @IntDef(flag = false, /*prefix = "RESULT",*/ value = { RESULT_SUCCESS, RESULT_ERROR_UNKNOWN, RESULT_ERROR_INVALID_STATE, RESULT_ERROR_BAD_VALUE, RESULT_ERROR_PERMISSION_DENIED, RESULT_ERROR_IO, RESULT_ERROR_NOT_SUPPORTED, RESULT_INFO_SKIPPED}) @Retention(RetentionPolicy.SOURCE) @RestrictTo(LIBRARY) public @interface ResultCode {} private final int mResultCode; private final long mCompletionTime; private final MediaItem mItem; /** * Constructor that uses the current system clock as the completion time. * * @param resultCode result code. Recommends to use the standard code defined here. * @param item media item when the command completed */ // Note: resultCode is intentionally not annotated for subclass to return extra error codes. public PlayerResult(int resultCode, @Nullable MediaItem item) { this(resultCode, item, SystemClock.elapsedRealtime()); } // Note: resultCode is intentionally not annotated for subclass to return extra error codes. private PlayerResult(int resultCode, @Nullable MediaItem item, long completionTime) { mResultCode = resultCode; mItem = item; mCompletionTime = completionTime; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) @NonNull public static ListenableFuture createFuture(int resultCode) { ResolvableFuture result = ResolvableFuture.create(); result.set(new PlayerResult(resultCode, null)); return result; } /** * Gets the result code. *

* Subclass of the {@link SessionPlayer} may have defined customized extra code other than * codes defined here. Check the documentation of the class that you're interested in. * * @return result code * @see #RESULT_ERROR_UNKNOWN * @see #RESULT_ERROR_INVALID_STATE * @see #RESULT_ERROR_BAD_VALUE * @see #RESULT_ERROR_PERMISSION_DENIED * @see #RESULT_ERROR_IO * @see #RESULT_ERROR_NOT_SUPPORTED * @see #RESULT_INFO_SKIPPED */ @Override @ResultCode public int getResultCode() { return mResultCode; } /** * Gets the completion time of the command. Being more specific, it's the same as * {@link android.os.SystemClock#elapsedRealtime()} when the command completed. * * @return completion time of the command */ @Override public long getCompletionTime() { return mCompletionTime; } /** * Gets the {@link MediaItem} for which the command was executed. In other words, this is * the item sent as an argument of the command if any, otherwise the current media item when * the command completed. * * @return media item when the command completed. Can be {@code null} for an error, or * the current media item was {@code null} */ @Override @Nullable public MediaItem getMediaItem() { return mItem; } } }