MediaUtils.java

/*
 * 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 android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS;
import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS;
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES;
import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_GET_METADATA;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_RELEASE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media.AudioAttributesCompat;
import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
import androidx.media.VolumeProviderCompat;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.HeartRating;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PercentageRating;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Command;
import androidx.media3.common.Player.Commands;
import androidx.media3.common.Rating;
import androidx.media3.common.StarRating;
import androidx.media3.common.ThumbRating;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.PlayerInfo.BundlingExclusions;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/* package */ final class MediaUtils {

  public static final String TAG = "MediaUtils";
  public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 256 * 1024; // 256KB
  /** Constant to identify whether two calculated positions are considered as same */
  public static final long POSITION_DIFF_TOLERANCE_MS = 100;

  // Stub BrowserRoot for accepting any connection here.
  public static final BrowserRoot defaultBrowserRoot =
      new BrowserRoot(MediaLibraryService.SERVICE_INTERFACE, null);

  /** Returns whether two {@link PlaybackStateCompat} have equal error. */
  public static boolean areEqualError(
      @Nullable PlaybackStateCompat a, @Nullable PlaybackStateCompat b) {
    boolean aHasError = a != null && a.getState() == PlaybackStateCompat.STATE_ERROR;
    boolean bHasError = b != null && b.getState() == PlaybackStateCompat.STATE_ERROR;
    if (aHasError && bHasError) {
      return castNonNull(a).getErrorCode() == castNonNull(b).getErrorCode()
          && TextUtils.equals(castNonNull(a).getErrorMessage(), castNonNull(b).getErrorMessage());
    }
    return aHasError == bHasError;
  }

  /** Converts {@link PlaybackStateCompat} to {@link PlaybackException}. */
  @Nullable
  public static PlaybackException convertToPlaybackException(
      @Nullable PlaybackStateCompat playbackStateCompat) {
    if (playbackStateCompat == null
        || playbackStateCompat.getState() != PlaybackStateCompat.STATE_ERROR) {
      return null;
    }
    StringBuilder stringBuilder = new StringBuilder();
    if (!TextUtils.isEmpty(playbackStateCompat.getErrorMessage())) {
      stringBuilder.append(playbackStateCompat.getErrorMessage().toString()).append(", ");
    }
    stringBuilder.append("code=").append(playbackStateCompat.getErrorCode());
    String errorMessage = stringBuilder.toString();
    return new PlaybackException(
        errorMessage, /* cause= */ null, PlaybackException.ERROR_CODE_REMOTE_ERROR);
  }

  public static MediaBrowserCompat.MediaItem convertToBrowserItem(
      MediaItem item, @Nullable Bitmap artworkBitmap) {
    MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap);
    MediaMetadata metadata = item.mediaMetadata;
    int flags = 0;
    if (metadata.isBrowsable != null && metadata.isBrowsable) {
      flags |= MediaBrowserCompat.MediaItem.FLAG_BROWSABLE;
    }
    if (metadata.isPlayable != null && metadata.isPlayable) {
      flags |= MediaBrowserCompat.MediaItem.FLAG_PLAYABLE;
    }
    return new MediaBrowserCompat.MediaItem(description, flags);
  }

  /** Converts a {@link MediaBrowserCompat.MediaItem} to a {@link MediaItem}. */
  public static MediaItem convertToMediaItem(MediaBrowserCompat.MediaItem item) {
    return convertToMediaItem(item.getDescription(), item.isBrowsable(), item.isPlayable());
  }

  /** Converts a {@link QueueItem} to a {@link MediaItem}. */
  public static MediaItem convertToMediaItem(QueueItem item) {
    return convertToMediaItem(item.getDescription());
  }

  /** Converts a {@link QueueItem} to a {@link MediaItem}. */
  public static MediaItem convertToMediaItem(MediaDescriptionCompat description) {
    checkNotNull(description);
    return convertToMediaItem(description, /* browsable= */ false, /* playable= */ true);
  }

  /** Converts a {@link MediaMetadataCompat} to a {@link MediaItem}. */
  public static MediaItem convertToMediaItem(
      MediaMetadataCompat metadataCompat, @RatingCompat.Style int ratingType) {
    return convertToMediaItem(
        metadataCompat.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID),
        metadataCompat,
        ratingType);
  }

  /** Converts a {@code mediaId} and {@link MediaMetadataCompat} to a {@link MediaItem}. */
  public static MediaItem convertToMediaItem(
      @Nullable String mediaId,
      MediaMetadataCompat metadataCompat,
      @RatingCompat.Style int ratingType) {
    MediaItem.Builder builder = new MediaItem.Builder();
    if (mediaId != null) {
      builder.setMediaId(mediaId);
    }
    @Nullable
    String mediaUriString = metadataCompat.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI);
    if (mediaUriString != null) {
      builder.setRequestMetadata(
          new MediaItem.RequestMetadata.Builder().setMediaUri(Uri.parse(mediaUriString)).build());
    }
    builder.setMediaMetadata(convertToMediaMetadata(metadataCompat, ratingType));
    return builder.build();
  }

  private static MediaItem convertToMediaItem(
      MediaDescriptionCompat descriptionCompat, boolean browsable, boolean playable) {
    @Nullable String mediaId = descriptionCompat.getMediaId();
    return new MediaItem.Builder()
        .setMediaId(mediaId == null ? MediaItem.DEFAULT_MEDIA_ID : mediaId)
        .setRequestMetadata(
            new MediaItem.RequestMetadata.Builder()
                .setMediaUri(descriptionCompat.getMediaUri())
                .build())
        .setMediaMetadata(
            convertToMediaMetadata(
                descriptionCompat, RatingCompat.RATING_NONE, browsable, playable))
        .build();
  }

  /** Converts a list of {@link MediaBrowserCompat.MediaItem} to a list of {@link MediaItem}. */
  public static ImmutableList<MediaItem> convertBrowserItemListToMediaItemList(
      List<MediaBrowserCompat.MediaItem> items) {
    ImmutableList.Builder<MediaItem> builder = new ImmutableList.Builder<>();
    for (int i = 0; i < items.size(); i++) {
      builder.add(convertToMediaItem(items.get(i)));
    }
    return builder.build();
  }

  /** Converts a {@link Timeline} to a list of {@link MediaItem MediaItems}. */
  public static List<MediaItem> convertToMediaItemList(Timeline timeline) {
    List<MediaItem> mediaItems = new ArrayList<>();
    Timeline.Window window = new Timeline.Window();
    for (int i = 0; i < timeline.getWindowCount(); i++) {
      mediaItems.add(timeline.getWindow(i, window).mediaItem);
    }
    return mediaItems;
  }

  /**
   * Converts a {@link MediaItem} to a {@link QueueItem}. The index of the item in the playlist
   * would be used as the queue ID to match the behavior of {@link MediaController}.
   */
  public static QueueItem convertToQueueItem(
      MediaItem item, int mediaItemIndex, @Nullable Bitmap artworkBitmap) {
    MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap);
    long id = convertToQueueItemId(mediaItemIndex);
    return new QueueItem(description, id);
  }

  /** Converts the index of a {@link MediaItem} in a playlist into id of {@link QueueItem}. */
  public static long convertToQueueItemId(int mediaItemIndex) {
    if (mediaItemIndex == C.INDEX_UNSET) {
      return QueueItem.UNKNOWN_ID;
    }
    return mediaItemIndex;
  }

  public static Window convertToWindow(MediaItem mediaItem, int periodIndex) {
    Window window = new Window();
    window.set(
        /* uid= */ 0,
        mediaItem,
        /* manifest= */ null,
        /* presentationStartTimeMs= */ 0,
        /* windowStartTimeMs= */ 0,
        /* elapsedRealtimeEpochOffsetMs= */ 0,
        /* isSeekable= */ true,
        /* isDynamic= */ false,
        /* liveConfiguration= */ null,
        /* defaultPositionUs= */ 0,
        /* durationUs= */ C.TIME_UNSET,
        /* firstPeriodIndex= */ periodIndex,
        /* lastPeriodIndex= */ periodIndex,
        /* positionInFirstPeriodUs= */ 0);
    return window;
  }

  public static Period convertToPeriod(int windowIndex) {
    Period period = new Period();
    period.set(
        /* id= */ null,
        /* uid= */ null,
        windowIndex,
        /* durationUs= */ C.TIME_UNSET,
        /* positionInWindowUs= */ 0,
        /* adPlaybackState= */ AdPlaybackState.NONE,
        /* isPlaceholder= */ true);
    return period;
  }

  /**
   * Returns a list which consists of first {@code N} items of the given list with the same order.
   * {@code N} is determined as the maximum number of items whose total parcelled size is less than
   * {@code sizeLimitInBytes}.
   */
  public static <T extends Parcelable> List<T> truncateListBySize(
      List<T> list, int sizeLimitInBytes) {
    List<T> result = new ArrayList<>();
    Parcel parcel = Parcel.obtain();
    try {
      for (int i = 0; i < list.size(); i++) {
        // Calculate the size.
        T item = list.get(i);
        parcel.writeParcelable(item, 0);
        if (parcel.dataSize() < sizeLimitInBytes) {
          result.add(item);
        } else {
          break;
        }
      }
    } finally {
      parcel.recycle();
    }
    return result;
  }

  /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */
  @SuppressWarnings("deprecation") // Converting deprecated fields.
  public static MediaDescriptionCompat convertToMediaDescriptionCompat(
      MediaItem item, @Nullable Bitmap artworkBitmap) {
    MediaDescriptionCompat.Builder builder =
        new MediaDescriptionCompat.Builder()
            .setMediaId(item.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? null : item.mediaId);
    MediaMetadata metadata = item.mediaMetadata;
    if (artworkBitmap != null) {
      builder.setIconBitmap(artworkBitmap);
    }
    @Nullable Bundle extras = metadata.extras;
    boolean hasFolderType =
        metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE;
    boolean hasMediaType = metadata.mediaType != null;
    if (hasFolderType || hasMediaType) {
      if (extras == null) {
        extras = new Bundle();
      } else {
        extras = new Bundle(extras);
      }
      if (hasFolderType) {
        extras.putLong(
            MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE,
            convertToExtraBtFolderType(checkNotNull(metadata.folderType)));
      }
      if (hasMediaType) {
        extras.putLong(
            MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, checkNotNull(metadata.mediaType));
      }
    }
    return builder
        .setTitle(metadata.title)
        // The BT AVRPC service expects the subtitle of the media description to be the artist
        // (see https://github.com/androidx/media/issues/148).
        .setSubtitle(metadata.artist != null ? metadata.artist : metadata.subtitle)
        .setDescription(metadata.description)
        .setIconUri(metadata.artworkUri)
        .setMediaUri(item.requestMetadata.mediaUri)
        .setExtras(extras)
        .build();
  }

  /** Creates {@link MediaMetadata} from the {@link CharSequence queue title}. */
  public static MediaMetadata convertToMediaMetadata(@Nullable CharSequence queueTitle) {
    if (queueTitle == null) {
      return MediaMetadata.EMPTY;
    }
    return new MediaMetadata.Builder().setTitle(queueTitle).build();
  }

  public static MediaMetadata convertToMediaMetadata(
      @Nullable MediaDescriptionCompat descriptionCompat, @RatingCompat.Style int ratingType) {
    return convertToMediaMetadata(
        descriptionCompat, ratingType, /* browsable= */ false, /* playable= */ true);
  }

  @SuppressWarnings("deprecation") // Populating deprecated fields.
  private static MediaMetadata convertToMediaMetadata(
      @Nullable MediaDescriptionCompat descriptionCompat,
      @RatingCompat.Style int ratingType,
      boolean browsable,
      boolean playable) {
    if (descriptionCompat == null) {
      return MediaMetadata.EMPTY;
    }

    MediaMetadata.Builder builder = new MediaMetadata.Builder();

    builder
        .setTitle(descriptionCompat.getTitle())
        .setSubtitle(descriptionCompat.getSubtitle())
        .setDescription(descriptionCompat.getDescription())
        .setArtworkUri(descriptionCompat.getIconUri())
        .setUserRating(convertToRating(RatingCompat.newUnratedRating(ratingType)));

    @Nullable Bitmap iconBitmap = descriptionCompat.getIconBitmap();
    if (iconBitmap != null) {
      @Nullable byte[] artworkData = null;
      try {
        artworkData = convertToByteArray(iconBitmap);
      } catch (IOException e) {
        Log.w(TAG, "Failed to convert iconBitmap to artworkData", e);
      }
      builder.setArtworkData(artworkData, MediaMetadata.PICTURE_TYPE_FRONT_COVER);
    }

    @Nullable Bundle compatExtras = descriptionCompat.getExtras();
    @Nullable Bundle extras = compatExtras == null ? null : new Bundle(compatExtras);

    if (extras != null && extras.containsKey(MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE)) {
      builder.setFolderType(
          convertToFolderType(extras.getLong(MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE)));
      extras.remove(MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE);
    }
    builder.setIsBrowsable(browsable);

    if (extras != null && extras.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) {
      builder.setMediaType((int) extras.getLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT));
      extras.remove(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT);
    }
    if (extras != null && !extras.isEmpty()) {
      builder.setExtras(extras);
    }

    builder.setIsPlayable(playable);

    return builder.build();
  }

  /** Creates {@link MediaMetadata} from the {@link MediaMetadataCompat} and rating type. */
  @SuppressWarnings("deprecation") // Populating deprecated fields.
  public static MediaMetadata convertToMediaMetadata(
      @Nullable MediaMetadataCompat metadataCompat, @RatingCompat.Style int ratingType) {
    if (metadataCompat == null) {
      return MediaMetadata.EMPTY;
    }

    MediaMetadata.Builder builder = new MediaMetadata.Builder();

    builder
        .setTitle(
            getFirstText(
                metadataCompat,
                MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
                MediaMetadataCompat.METADATA_KEY_TITLE))
        .setSubtitle(metadataCompat.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE))
        .setDescription(
            metadataCompat.getText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION))
        .setArtist(metadataCompat.getText(MediaMetadataCompat.METADATA_KEY_ARTIST))
        .setAlbumTitle(metadataCompat.getText(MediaMetadataCompat.METADATA_KEY_ALBUM))
        .setAlbumArtist(metadataCompat.getText(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST))
        .setOverallRating(
            convertToRating(metadataCompat.getRating(MediaMetadataCompat.METADATA_KEY_RATING)));

    @Nullable
    Rating userRating =
        convertToRating(metadataCompat.getRating(MediaMetadataCompat.METADATA_KEY_USER_RATING));
    if (userRating != null) {
      builder.setUserRating(userRating);
    } else {
      builder.setUserRating(convertToRating(RatingCompat.newUnratedRating(ratingType)));
    }

    if (metadataCompat.containsKey(MediaMetadataCompat.METADATA_KEY_YEAR)) {
      long year = metadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_YEAR);
      builder.setRecordingYear((int) year);
    }

    @Nullable
    String artworkUriString =
        getFirstString(
            metadataCompat,
            MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI);
    if (artworkUriString != null) {
      builder.setArtworkUri(Uri.parse(artworkUriString));
    }

    @Nullable
    Bitmap artworkBitmap =
        getFirstBitmap(
            metadataCompat,
            MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON,
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
    if (artworkBitmap != null) {
      try {
        byte[] artworkData = convertToByteArray(artworkBitmap);
        builder.setArtworkData(artworkData, MediaMetadata.PICTURE_TYPE_FRONT_COVER);
      } catch (IOException e) {
        Log.w(TAG, "Failed to convert artworkBitmap to artworkData", e);
      }
    }

    boolean isBrowsable =
        metadataCompat.containsKey(MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE);
    builder.setIsBrowsable(isBrowsable);
    if (isBrowsable) {
      builder.setFolderType(
          convertToFolderType(
              metadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE)));
    }

    if (metadataCompat.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) {
      builder.setMediaType(
          (int) metadataCompat.getLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT));
    }

    builder.setIsPlayable(true);

    return builder.build();
  }

  @Nullable
  private static Bitmap getFirstBitmap(MediaMetadataCompat mediaMetadataCompat, String... keys) {
    for (String key : keys) {
      if (mediaMetadataCompat.containsKey(key)) {
        return mediaMetadataCompat.getBitmap(key);
      }
    }
    return null;
  }

  @Nullable
  private static String getFirstString(MediaMetadataCompat mediaMetadataCompat, String... keys) {
    for (String key : keys) {
      if (mediaMetadataCompat.containsKey(key)) {
        return mediaMetadataCompat.getString(key);
      }
    }
    return null;
  }

  @Nullable
  private static CharSequence getFirstText(
      MediaMetadataCompat mediaMetadataCompat, String... keys) {
    for (String key : keys) {
      if (mediaMetadataCompat.containsKey(key)) {
        return mediaMetadataCompat.getText(key);
      }
    }
    return null;
  }

  /**
   * Converts a {@link MediaMetadata} to a {@link MediaMetadataCompat}.
   *
   * @param metadata The {@link MediaMetadata} instance to convert.
   * @param mediaId The corresponding media ID.
   * @param mediaUri The corresponding media URI, or null if unknown.
   * @param durationMs The duration of the media, in milliseconds or {@link C#TIME_UNSET}, if no
   *     duration should be included.
   * @return An instance of the legacy {@link MediaMetadataCompat}.
   */
  @SuppressWarnings("deprecation") // Converting deprecated fields.
  public static MediaMetadataCompat convertToMediaMetadataCompat(
      MediaMetadata metadata,
      String mediaId,
      @Nullable Uri mediaUri,
      long durationMs,
      @Nullable Bitmap artworkBitmap) {
    MediaMetadataCompat.Builder builder =
        new MediaMetadataCompat.Builder()
            .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);

    if (metadata.title != null) {
      builder.putText(MediaMetadataCompat.METADATA_KEY_TITLE, metadata.title);
      builder.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, metadata.title);
    }

    if (metadata.subtitle != null) {
      builder.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, metadata.subtitle);
    }

    if (metadata.description != null) {
      builder.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, metadata.description);
    }

    if (metadata.artist != null) {
      builder.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, metadata.artist);
    }

    if (metadata.albumTitle != null) {
      builder.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, metadata.albumTitle);
    }

    if (metadata.albumArtist != null) {
      builder.putText(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, metadata.albumArtist);
    }

    if (metadata.recordingYear != null) {
      builder.putLong(MediaMetadataCompat.METADATA_KEY_YEAR, metadata.recordingYear);
    }

    if (mediaUri != null) {
      builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, mediaUri.toString());
    }

    if (metadata.artworkUri != null) {
      builder.putString(
          MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, metadata.artworkUri.toString());
      builder.putString(
          MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, metadata.artworkUri.toString());
    }

    if (artworkBitmap != null) {
      builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, artworkBitmap);
      builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, artworkBitmap);
    }

    if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) {
      builder.putLong(
          MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE,
          convertToExtraBtFolderType(metadata.folderType));
    }

    if (durationMs != C.TIME_UNSET) {
      builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs);
    }

    @Nullable RatingCompat userRatingCompat = convertToRatingCompat(metadata.userRating);
    if (userRatingCompat != null) {
      builder.putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING, userRatingCompat);
    }

    @Nullable RatingCompat overallRatingCompat = convertToRatingCompat(metadata.overallRating);
    if (overallRatingCompat != null) {
      builder.putRating(MediaMetadataCompat.METADATA_KEY_RATING, overallRatingCompat);
    }

    if (metadata.mediaType != null) {
      builder.putLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, metadata.mediaType);
    }

    return builder.build();
  }

  @SuppressWarnings("deprecation") // Converting to deprecated constants.
  @MediaMetadata.FolderType
  private static int convertToFolderType(long extraBtFolderType) {
    if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_MIXED) {
      return MediaMetadata.FOLDER_TYPE_MIXED;
    } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_TITLES) {
      return MediaMetadata.FOLDER_TYPE_TITLES;
    } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_ALBUMS) {
      return MediaMetadata.FOLDER_TYPE_ALBUMS;
    } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_ARTISTS) {
      return MediaMetadata.FOLDER_TYPE_ARTISTS;
    } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_GENRES) {
      return MediaMetadata.FOLDER_TYPE_GENRES;
    } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_PLAYLISTS) {
      return MediaMetadata.FOLDER_TYPE_PLAYLISTS;
    } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_YEARS) {
      return MediaMetadata.FOLDER_TYPE_YEARS;
    } else {
      return MediaMetadata.FOLDER_TYPE_MIXED;
    }
  }

  @SuppressWarnings("deprecation") // Converting from deprecated constants.
  private static long convertToExtraBtFolderType(@MediaMetadata.FolderType int folderType) {
    switch (folderType) {
      case MediaMetadata.FOLDER_TYPE_MIXED:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_MIXED;
      case MediaMetadata.FOLDER_TYPE_TITLES:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_TITLES;
      case MediaMetadata.FOLDER_TYPE_ALBUMS:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_ALBUMS;
      case MediaMetadata.FOLDER_TYPE_ARTISTS:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_ARTISTS;
      case MediaMetadata.FOLDER_TYPE_GENRES:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_GENRES;
      case MediaMetadata.FOLDER_TYPE_PLAYLISTS:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_PLAYLISTS;
      case MediaMetadata.FOLDER_TYPE_YEARS:
        return MediaDescriptionCompat.BT_FOLDER_TYPE_YEARS;
      case MediaMetadata.FOLDER_TYPE_NONE:
      default:
        throw new IllegalArgumentException("Unrecognized FolderType: " + folderType);
    }
  }

  /**
   * Creates a {@link Rating} from the {@link RatingCompat}.
   *
   * @param ratingCompat A {@link RatingCompat} object.
   * @return The newly created {@link Rating} object.
   */
  @Nullable
  public static Rating convertToRating(@Nullable RatingCompat ratingCompat) {
    if (ratingCompat == null) {
      return null;
    }
    switch (ratingCompat.getRatingStyle()) {
      case RatingCompat.RATING_3_STARS:
        return ratingCompat.isRated()
            ? new StarRating(3, ratingCompat.getStarRating())
            : new StarRating(3);
      case RatingCompat.RATING_4_STARS:
        return ratingCompat.isRated()
            ? new StarRating(4, ratingCompat.getStarRating())
            : new StarRating(4);
      case RatingCompat.RATING_5_STARS:
        return ratingCompat.isRated()
            ? new StarRating(5, ratingCompat.getStarRating())
            : new StarRating(5);
      case RatingCompat.RATING_HEART:
        return ratingCompat.isRated()
            ? new HeartRating(ratingCompat.hasHeart())
            : new HeartRating();
      case RatingCompat.RATING_THUMB_UP_DOWN:
        return ratingCompat.isRated()
            ? new ThumbRating(ratingCompat.isThumbUp())
            : new ThumbRating();
      case RatingCompat.RATING_PERCENTAGE:
        return ratingCompat.isRated()
            ? new PercentageRating(ratingCompat.getPercentRating())
            : new PercentageRating();
      case RatingCompat.RATING_NONE:
      default:
        return null;
    }
  }

  /**
   * Creates a {@link RatingCompat} from the {@link Rating}.
   *
   * @param rating A {@link Rating} object.
   * @return The newly created {@link RatingCompat} object.
   */
  @SuppressLint("WrongConstant") // for @StarStyle
  @Nullable
  public static RatingCompat convertToRatingCompat(@Nullable Rating rating) {
    if (rating == null) {
      return null;
    }
    int ratingCompatStyle = getRatingCompatStyle(rating);
    if (!rating.isRated()) {
      return RatingCompat.newUnratedRating(ratingCompatStyle);
    }

    switch (ratingCompatStyle) {
      case RatingCompat.RATING_3_STARS:
      case RatingCompat.RATING_4_STARS:
      case RatingCompat.RATING_5_STARS:
        return RatingCompat.newStarRating(ratingCompatStyle, ((StarRating) rating).getStarRating());
      case RatingCompat.RATING_HEART:
        return RatingCompat.newHeartRating(((HeartRating) rating).isHeart());
      case RatingCompat.RATING_THUMB_UP_DOWN:
        return RatingCompat.newThumbRating(((ThumbRating) rating).isThumbsUp());
      case RatingCompat.RATING_PERCENTAGE:
        return RatingCompat.newPercentageRating(((PercentageRating) rating).getPercent());
      case RatingCompat.RATING_NONE:
      default:
        return null;
    }
  }

  /** Converts {@link Player}' states to state of {@link PlaybackStateCompat}. */
  @PlaybackStateCompat.State
  public static int convertToPlaybackStateCompatState(
      @Nullable PlaybackException playerError,
      @Player.State int playbackState,
      boolean playWhenReady) {
    if (playerError != null) {
      return PlaybackStateCompat.STATE_ERROR;
    }
    switch (playbackState) {
      case Player.STATE_IDLE:
        return PlaybackStateCompat.STATE_NONE;
      case Player.STATE_READY:
        return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
      case Player.STATE_ENDED:
        return PlaybackStateCompat.STATE_STOPPED;
      case Player.STATE_BUFFERING:
        return playWhenReady
            ? PlaybackStateCompat.STATE_BUFFERING
            : PlaybackStateCompat.STATE_PAUSED;
      default:
        throw new IllegalArgumentException("Unrecognized State: " + playbackState);
    }
  }

  /** Converts a {@link PlaybackStateCompat} to {@link PlaybackParameters}. */
  public static PlaybackParameters convertToPlaybackParameters(
      @Nullable PlaybackStateCompat playbackStateCompat) {
    return playbackStateCompat == null
        ? PlaybackParameters.DEFAULT
        : new PlaybackParameters(playbackStateCompat.getPlaybackSpeed());
  }

  /** Converts a {@link PlaybackStateCompat} to {@link Player}'s play when ready. */
  public static boolean convertToPlayWhenReady(@Nullable PlaybackStateCompat playbackState) {
    if (playbackState == null) {
      return false;
    }
    switch (playbackState.getState()) {
      case PlaybackStateCompat.STATE_BUFFERING:
      case PlaybackStateCompat.STATE_FAST_FORWARDING:
      case PlaybackStateCompat.STATE_PLAYING:
      case PlaybackStateCompat.STATE_REWINDING:
      case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT:
      case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
      case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
        return true;
      case PlaybackStateCompat.STATE_CONNECTING:
      case PlaybackStateCompat.STATE_ERROR:
      case PlaybackStateCompat.STATE_NONE:
      case PlaybackStateCompat.STATE_PAUSED:
      case PlaybackStateCompat.STATE_STOPPED:
        return false;
    }
    return false;
  }

  /** Converts a {@link PlaybackStateCompat} to {@link Player.State} */
  public static @Player.State int convertToPlaybackState(
      @Nullable PlaybackStateCompat playbackStateCompat,
      @Nullable MediaMetadataCompat currentMediaMetadata,
      long timeDiffMs) {
    if (playbackStateCompat == null) {
      return Player.STATE_IDLE;
    }
    switch (playbackStateCompat.getState()) {
      case PlaybackStateCompat.STATE_CONNECTING:
      case PlaybackStateCompat.STATE_ERROR:
      case PlaybackStateCompat.STATE_NONE:
      case PlaybackStateCompat.STATE_STOPPED:
        return Player.STATE_IDLE;
      case PlaybackStateCompat.STATE_BUFFERING:
      case PlaybackStateCompat.STATE_FAST_FORWARDING:
      case PlaybackStateCompat.STATE_REWINDING:
      case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT:
      case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
      case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
        return Player.STATE_BUFFERING;
      case PlaybackStateCompat.STATE_PLAYING:
        return Player.STATE_READY;
      case PlaybackStateCompat.STATE_PAUSED:
        long duration = convertToDurationMs(currentMediaMetadata);
        if (duration == C.TIME_UNSET) {
          return Player.STATE_READY;
        }
        long currentPosition =
            convertToCurrentPositionMs(playbackStateCompat, currentMediaMetadata, timeDiffMs);
        return (currentPosition < duration) ? Player.STATE_READY : Player.STATE_ENDED;
      default:
        throw new IllegalStateException(
            "Unrecognized PlaybackStateCompat: " + playbackStateCompat.getState());
    }
  }

  /** Converts a {@link PlaybackStateCompat} to isPlaying, defined by {@link Player#isPlaying()}. */
  public static boolean convertToIsPlaying(@Nullable PlaybackStateCompat playbackStateCompat) {
    if (playbackStateCompat == null) {
      return false;
    }
    return playbackStateCompat.getState() == PlaybackStateCompat.STATE_PLAYING;
  }

  /** Converts a {@link PlaybackStateCompat} to isPlaying, defined by {@link Player#isPlaying()}. */
  public static boolean convertToIsPlayingAd(@Nullable MediaMetadataCompat metadataCompat) {
    if (metadataCompat == null) {
      return false;
    }
    return metadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT) != 0;
  }

  /** Gets the current position. {@code 0} will be returned if unknown. */
  public static long convertToCurrentPositionMs(
      @Nullable PlaybackStateCompat playbackStateCompat,
      @Nullable MediaMetadataCompat metadataCompat,
      long timeDiffMs) {
    if (playbackStateCompat == null) {
      return 0;
    }
    long positionMs =
        playbackStateCompat.getState() == PlaybackStateCompat.STATE_PLAYING
            ? getCurrentPosition(playbackStateCompat, timeDiffMs)
            : playbackStateCompat.getPosition();
    long durationMs = convertToDurationMs(metadataCompat);
    return durationMs == C.TIME_UNSET
        ? max(0, positionMs)
        : constrainValue(positionMs, /* min= */ 0, durationMs);
  }

  @SuppressWarnings("nullness:argument") // PlaybackStateCompat#getCurrentPosition can take null.
  private static long getCurrentPosition(PlaybackStateCompat playbackStateCompat, long timeDiffMs) {
    return playbackStateCompat.getCurrentPosition(timeDiffMs == C.TIME_UNSET ? null : timeDiffMs);
  }

  /** Gets the duration. {@link C#TIME_UNSET} will be returned if unknown. */
  public static long convertToDurationMs(@Nullable MediaMetadataCompat metadataCompat) {
    if (metadataCompat == null
        || !metadataCompat.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) {
      return C.TIME_UNSET;
    }
    long legacyDurationMs = metadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
    return legacyDurationMs <= 0 ? C.TIME_UNSET : legacyDurationMs;
  }

  /** Gets the buffered position. {@code 0} will be returned if unknown. */
  public static long convertToBufferedPositionMs(
      @Nullable PlaybackStateCompat playbackStateCompat,
      @Nullable MediaMetadataCompat metadataCompat,
      long timeDiffMs) {
    long legacyBufferedPositionMs =
        (playbackStateCompat == null) ? 0 : playbackStateCompat.getBufferedPosition();
    long currentPositionMs =
        convertToCurrentPositionMs(playbackStateCompat, metadataCompat, timeDiffMs);
    long durationMs = convertToDurationMs(metadataCompat);
    return (durationMs == C.TIME_UNSET)
        ? max(currentPositionMs, legacyBufferedPositionMs)
        : constrainValue(legacyBufferedPositionMs, currentPositionMs, durationMs);
  }

  /** Gets the total buffered duration. {@code 0} will be returned if unknown. */
  public static long convertToTotalBufferedDurationMs(
      @Nullable PlaybackStateCompat playbackStateCompat,
      @Nullable MediaMetadataCompat metadataCompat,
      long timeDiffMs) {
    long bufferedPositionMs =
        convertToBufferedPositionMs(playbackStateCompat, metadataCompat, timeDiffMs);
    long currentPositionMs =
        convertToCurrentPositionMs(playbackStateCompat, metadataCompat, timeDiffMs);
    return bufferedPositionMs - currentPositionMs;
  }

  /** Gets the buffered percentage. {@code 0} will be returned if unknown. */
  public static int convertToBufferedPercentage(
      @Nullable PlaybackStateCompat playbackStateCompat,
      @Nullable MediaMetadataCompat mediaMetadataCompat,
      long timeDiffMs) {
    long bufferedPositionMs =
        MediaUtils.convertToBufferedPositionMs(
            playbackStateCompat, mediaMetadataCompat, timeDiffMs);
    long durationMs = MediaUtils.convertToDurationMs(mediaMetadataCompat);
    return calculateBufferedPercentage(bufferedPositionMs, durationMs);
  }

  public static @RatingCompat.Style int getRatingCompatStyle(@Nullable Rating rating) {
    if (rating instanceof HeartRating) {
      return RatingCompat.RATING_HEART;
    } else if (rating instanceof ThumbRating) {
      return RatingCompat.RATING_THUMB_UP_DOWN;
    } else if (rating instanceof StarRating) {
      switch (((StarRating) rating).getMaxStars()) {
        case 3:
          return RatingCompat.RATING_3_STARS;
        case 4:
          return RatingCompat.RATING_4_STARS;
        case 5:
          return RatingCompat.RATING_5_STARS;
      }
    } else if (rating instanceof PercentageRating) {
      return RatingCompat.RATING_PERCENTAGE;
    }
    return RatingCompat.RATING_NONE;
  }

  /** Converts {@link PlaybackStateCompat.RepeatMode} to {@link Player.RepeatMode}. */
  public static @Player.RepeatMode int convertToRepeatMode(
      @PlaybackStateCompat.RepeatMode int playbackStateCompatRepeatMode) {
    switch (playbackStateCompatRepeatMode) {
      case PlaybackStateCompat.REPEAT_MODE_INVALID:
      case PlaybackStateCompat.REPEAT_MODE_NONE:
        return Player.REPEAT_MODE_OFF;
      case PlaybackStateCompat.REPEAT_MODE_ONE:
        return Player.REPEAT_MODE_ONE;
      case PlaybackStateCompat.REPEAT_MODE_ALL:
      case PlaybackStateCompat.REPEAT_MODE_GROUP:
        return Player.REPEAT_MODE_ALL;
      default:
        throw new IllegalArgumentException(
            "Unrecognized PlaybackStateCompat.RepeatMode: " + playbackStateCompatRepeatMode);
    }
  }

  /** Converts {@link Player.RepeatMode} to {@link PlaybackStateCompat.RepeatMode} */
  @PlaybackStateCompat.RepeatMode
  public static int convertToPlaybackStateCompatRepeatMode(@Player.RepeatMode int repeatMode) {
    switch (repeatMode) {
      case Player.REPEAT_MODE_OFF:
        return PlaybackStateCompat.REPEAT_MODE_NONE;
      case Player.REPEAT_MODE_ONE:
        return PlaybackStateCompat.REPEAT_MODE_ONE;
      case Player.REPEAT_MODE_ALL:
        return PlaybackStateCompat.REPEAT_MODE_ALL;
      default:
        throw new IllegalArgumentException("Unrecognized RepeatMode: " + repeatMode);
    }
  }

  /** Converts {@link PlaybackStateCompat.ShuffleMode} to shuffle mode enabled. */
  public static boolean convertToShuffleModeEnabled(
      @PlaybackStateCompat.ShuffleMode int playbackStateCompatShuffleMode) {
    switch (playbackStateCompatShuffleMode) {
      case PlaybackStateCompat.SHUFFLE_MODE_INVALID:
      case PlaybackStateCompat.SHUFFLE_MODE_NONE:
        return false;
      case PlaybackStateCompat.SHUFFLE_MODE_ALL:
      case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
        return true;
      default:
        throw new IllegalArgumentException(
            "Unrecognized ShuffleMode: " + playbackStateCompatShuffleMode);
    }
  }

  /** Converts shuffle mode enabled to {@link PlaybackStateCompat.ShuffleMode} */
  @PlaybackStateCompat.ShuffleMode
  public static int convertToPlaybackStateCompatShuffleMode(boolean shuffleModeEnabled) {
    return shuffleModeEnabled
        ? PlaybackStateCompat.SHUFFLE_MODE_ALL
        : PlaybackStateCompat.SHUFFLE_MODE_NONE;
  }

  /** Converts the rootHints, option, and extra to the {@link LibraryParams}. */
  @Nullable
  public static LibraryParams convertToLibraryParams(
      Context context, @Nullable Bundle legacyBundle) {
    if (legacyBundle == null) {
      return null;
    }
    try {
      legacyBundle.setClassLoader(context.getClassLoader());
      int supportedChildrenFlags =
          legacyBundle.getInt(
              BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, /* defaultValue= */ -1);
      if (supportedChildrenFlags >= 0) {
        legacyBundle.remove(BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS);
        legacyBundle.putBoolean(
            EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY,
            supportedChildrenFlags == MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
      }
      return new LibraryParams.Builder()
          .setExtras(legacyBundle)
          .setRecent(legacyBundle.getBoolean(BrowserRoot.EXTRA_RECENT))
          .setOffline(legacyBundle.getBoolean(BrowserRoot.EXTRA_OFFLINE))
          .setSuggested(legacyBundle.getBoolean(BrowserRoot.EXTRA_SUGGESTED))
          .build();
    } catch (Exception e) {
      // Failure when unpacking the legacy bundle.
      return new LibraryParams.Builder().setExtras(legacyBundle).build();
    }
  }

  /** Converts {@link LibraryParams} to the root hints. */
  @Nullable
  public static Bundle convertToRootHints(@Nullable LibraryParams params) {
    if (params == null) {
      return null;
    }
    Bundle rootHints = new Bundle(params.extras);
    if (params.extras.containsKey(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)) {
      boolean browsableChildrenSupported =
          params.extras.getBoolean(
              EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, /* defaultValue= */ false);
      rootHints.remove(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY);
      rootHints.putInt(
          BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
          browsableChildrenSupported
              ? MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
              : MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
                  | MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);
    }
    rootHints.putBoolean(BrowserRoot.EXTRA_RECENT, params.isRecent);
    rootHints.putBoolean(BrowserRoot.EXTRA_OFFLINE, params.isOffline);
    rootHints.putBoolean(BrowserRoot.EXTRA_SUGGESTED, params.isSuggested);
    return rootHints;
  }

  /** Returns a new list that only contains non-null elements of the original list. */
  public static <T> List<T> removeNullElements(List<@NullableType T> list) {
    List<T> newList = new ArrayList<>();
    for (@Nullable T item : list) {
      if (item != null) {
        newList.add(item);
      }
    }
    return newList;
  }

  /**
   * Converts {@link PlaybackStateCompat}, {@link
   * MediaControllerCompat.PlaybackInfo#getVolumeControl() volume control type}, {@link
   * MediaControllerCompat#getFlags() session flags} and {@link MediaControllerCompat#isSessionReady
   * whether the session is ready} to {@link Player.Commands}.
   *
   * @param playbackStateCompat The {@link PlaybackStateCompat}.
   * @param volumeControlType The {@link MediaControllerCompat.PlaybackInfo#getVolumeControl()
   *     volume control type}.
   * @param sessionFlags The session flags.
   * @param isSessionReady Whether the session compat is ready.
   * @return The converted player commands.
   */
  @SuppressWarnings("deprecation") // Backwards compatibility with old volume commands
  public static Player.Commands convertToPlayerCommands(
      @Nullable PlaybackStateCompat playbackStateCompat,
      int volumeControlType,
      long sessionFlags,
      boolean isSessionReady) {
    Commands.Builder playerCommandsBuilder = new Commands.Builder();
    long actions = playbackStateCompat == null ? 0 : playbackStateCompat.getActions();
    if ((hasAction(actions, PlaybackStateCompat.ACTION_PLAY)
            && hasAction(actions, PlaybackStateCompat.ACTION_PAUSE))
        || hasAction(actions, PlaybackStateCompat.ACTION_PLAY_PAUSE)) {
      playerCommandsBuilder.add(COMMAND_PLAY_PAUSE);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE)) {
      playerCommandsBuilder.add(COMMAND_PREPARE);
    }
    if ((hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)
            && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID))
        || (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)
            && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH))
        || (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_URI)
            && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_URI))) {
      // Require both PREPARE and PLAY actions as we have no logic to handle having just one action.
      playerCommandsBuilder.addAll(COMMAND_SET_MEDIA_ITEM, COMMAND_PREPARE);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_REWIND)) {
      playerCommandsBuilder.add(COMMAND_SEEK_BACK);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_FAST_FORWARD)) {
      playerCommandsBuilder.add(COMMAND_SEEK_FORWARD);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_SEEK_TO)) {
      playerCommandsBuilder.addAll(
          COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_DEFAULT_POSITION);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
      playerCommandsBuilder.addAll(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
      playerCommandsBuilder.addAll(COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED)) {
      playerCommandsBuilder.add(COMMAND_SET_SPEED_AND_PITCH);
    }
    if (hasAction(actions, PlaybackStateCompat.ACTION_STOP)) {
      playerCommandsBuilder.add(COMMAND_STOP);
    }
    if (volumeControlType == VolumeProviderCompat.VOLUME_CONTROL_RELATIVE) {
      playerCommandsBuilder.addAll(
          COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS);
    } else if (volumeControlType == VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE) {
      playerCommandsBuilder.addAll(
          COMMAND_ADJUST_DEVICE_VOLUME,
          COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
          COMMAND_SET_DEVICE_VOLUME,
          COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS);
    }
    playerCommandsBuilder.addAll(
        COMMAND_GET_DEVICE_VOLUME,
        COMMAND_GET_TIMELINE,
        COMMAND_GET_METADATA,
        COMMAND_GET_CURRENT_MEDIA_ITEM,
        COMMAND_GET_AUDIO_ATTRIBUTES,
        COMMAND_RELEASE);
    if ((sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0) {
      playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS);
      if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
        playerCommandsBuilder.add(Player.COMMAND_SEEK_TO_MEDIA_ITEM);
      }
    }
    if (isSessionReady) {
      if (hasAction(actions, PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
        playerCommandsBuilder.add(COMMAND_SET_REPEAT_MODE);
      }
      if (hasAction(actions, PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
        playerCommandsBuilder.add(COMMAND_SET_SHUFFLE_MODE);
      }
    }
    return playerCommandsBuilder.build();
  }

  /**
   * Checks if the set of actions contains the specified action.
   *
   * @param actions A bit set of actions.
   * @param action The action to check.
   * @return Whether the action is contained in the set.
   */
  private static boolean hasAction(long actions, @PlaybackStateCompat.Actions long action) {
    return (actions & action) != 0;
  }

  /**
   * Converts {@link PlaybackStateCompat} to {@link SessionCommands}.
   *
   * <p>This ignores {@link PlaybackStateCompat#getActions() actions} in the {@link
   * PlaybackStateCompat} to workaround media apps' issues that they don't set playback state
   * correctly.
   *
   * @param state playback state
   * @param isSessionReady Whether the session compat is ready.
   * @return the converted session commands
   */
  public static SessionCommands convertToSessionCommands(
      @Nullable PlaybackStateCompat state, boolean isSessionReady) {
    SessionCommands.Builder sessionCommandsBuilder = new SessionCommands.Builder();
    sessionCommandsBuilder.addAllSessionCommands();
    if (!isSessionReady) {
      // Disables rating function when session isn't ready because of the
      // MediaController#setRating(RatingCompat, Bundle) and MediaController#getRatingType().
      sessionCommandsBuilder.remove(SessionCommand.COMMAND_CODE_SESSION_SET_RATING);
    }

    if (state != null && state.getCustomActions() != null) {
      for (CustomAction customAction : state.getCustomActions()) {
        String action = customAction.getAction();
        @Nullable Bundle extras = customAction.getExtras();
        sessionCommandsBuilder.add(
            new SessionCommand(action, extras == null ? Bundle.EMPTY : extras));
      }
    }
    return sessionCommandsBuilder.build();
  }

  /**
   * Converts {@link CustomAction} in the {@link PlaybackStateCompat} to the custom layout which is
   * the list of the {@link CommandButton}.
   *
   * @param state playback state
   * @return custom layout. Always non-null.
   */
  public static ImmutableList<CommandButton> convertToCustomLayout(
      @Nullable PlaybackStateCompat state) {
    if (state == null) {
      return ImmutableList.of();
    }
    ImmutableList.Builder<CommandButton> layout = new ImmutableList.Builder<>();
    for (CustomAction customAction : state.getCustomActions()) {
      String action = customAction.getAction();
      @Nullable Bundle extras = customAction.getExtras();
      CommandButton button =
          new CommandButton.Builder()
              .setSessionCommand(new SessionCommand(action, extras == null ? Bundle.EMPTY : extras))
              .setDisplayName(customAction.getName())
              .setEnabled(true)
              .setIconResId(customAction.getIcon())
              .build();
      layout.add(button);
    }
    return layout.build();
  }

  /** Converts {@link AudioAttributesCompat} into {@link AudioAttributes}. */
  /*
   * @AudioAttributesCompat.AttributeUsage and @C.AudioUsage both use the same constant values,
   * defined by AudioAttributes in the platform.
   */
  @SuppressLint("WrongConstant")
  public static AudioAttributes convertToAudioAttributes(
      @Nullable AudioAttributesCompat audioAttributesCompat) {
    if (audioAttributesCompat == null) {
      return AudioAttributes.DEFAULT;
    }
    return new AudioAttributes.Builder()
        .setContentType(audioAttributesCompat.getContentType())
        .setFlags(audioAttributesCompat.getFlags())
        .setUsage(audioAttributesCompat.getUsage())
        .build();
  }

  /** Converts {@link MediaControllerCompat.PlaybackInfo} to {@link AudioAttributes}. */
  public static AudioAttributes convertToAudioAttributes(
      @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat) {
    if (playbackInfoCompat == null) {
      return AudioAttributes.DEFAULT;
    }
    return MediaUtils.convertToAudioAttributes(playbackInfoCompat.getAudioAttributes());
  }

  /** Converts {@link AudioAttributes} into {@link AudioAttributesCompat}. */
  public static AudioAttributesCompat convertToAudioAttributesCompat(
      AudioAttributes audioAttributes) {
    return new AudioAttributesCompat.Builder()
        .setContentType(audioAttributes.contentType)
        .setFlags(audioAttributes.flags)
        .setUsage(audioAttributes.usage)
        .build();
  }

  /**
   * Gets the legacy stream type from {@link AudioAttributes}.
   *
   * @param audioAttributes audio attributes
   * @return int legacy stream type from {@link AudioManager}
   */
  public static int getLegacyStreamType(AudioAttributes audioAttributes) {
    int legacyStreamType =
        MediaUtils.convertToAudioAttributesCompat(audioAttributes).getLegacyStreamType();
    if (legacyStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
      // Usually, AudioAttributesCompat#getLegacyStreamType() does not return
      // USE_DEFAULT_STREAM_TYPE unless the developer sets it with
      // AudioAttributesCompat.Builder#setLegacyStreamType().
      // But for safety, let's convert USE_DEFAULT_STREAM_TYPE to STREAM_MUSIC here.
      return AudioManager.STREAM_MUSIC;
    }
    return legacyStreamType;
  }

  public static <T> T getFutureResult(Future<T> future, long timeoutMs)
      throws ExecutionException, TimeoutException {
    long initialTimeMs = SystemClock.elapsedRealtime();
    long remainingTimeMs = timeoutMs;
    boolean interrupted = false;
    try {
      while (true) {
        try {
          return future.get(remainingTimeMs, MILLISECONDS);
        } catch (InterruptedException e) {
          interrupted = true;
          long elapsedTimeMs = SystemClock.elapsedRealtime() - initialTimeMs;
          if (elapsedTimeMs >= timeoutMs) {
            throw new TimeoutException();
          }
          remainingTimeMs = timeoutMs - elapsedTimeMs;
        }
      }
    } finally {
      if (interrupted) {
        Thread.currentThread().interrupt();
      }
    }
  }

  /** Converts {@link MediaControllerCompat.PlaybackInfo} to {@link DeviceInfo}. */
  public static DeviceInfo convertToDeviceInfo(
      @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat,
      @Nullable String routingControllerId) {
    if (playbackInfoCompat == null) {
      return DeviceInfo.UNKNOWN;
    }
    return new DeviceInfo.Builder(
            playbackInfoCompat.getPlaybackType()
                    == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE
                ? DeviceInfo.PLAYBACK_TYPE_REMOTE
                : DeviceInfo.PLAYBACK_TYPE_LOCAL)
        .setMaxVolume(playbackInfoCompat.getMaxVolume())
        .setRoutingControllerId(routingControllerId)
        .build();
  }

  /** Converts {@link MediaControllerCompat.PlaybackInfo} to device volume. */
  public static int convertToDeviceVolume(
      @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat) {
    if (playbackInfoCompat == null) {
      return 0;
    }
    return playbackInfoCompat.getCurrentVolume();
  }

  /** Converts {@link MediaControllerCompat.PlaybackInfo} to device muted. */
  public static boolean convertToIsDeviceMuted(
      @Nullable MediaControllerCompat.PlaybackInfo playbackInfoCompat) {
    if (playbackInfoCompat == null) {
      return false;
    }
    return playbackInfoCompat.getCurrentVolume() == 0;
  }

  public static Commands createPlayerCommandsWith(@Command int command) {
    return new Commands.Builder().add(command).build();
  }

  public static Commands createPlayerCommandsWithout(@Command int command) {
    return new Commands.Builder().addAllCommands().remove(command).build();
  }

  /**
   * Returns the intersection of {@link Player.Command commands} from the given two {@link
   * Commands}.
   */
  public static Commands intersect(@Nullable Commands commands1, @Nullable Commands commands2) {
    if (commands1 == null || commands2 == null) {
      return Commands.EMPTY;
    }
    Commands.Builder intersectCommandsBuilder = new Commands.Builder();
    for (int i = 0; i < commands1.size(); i++) {
      if (commands2.contains(commands1.get(i))) {
        intersectCommandsBuilder.add(commands1.get(i));
      }
    }
    return intersectCommandsBuilder.build();
  }

  /**
   * Merges the excluded fields into the {@code newPlayerInfo} by taking the values of the {@code
   * previousPlayerInfo} and taking into account the passed available commands.
   *
   * @param oldPlayerInfo The old {@link PlayerInfo}.
   * @param oldBundlingExclusions The bundling exlusions in the old {@link PlayerInfo}.
   * @param newPlayerInfo The new {@link PlayerInfo}.
   * @param newBundlingExclusions The bundling exlusions in the new {@link PlayerInfo}.
   * @param availablePlayerCommands The available commands to take into account when merging.
   * @return A pair with the resulting {@link PlayerInfo} and {@link BundlingExclusions}.
   */
  public static Pair<PlayerInfo, BundlingExclusions> mergePlayerInfo(
      PlayerInfo oldPlayerInfo,
      BundlingExclusions oldBundlingExclusions,
      PlayerInfo newPlayerInfo,
      BundlingExclusions newBundlingExclusions,
      Commands availablePlayerCommands) {
    PlayerInfo mergedPlayerInfo = newPlayerInfo;
    BundlingExclusions mergedBundlingExclusions = newBundlingExclusions;
    if (newBundlingExclusions.isTimelineExcluded
        && availablePlayerCommands.contains(Player.COMMAND_GET_TIMELINE)
        && !oldBundlingExclusions.isTimelineExcluded) {
      // Use the previous timeline if it is excluded in the most recent update.
      mergedPlayerInfo = mergedPlayerInfo.copyWithTimeline(oldPlayerInfo.timeline);
      mergedBundlingExclusions =
          new BundlingExclusions(
              /* isTimelineExcluded= */ false, mergedBundlingExclusions.areCurrentTracksExcluded);
    }
    if (newBundlingExclusions.areCurrentTracksExcluded
        && availablePlayerCommands.contains(Player.COMMAND_GET_TRACKS)
        && !oldBundlingExclusions.areCurrentTracksExcluded) {
      // Use the previous tracks if it is excluded in the most recent update.
      mergedPlayerInfo = mergedPlayerInfo.copyWithCurrentTracks(oldPlayerInfo.currentTracks);
      mergedBundlingExclusions =
          new BundlingExclusions(
              mergedBundlingExclusions.isTimelineExcluded, /* areCurrentTracksExcluded= */ false);
    }
    return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions);
  }

  /** Generates an array of {@code n} indices. */
  public static int[] generateUnshuffledIndices(int n) {
    int[] indices = new int[n];
    for (int i = 0; i < n; i++) {
      indices[i] = i;
    }
    return indices;
  }

  /**
   * Calculates the buffered percentage of the given buffered position and the duration in
   * milliseconds.
   */
  public static int calculateBufferedPercentage(long bufferedPositionMs, long durationMs) {
    return bufferedPositionMs == C.TIME_UNSET || durationMs == C.TIME_UNSET
        ? 0
        : durationMs == 0
            ? 100
            : Util.constrainValue((int) ((bufferedPositionMs * 100) / durationMs), 0, 100);
  }

  /**
   * Sets media items with start index and position for the given {@link Player} by honoring the
   * available commands.
   *
   * @param player The player to set the media items.
   * @param mediaItemsWithStartPosition The media items, the index and the position to set.
   */
  public static void setMediaItemsWithStartIndexAndPosition(
      Player player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) {
    if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET) {
      if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
        player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true);
      } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) {
        player.setMediaItem(
            mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true);
      }
    } else if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
      player.setMediaItems(
          mediaItemsWithStartPosition.mediaItems,
          mediaItemsWithStartPosition.startIndex,
          mediaItemsWithStartPosition.startPositionMs);
    } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) {
      player.setMediaItem(
          mediaItemsWithStartPosition.mediaItems.get(0),
          mediaItemsWithStartPosition.startPositionMs);
    }
  }

  private static byte[] convertToByteArray(Bitmap bitmap) throws IOException {
    try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
      bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);
      return stream.toByteArray();
    }
  }

  private MediaUtils() {}
}