PlayerWrapper.java

/*
 * Copyright 2020 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.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_ID_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT;

import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.Nullable;
import androidx.media.VolumeProviderCompat;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.ForwardingPlayer;
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.Timeline;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.List;

/**
 * A wrapper of {@link Player} given by constructor of {@link MediaSession} or {@link
 * MediaSession#setPlayer(Player)}. Use this wrapper for extra checks before calling methods and/or
 * overriding the behavior.
 */
/* package */ class PlayerWrapper extends ForwardingPlayer {

  private static final int STATUS_CODE_SUCCESS_COMPAT = -1;

  private int legacyStatusCode;
  @Nullable private String legacyErrorMessage;
  @Nullable private Bundle legacyErrorExtras;
  private ImmutableList<CommandButton> customLayout;

  public PlayerWrapper(Player player) {
    super(player);
    legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
    customLayout = ImmutableList.of();
  }

  /**
   * Sets the legacy error code.
   *
   * <p>This sets the legacy {@link PlaybackStateCompat} to {@link PlaybackStateCompat#STATE_ERROR}
   * and calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, CharSequence)} and {@link
   * PlaybackStateCompat.Builder#setExtras(Bundle)} with the given arguments.
   *
   * <p>Use {@link #clearLegacyErrorStatus()} to clear the error state and to resume to the actual
   * playback state reflecting the player.
   *
   * @param errorCode The legacy error code.
   * @param errorMessage The legacy error message.
   * @param extras The extras.
   */
  public void setLegacyErrorStatus(int errorCode, String errorMessage, Bundle extras) {
    checkState(errorCode != STATUS_CODE_SUCCESS_COMPAT);
    legacyStatusCode = errorCode;
    legacyErrorMessage = errorMessage;
    legacyErrorExtras = extras;
  }

  /** Returns the legacy status code. */
  public int getLegacyStatusCode() {
    return legacyStatusCode;
  }

  /** Sets the custom layout. */
  public void setCustomLayout(ImmutableList<CommandButton> customLayout) {
    this.customLayout = customLayout;
  }

  /** Clears the legacy error status. */
  public void clearLegacyErrorStatus() {
    legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
    legacyErrorMessage = null;
    legacyErrorExtras = null;
  }

  @Override
  public void addListener(Listener listener) {
    verifyApplicationThread();
    super.addListener(listener);
  }

  @Override
  public void removeListener(Listener listener) {
    verifyApplicationThread();
    super.removeListener(listener);
  }

  @Override
  @Nullable
  public PlaybackException getPlayerError() {
    verifyApplicationThread();
    return super.getPlayerError();
  }

  @Override
  public void play() {
    verifyApplicationThread();
    super.play();
  }

  public void playIfCommandAvailable() {
    if (isCommandAvailable(COMMAND_PLAY_PAUSE)) {
      play();
    }
  }

  @Override
  public void pause() {
    verifyApplicationThread();
    super.pause();
  }

  @Override
  public void prepare() {
    verifyApplicationThread();
    super.prepare();
  }

  public void prepareIfCommandAvailable() {
    if (isCommandAvailable(COMMAND_PREPARE)) {
      prepare();
    }
  }

  @Override
  public void stop() {
    verifyApplicationThread();
    super.stop();
  }

  @Override
  public void release() {
    verifyApplicationThread();
    super.release();
  }

  @Override
  public void seekToDefaultPosition(int mediaItemIndex) {
    verifyApplicationThread();
    super.seekToDefaultPosition(mediaItemIndex);
  }

  @Override
  public void seekToDefaultPosition() {
    verifyApplicationThread();
    super.seekToDefaultPosition();
  }

  public void seekToDefaultPositionIfCommandAvailable() {
    if (isCommandAvailable(Player.COMMAND_SEEK_TO_DEFAULT_POSITION)) {
      seekToDefaultPosition();
    }
  }

  @Override
  public void seekTo(long positionMs) {
    verifyApplicationThread();
    super.seekTo(positionMs);
  }

  @Override
  public void seekTo(int mediaItemIndex, long positionMs) {
    verifyApplicationThread();
    super.seekTo(mediaItemIndex, positionMs);
  }

  @Override
  public long getSeekBackIncrement() {
    verifyApplicationThread();
    return super.getSeekBackIncrement();
  }

  @Override
  public void seekBack() {
    verifyApplicationThread();
    super.seekBack();
  }

  @Override
  public long getSeekForwardIncrement() {
    verifyApplicationThread();
    return super.getSeekForwardIncrement();
  }

  @Override
  public void seekForward() {
    verifyApplicationThread();
    super.seekForward();
  }

  @Override
  public void setPlaybackParameters(PlaybackParameters playbackParameters) {
    verifyApplicationThread();
    super.setPlaybackParameters(playbackParameters);
  }

  @Override
  public void setPlaybackSpeed(float playbackSpeed) {
    verifyApplicationThread();
    super.setPlaybackSpeed(playbackSpeed);
  }

  @Override
  public long getCurrentPosition() {
    verifyApplicationThread();
    return super.getCurrentPosition();
  }

  @Override
  public long getDuration() {
    verifyApplicationThread();
    return super.getDuration();
  }

  public long getDurationWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) ? getDuration() : C.TIME_UNSET;
  }

  @Override
  public long getBufferedPosition() {
    verifyApplicationThread();
    return super.getBufferedPosition();
  }

  @Override
  public int getBufferedPercentage() {
    verifyApplicationThread();
    return super.getBufferedPercentage();
  }

  @Override
  public long getTotalBufferedDuration() {
    verifyApplicationThread();
    return super.getTotalBufferedDuration();
  }

  @Override
  public long getCurrentLiveOffset() {
    verifyApplicationThread();
    return super.getCurrentLiveOffset();
  }

  @Override
  public long getContentDuration() {
    verifyApplicationThread();
    return super.getContentDuration();
  }

  @Override
  public long getContentPosition() {
    verifyApplicationThread();
    return super.getContentPosition();
  }

  @Override
  public long getContentBufferedPosition() {
    verifyApplicationThread();
    return super.getContentBufferedPosition();
  }

  @Override
  public boolean isPlayingAd() {
    verifyApplicationThread();
    return super.isPlayingAd();
  }

  @Override
  public int getCurrentAdGroupIndex() {
    verifyApplicationThread();
    return super.getCurrentAdGroupIndex();
  }

  @Override
  public int getCurrentAdIndexInAdGroup() {
    verifyApplicationThread();
    return super.getCurrentAdIndexInAdGroup();
  }

  @Override
  public PlaybackParameters getPlaybackParameters() {
    verifyApplicationThread();
    return super.getPlaybackParameters();
  }

  @Override
  public VideoSize getVideoSize() {
    verifyApplicationThread();
    return super.getVideoSize();
  }

  @Override
  public void clearVideoSurface() {
    verifyApplicationThread();
    super.clearVideoSurface();
  }

  @Override
  public void clearVideoSurface(@Nullable Surface surface) {
    verifyApplicationThread();
    super.clearVideoSurface(surface);
  }

  @Override
  public void setVideoSurface(@Nullable Surface surface) {
    verifyApplicationThread();
    super.setVideoSurface(surface);
  }

  @Override
  public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
    verifyApplicationThread();
    super.setVideoSurfaceHolder(surfaceHolder);
  }

  @Override
  public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
    verifyApplicationThread();
    super.clearVideoSurfaceHolder(surfaceHolder);
  }

  @Override
  public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {
    verifyApplicationThread();
    super.setVideoSurfaceView(surfaceView);
  }

  @Override
  public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {
    verifyApplicationThread();
    super.clearVideoSurfaceView(surfaceView);
  }

  @Override
  public void setVideoTextureView(@Nullable TextureView textureView) {
    verifyApplicationThread();
    super.setVideoTextureView(textureView);
  }

  @Override
  public void clearVideoTextureView(@Nullable TextureView textureView) {
    verifyApplicationThread();
    super.clearVideoTextureView(textureView);
  }

  @Override
  public AudioAttributes getAudioAttributes() {
    verifyApplicationThread();
    return super.getAudioAttributes();
  }

  public AudioAttributes getAudioAttributesWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)
        ? getAudioAttributes()
        : AudioAttributes.DEFAULT;
  }

  @Override
  public void setMediaItem(MediaItem mediaItem) {
    verifyApplicationThread();
    super.setMediaItem(mediaItem);
  }

  @Override
  public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
    verifyApplicationThread();
    super.setMediaItem(mediaItem, startPositionMs);
  }

  @Override
  public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
    verifyApplicationThread();
    super.setMediaItem(mediaItem, resetPosition);
  }

  @Override
  public void setMediaItems(List<MediaItem> mediaItems) {
    verifyApplicationThread();
    super.setMediaItems(mediaItems);
  }

  @Override
  public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
    verifyApplicationThread();
    super.setMediaItems(mediaItems, resetPosition);
  }

  @Override
  public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
    verifyApplicationThread();
    super.setMediaItems(mediaItems, startIndex, startPositionMs);
  }

  @Override
  public void addMediaItem(MediaItem mediaItem) {
    verifyApplicationThread();
    super.addMediaItem(mediaItem);
  }

  @Override
  public void addMediaItem(int index, MediaItem mediaItem) {
    verifyApplicationThread();
    super.addMediaItem(index, mediaItem);
  }

  @Override
  public void addMediaItems(List<MediaItem> mediaItems) {
    verifyApplicationThread();
    super.addMediaItems(mediaItems);
  }

  @Override
  public void addMediaItems(int index, List<MediaItem> mediaItems) {
    verifyApplicationThread();
    super.addMediaItems(index, mediaItems);
  }

  @Override
  public void clearMediaItems() {
    verifyApplicationThread();
    super.clearMediaItems();
  }

  @Override
  public void removeMediaItem(int index) {
    verifyApplicationThread();
    super.removeMediaItem(index);
  }

  @Override
  public void removeMediaItems(int fromIndex, int toIndex) {
    verifyApplicationThread();
    super.removeMediaItems(fromIndex, toIndex);
  }

  @Override
  public void moveMediaItem(int currentIndex, int newIndex) {
    verifyApplicationThread();
    super.moveMediaItem(currentIndex, newIndex);
  }

  @Override
  public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
    verifyApplicationThread();
    super.moveMediaItems(fromIndex, toIndex, newIndex);
  }

  @Deprecated
  @Override
  public boolean hasPrevious() {
    verifyApplicationThread();
    return super.hasPrevious();
  }

  @Deprecated
  @Override
  public boolean hasNext() {
    verifyApplicationThread();
    return super.hasNext();
  }

  @Deprecated
  @Override
  public boolean hasPreviousWindow() {
    verifyApplicationThread();
    return super.hasPreviousWindow();
  }

  @Deprecated
  @Override
  public boolean hasNextWindow() {
    verifyApplicationThread();
    return super.hasNextWindow();
  }

  @Override
  public boolean hasPreviousMediaItem() {
    verifyApplicationThread();
    return super.hasPreviousMediaItem();
  }

  @Override
  public boolean hasNextMediaItem() {
    verifyApplicationThread();
    return super.hasNextMediaItem();
  }

  @Deprecated
  @Override
  public void previous() {
    verifyApplicationThread();
    super.previous();
  }

  @Deprecated
  @Override
  public void next() {
    verifyApplicationThread();
    super.next();
  }

  @Deprecated
  @Override
  public void seekToPreviousWindow() {
    verifyApplicationThread();
    super.seekToPreviousWindow();
  }

  @Deprecated
  @Override
  public void seekToNextWindow() {
    verifyApplicationThread();
    super.seekToNextWindow();
  }

  @Override
  public void seekToPreviousMediaItem() {
    verifyApplicationThread();
    super.seekToPreviousMediaItem();
  }

  @Override
  public void seekToNextMediaItem() {
    verifyApplicationThread();
    super.seekToNextMediaItem();
  }

  @Override
  public void setPlaylistMetadata(MediaMetadata playlistMetadata) {
    verifyApplicationThread();
    super.setPlaylistMetadata(playlistMetadata);
  }

  @Override
  public void setRepeatMode(int repeatMode) {
    verifyApplicationThread();
    super.setRepeatMode(repeatMode);
  }

  @Override
  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
    verifyApplicationThread();
    super.setShuffleModeEnabled(shuffleModeEnabled);
  }

  @Override
  public Timeline getCurrentTimeline() {
    verifyApplicationThread();
    return super.getCurrentTimeline();
  }

  public Timeline getCurrentTimelineWithCommandCheck() {
    if (isCommandAvailable(COMMAND_GET_TIMELINE)) {
      return getCurrentTimeline();
    } else if (isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) {
      return new CurrentMediaItemOnlyTimeline(this);
    }
    return Timeline.EMPTY;
  }

  @Override
  public MediaMetadata getPlaylistMetadata() {
    verifyApplicationThread();
    return super.getPlaylistMetadata();
  }

  public MediaMetadata getPlaylistMetadataWithCommandCheck() {
    return isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)
        ? getPlaylistMetadata()
        : MediaMetadata.EMPTY;
  }

  @Override
  public int getRepeatMode() {
    verifyApplicationThread();
    return super.getRepeatMode();
  }

  @Override
  public boolean getShuffleModeEnabled() {
    verifyApplicationThread();
    return super.getShuffleModeEnabled();
  }

  @Override
  @Nullable
  public MediaItem getCurrentMediaItem() {
    verifyApplicationThread();
    return super.getCurrentMediaItem();
  }

  @Nullable
  public MediaItem getCurrentMediaItemWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) ? getCurrentMediaItem() : null;
  }

  @Override
  public int getMediaItemCount() {
    verifyApplicationThread();
    return super.getMediaItemCount();
  }

  @Override
  public MediaItem getMediaItemAt(int index) {
    verifyApplicationThread();
    return super.getMediaItemAt(index);
  }

  @Deprecated
  @Override
  public int getCurrentWindowIndex() {
    verifyApplicationThread();
    return super.getCurrentWindowIndex();
  }

  @Override
  public int getCurrentMediaItemIndex() {
    verifyApplicationThread();
    return super.getCurrentMediaItemIndex();
  }

  @Deprecated
  @Override
  public int getPreviousWindowIndex() {
    verifyApplicationThread();
    return super.getPreviousWindowIndex();
  }

  @Override
  public int getPreviousMediaItemIndex() {
    verifyApplicationThread();
    return super.getPreviousMediaItemIndex();
  }

  @Deprecated
  @Override
  public int getNextWindowIndex() {
    verifyApplicationThread();
    return super.getNextWindowIndex();
  }

  @Override
  public int getNextMediaItemIndex() {
    verifyApplicationThread();
    return super.getNextMediaItemIndex();
  }

  @Override
  public float getVolume() {
    verifyApplicationThread();
    return super.getVolume();
  }

  public float getVolumeWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_VOLUME) ? getVolume() : 0;
  }

  @Override
  public void setVolume(float volume) {
    verifyApplicationThread();
    super.setVolume(volume);
  }

  @Override
  public CueGroup getCurrentCues() {
    verifyApplicationThread();
    return super.getCurrentCues();
  }

  public CueGroup getCurrentCuesWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_TEXT) ? getCurrentCues() : CueGroup.EMPTY_TIME_ZERO;
  }

  @Override
  public DeviceInfo getDeviceInfo() {
    verifyApplicationThread();
    return super.getDeviceInfo();
  }

  @Override
  public int getDeviceVolume() {
    verifyApplicationThread();
    return super.getDeviceVolume();
  }

  public int getDeviceVolumeWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_DEVICE_VOLUME) ? getDeviceVolume() : 0;
  }

  @Override
  public boolean isDeviceMuted() {
    verifyApplicationThread();
    return super.isDeviceMuted();
  }

  public boolean isDeviceMutedWithCommandCheck() {
    return isCommandAvailable(Player.COMMAND_GET_DEVICE_VOLUME) && isDeviceMuted();
  }

  @Override
  public void setDeviceVolume(int volume) {
    verifyApplicationThread();
    super.setDeviceVolume(volume);
  }

  public void setDeviceVolumeIfCommandAvailable(int volume) {
    if (isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)) {
      setDeviceVolume(volume);
    }
  }

  @Override
  public void increaseDeviceVolume() {
    verifyApplicationThread();
    super.increaseDeviceVolume();
  }

  @Override
  public void decreaseDeviceVolume() {
    verifyApplicationThread();
    super.decreaseDeviceVolume();
  }

  @Override
  public void setDeviceMuted(boolean muted) {
    verifyApplicationThread();
    super.setDeviceMuted(muted);
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    verifyApplicationThread();
    super.setPlayWhenReady(playWhenReady);
  }

  @Override
  public boolean getPlayWhenReady() {
    verifyApplicationThread();
    return super.getPlayWhenReady();
  }

  @Override
  @PlaybackSuppressionReason
  public int getPlaybackSuppressionReason() {
    verifyApplicationThread();
    return super.getPlaybackSuppressionReason();
  }

  @Override
  @State
  public int getPlaybackState() {
    verifyApplicationThread();
    return super.getPlaybackState();
  }

  @Override
  public boolean isPlaying() {
    verifyApplicationThread();
    return super.isPlaying();
  }

  @Override
  public boolean isLoading() {
    verifyApplicationThread();
    return super.isLoading();
  }

  @Override
  public MediaMetadata getMediaMetadata() {
    verifyApplicationThread();
    return super.getMediaMetadata();
  }

  public MediaMetadata getMediaMetadataWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)
        ? getMediaMetadata()
        : MediaMetadata.EMPTY;
  }

  @Override
  public boolean isCommandAvailable(@Command int command) {
    verifyApplicationThread();
    return super.isCommandAvailable(command);
  }

  @Override
  public Commands getAvailableCommands() {
    verifyApplicationThread();
    return super.getAvailableCommands();
  }

  @Override
  public TrackSelectionParameters getTrackSelectionParameters() {
    verifyApplicationThread();
    return super.getTrackSelectionParameters();
  }

  @Override
  public void setTrackSelectionParameters(TrackSelectionParameters parameters) {
    verifyApplicationThread();
    super.setTrackSelectionParameters(parameters);
  }

  @Override
  public void seekToPrevious() {
    verifyApplicationThread();
    super.seekToPrevious();
  }

  @Override
  public long getMaxSeekToPreviousPosition() {
    verifyApplicationThread();
    return super.getMaxSeekToPreviousPosition();
  }

  @Override
  public void seekToNext() {
    verifyApplicationThread();
    super.seekToNext();
  }

  @Override
  public Tracks getCurrentTracks() {
    verifyApplicationThread();
    return super.getCurrentTracks();
  }

  public Tracks getCurrentTracksWithCommandCheck() {
    return isCommandAvailable(COMMAND_GET_TRACKS) ? getCurrentTracks() : Tracks.EMPTY;
  }

  @Nullable
  @Override
  public Object getCurrentManifest() {
    verifyApplicationThread();
    return super.getCurrentManifest();
  }

  @Override
  public int getCurrentPeriodIndex() {
    verifyApplicationThread();
    return super.getCurrentPeriodIndex();
  }

  @Override
  public boolean isCurrentMediaItemDynamic() {
    verifyApplicationThread();
    return super.isCurrentMediaItemDynamic();
  }

  @Override
  public boolean isCurrentMediaItemLive() {
    verifyApplicationThread();
    return super.isCurrentMediaItemLive();
  }

  @Override
  public boolean isCurrentMediaItemSeekable() {
    verifyApplicationThread();
    return super.isCurrentMediaItemSeekable();
  }

  @Override
  public Size getSurfaceSize() {
    verifyApplicationThread();
    return super.getSurfaceSize();
  }

  public PlaybackStateCompat createPlaybackStateCompat() {
    if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) {
      return new PlaybackStateCompat.Builder()
          .setState(
              PlaybackStateCompat.STATE_ERROR,
              /* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN,
              /* playbackSpeed= */ 0,
              /* updateTime= */ SystemClock.elapsedRealtime())
          .setActions(0)
          .setBufferedPosition(0)
          .setErrorMessage(legacyStatusCode, checkNotNull(legacyErrorMessage))
          .setExtras(checkNotNull(legacyErrorExtras))
          .build();
    }
    @Nullable PlaybackException playerError = getPlayerError();
    int state =
        MediaUtils.convertToPlaybackStateCompatState(
            playerError, getPlaybackState(), getPlayWhenReady());
    // Always advertise ACTION_SET_RATING.
    long actions = PlaybackStateCompat.ACTION_SET_RATING;
    Commands availableCommands = getAvailableCommands();
    for (int i = 0; i < availableCommands.size(); i++) {
      actions |= convertCommandToPlaybackStateActions(availableCommands.get(i));
    }
    long queueItemId =
        isCommandAvailable(COMMAND_GET_TIMELINE)
            ? MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex())
            : MediaSessionCompat.QueueItem.UNKNOWN_ID;
    float playbackSpeed = getPlaybackParameters().speed;
    float sessionPlaybackSpeed = isPlaying() ? playbackSpeed : 0f;
    Bundle extras = new Bundle();
    extras.putFloat(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT, playbackSpeed);
    @Nullable MediaItem currentMediaItem = getCurrentMediaItemWithCommandCheck();
    if (currentMediaItem != null && !MediaItem.DEFAULT_MEDIA_ID.equals(currentMediaItem.mediaId)) {
      extras.putString(EXTRAS_KEY_MEDIA_ID_COMPAT, currentMediaItem.mediaId);
    }
    boolean canReadPositions = isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM);
    long compatPosition =
        canReadPositions ? getCurrentPosition() : PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN;
    long compatBufferedPosition = canReadPositions ? getBufferedPosition() : 0;
    PlaybackStateCompat.Builder builder =
        new PlaybackStateCompat.Builder()
            .setState(state, compatPosition, sessionPlaybackSpeed, SystemClock.elapsedRealtime())
            .setActions(actions)
            .setActiveQueueItemId(queueItemId)
            .setBufferedPosition(compatBufferedPosition)
            .setExtras(extras);

    for (int i = 0; i < customLayout.size(); i++) {
      CommandButton commandButton = customLayout.get(i);
      if (commandButton.sessionCommand != null) {
        SessionCommand sessionCommand = commandButton.sessionCommand;
        if (sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
          builder.addCustomAction(
              new PlaybackStateCompat.CustomAction.Builder(
                      sessionCommand.customAction,
                      commandButton.displayName,
                      commandButton.iconResId)
                  .setExtras(sessionCommand.customExtras)
                  .build());
        }
      }
    }
    if (playerError != null) {
      builder.setErrorMessage(
          PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, Util.castNonNull(playerError.getMessage()));
    }
    return builder.build();
  }

  @Nullable
  public VolumeProviderCompat createVolumeProviderCompat() {
    if (getDeviceInfo().playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) {
      return null;
    }
    Commands availableCommands = getAvailableCommands();
    int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
    if (availableCommands.contains(COMMAND_ADJUST_DEVICE_VOLUME)) {
      volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_RELATIVE;
      if (availableCommands.contains(COMMAND_SET_DEVICE_VOLUME)) {
        volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
      }
    }
    Handler handler = new Handler(getApplicationLooper());
    int currentVolume = getDeviceVolumeWithCommandCheck();
    return new VolumeProviderCompat(volumeControlType, getDeviceInfo().maxVolume, currentVolume) {
      @Override
      public void onSetVolumeTo(int volume) {
        postOrRun(handler, () -> setDeviceVolumeIfCommandAvailable(volume));
      }

      @Override
      public void onAdjustVolume(int direction) {
        postOrRun(
            handler,
            () -> {
              if (!isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)) {
                return;
              }
              switch (direction) {
                case AudioManager.ADJUST_RAISE:
                  increaseDeviceVolume();
                  break;
                case AudioManager.ADJUST_LOWER:
                  decreaseDeviceVolume();
                  break;
                case AudioManager.ADJUST_MUTE:
                  setDeviceMuted(true);
                  break;
                case AudioManager.ADJUST_UNMUTE:
                  setDeviceMuted(false);
                  break;
                case AudioManager.ADJUST_TOGGLE_MUTE:
                  setDeviceMuted(!isDeviceMutedWithCommandCheck());
                  break;
                default:
                  Log.w(
                      "VolumeProviderCompat",
                      "onAdjustVolume: Ignoring unknown direction: " + direction);
                  break;
              }
            });
      }
    };
  }

  /**
   * Creates a {@link PositionInfo} of this player for Bundling.
   *
   * <p>This excludes window uid and period uid that wouldn't be preserved when bundling.
   */
  public PositionInfo createPositionInfoForBundling() {
    boolean canAccessCurrentMediaItem = isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM);
    boolean canAccessTimeline = isCommandAvailable(COMMAND_GET_TIMELINE);
    return new PositionInfo(
        /* windowUid= */ null,
        canAccessTimeline ? getCurrentMediaItemIndex() : 0,
        canAccessCurrentMediaItem ? getCurrentMediaItem() : null,
        /* periodUid= */ null,
        canAccessTimeline ? getCurrentPeriodIndex() : 0,
        canAccessCurrentMediaItem ? getCurrentPosition() : 0,
        canAccessCurrentMediaItem ? getContentPosition() : 0,
        canAccessCurrentMediaItem ? getCurrentAdGroupIndex() : C.INDEX_UNSET,
        canAccessCurrentMediaItem ? getCurrentAdIndexInAdGroup() : C.INDEX_UNSET);
  }

  /**
   * Creates a {@link SessionPositionInfo} of this player for Bundling.
   *
   * <p>This excludes window uid and period uid that wouldn't be preserved when bundling.
   */
  public SessionPositionInfo createSessionPositionInfoForBundling() {
    boolean canAccessCurrentMediaItem = isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM);
    return new SessionPositionInfo(
        createPositionInfoForBundling(),
        canAccessCurrentMediaItem && isPlayingAd(),
        /* eventTimeMs= */ SystemClock.elapsedRealtime(),
        canAccessCurrentMediaItem ? getDuration() : C.TIME_UNSET,
        canAccessCurrentMediaItem ? getBufferedPosition() : 0,
        canAccessCurrentMediaItem ? getBufferedPercentage() : 0,
        canAccessCurrentMediaItem ? getTotalBufferedDuration() : 0,
        canAccessCurrentMediaItem ? getCurrentLiveOffset() : C.TIME_UNSET,
        canAccessCurrentMediaItem ? getContentDuration() : C.TIME_UNSET,
        canAccessCurrentMediaItem ? getContentBufferedPosition() : 0);
  }

  public PlayerInfo createPlayerInfoForBundling() {
    return new PlayerInfo(
        getPlayerError(),
        PlayerInfo.MEDIA_ITEM_TRANSITION_REASON_DEFAULT,
        createSessionPositionInfoForBundling(),
        createPositionInfoForBundling(),
        createPositionInfoForBundling(),
        PlayerInfo.DISCONTINUITY_REASON_DEFAULT,
        getPlaybackParameters(),
        getRepeatMode(),
        getShuffleModeEnabled(),
        getVideoSize(),
        getCurrentTimelineWithCommandCheck(),
        getPlaylistMetadataWithCommandCheck(),
        getVolumeWithCommandCheck(),
        getAudioAttributesWithCommandCheck(),
        getCurrentCuesWithCommandCheck(),
        getDeviceInfo(),
        getDeviceVolumeWithCommandCheck(),
        isDeviceMutedWithCommandCheck(),
        getPlayWhenReady(),
        PlayerInfo.PLAY_WHEN_READY_CHANGE_REASON_DEFAULT,
        getPlaybackSuppressionReason(),
        getPlaybackState(),
        isPlaying(),
        isLoading(),
        getMediaMetadataWithCommandCheck(),
        getSeekBackIncrement(),
        getSeekForwardIncrement(),
        getMaxSeekToPreviousPosition(),
        getCurrentTracksWithCommandCheck(),
        getTrackSelectionParameters());
  }

  private void verifyApplicationThread() {
    checkState(Looper.myLooper() == getApplicationLooper());
  }

  @SuppressWarnings("deprecation") // Uses deprecated PlaybackStateCompat actions.
  private static long convertCommandToPlaybackStateActions(@Command int command) {
    switch (command) {
      case Player.COMMAND_PLAY_PAUSE:
        return PlaybackStateCompat.ACTION_PAUSE
            | PlaybackStateCompat.ACTION_PLAY
            | PlaybackStateCompat.ACTION_PLAY_PAUSE;
      case Player.COMMAND_PREPARE:
        return PlaybackStateCompat.ACTION_PREPARE;
      case Player.COMMAND_SEEK_BACK:
        return PlaybackStateCompat.ACTION_REWIND;
      case Player.COMMAND_SEEK_FORWARD:
        return PlaybackStateCompat.ACTION_FAST_FORWARD;
      case Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM:
        return PlaybackStateCompat.ACTION_SEEK_TO;
      case Player.COMMAND_SEEK_TO_MEDIA_ITEM:
        return PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
      case Player.COMMAND_SEEK_TO_NEXT:
      case Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM:
        return PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
      case Player.COMMAND_SEEK_TO_PREVIOUS:
      case Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM:
        return PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
      case Player.COMMAND_SET_MEDIA_ITEM:
        return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
            | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
            | PlaybackStateCompat.ACTION_PLAY_FROM_URI
            | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
            | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
            | PlaybackStateCompat.ACTION_PREPARE_FROM_URI;
      case Player.COMMAND_SET_REPEAT_MODE:
        return PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
      case Player.COMMAND_SET_SPEED_AND_PITCH:
        return PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED;
      case Player.COMMAND_SET_SHUFFLE_MODE:
        return PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
            | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED;
      case Player.COMMAND_STOP:
        return PlaybackStateCompat.ACTION_STOP;
      case Player.COMMAND_ADJUST_DEVICE_VOLUME:
      case Player.COMMAND_CHANGE_MEDIA_ITEMS:
        // TODO(b/227346735): Handle this through
        // MediaSessionCompat.setFlags(FLAG_HANDLES_QUEUE_COMMANDS)
      case Player.COMMAND_GET_AUDIO_ATTRIBUTES:
      case Player.COMMAND_GET_CURRENT_MEDIA_ITEM:
      case Player.COMMAND_GET_DEVICE_VOLUME:
      case Player.COMMAND_GET_MEDIA_ITEMS_METADATA:
      case Player.COMMAND_GET_TEXT:
      case Player.COMMAND_GET_TIMELINE:
      case Player.COMMAND_GET_TRACKS:
      case Player.COMMAND_GET_VOLUME:
      case Player.COMMAND_INVALID:
      case Player.COMMAND_SEEK_TO_DEFAULT_POSITION:
      case Player.COMMAND_SET_DEVICE_VOLUME:
      case Player.COMMAND_SET_MEDIA_ITEMS_METADATA:
      case Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS:
      case Player.COMMAND_SET_VIDEO_SURFACE:
      case Player.COMMAND_SET_VOLUME:
      default:
        return 0;
    }
  }

  private static final class CurrentMediaItemOnlyTimeline extends Timeline {

    private static final Object UID = new Object();

    @Nullable private final MediaItem mediaItem;
    private final boolean isSeekable;
    private final boolean isDynamic;
    @Nullable private final MediaItem.LiveConfiguration liveConfiguration;
    private final long durationUs;

    public CurrentMediaItemOnlyTimeline(PlayerWrapper player) {
      mediaItem = player.getCurrentMediaItem();
      isSeekable = player.isCurrentMediaItemSeekable();
      isDynamic = player.isCurrentMediaItemDynamic();
      liveConfiguration =
          player.isCurrentMediaItemLive() ? MediaItem.LiveConfiguration.UNSET : null;
      durationUs = msToUs(player.getContentDuration());
    }

    @Override
    public int getWindowCount() {
      return 1;
    }

    @Override
    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
      window.set(
          UID,
          mediaItem,
          /* manifest= */ null,
          /* presentationStartTimeMs= */ C.TIME_UNSET,
          /* windowStartTimeMs= */ C.TIME_UNSET,
          /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
          isSeekable,
          isDynamic,
          liveConfiguration,
          /* defaultPositionUs= */ 0,
          durationUs,
          /* firstPeriodIndex= */ 0,
          /* lastPeriodIndex= */ 0,
          /* positionInFirstPeriodUs= */ 0);
      return window;
    }

    @Override
    public int getPeriodCount() {
      return 1;
    }

    @Override
    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
      period.set(
          /* id= */ UID,
          /* uid= */ UID,
          /* windowIndex= */ 0,
          durationUs,
          /* positionInWindowUs= */ 0);
      return period;
    }

    @Override
    public int getIndexOfPeriod(Object uid) {
      return UID.equals(uid) ? 0 : C.INDEX_UNSET;
    }

    @Override
    public Object getUidOfPeriod(int periodIndex) {
      return UID;
    }
  }
}