DownloadRequest.java

/*
 * Copyright (C) 2017 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.exoplayer.offline;

import static androidx.media3.common.util.Util.castNonNull;

import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/** Defines content to be downloaded. */
@UnstableApi
public final class DownloadRequest implements Parcelable {

  /** Thrown when the encoded request data belongs to an unsupported request type. */
  public static class UnsupportedRequestException extends IOException {}

  /** A builder for download requests. */
  public static class Builder {
    private final String id;
    private final Uri uri;
    @Nullable private String mimeType;
    @Nullable private List<StreamKey> streamKeys;
    @Nullable private byte[] keySetId;
    @Nullable private String customCacheKey;
    @Nullable private byte[] data;

    /** Creates a new instance with the specified id and uri. */
    public Builder(String id, Uri uri) {
      this.id = id;
      this.uri = uri;
    }

    /** Sets the {@link DownloadRequest#mimeType}. */
    public Builder setMimeType(@Nullable String mimeType) {
      this.mimeType = mimeType;
      return this;
    }

    /** Sets the {@link DownloadRequest#streamKeys}. */
    public Builder setStreamKeys(@Nullable List<StreamKey> streamKeys) {
      this.streamKeys = streamKeys;
      return this;
    }

    /** Sets the {@link DownloadRequest#keySetId}. */
    public Builder setKeySetId(@Nullable byte[] keySetId) {
      this.keySetId = keySetId;
      return this;
    }

    /** Sets the {@link DownloadRequest#customCacheKey}. */
    public Builder setCustomCacheKey(@Nullable String customCacheKey) {
      this.customCacheKey = customCacheKey;
      return this;
    }

    /** Sets the {@link DownloadRequest#data}. */
    public Builder setData(@Nullable byte[] data) {
      this.data = data;
      return this;
    }

    public DownloadRequest build() {
      return new DownloadRequest(
          id,
          uri,
          mimeType,
          streamKeys != null ? streamKeys : ImmutableList.of(),
          keySetId,
          customCacheKey,
          data);
    }
  }

  /** The unique content id. */
  public final String id;
  /** The uri being downloaded. */
  public final Uri uri;
  /**
   * The MIME type of this content. Used as a hint to infer the content's type (DASH, HLS,
   * SmoothStreaming). If null, a {@code DownloadService} will infer the content type from the
   * {@link #uri}.
   */
  @Nullable public final String mimeType;
  /** Stream keys to be downloaded. If empty, all streams will be downloaded. */
  public final List<StreamKey> streamKeys;
  /** The key set id of the offline licence if the content is protected with DRM. */
  @Nullable public final byte[] keySetId;
  /**
   * Custom key for cache indexing, or null. Must be null for DASH, HLS and SmoothStreaming
   * downloads.
   */
  @Nullable public final String customCacheKey;
  /** Application defined data associated with the download. May be empty. */
  public final byte[] data;

  /**
   * @param id See {@link #id}.
   * @param uri See {@link #uri}.
   * @param mimeType See {@link #mimeType}
   * @param streamKeys See {@link #streamKeys}.
   * @param customCacheKey See {@link #customCacheKey}.
   * @param data See {@link #data}.
   */
  private DownloadRequest(
      String id,
      Uri uri,
      @Nullable String mimeType,
      List<StreamKey> streamKeys,
      @Nullable byte[] keySetId,
      @Nullable String customCacheKey,
      @Nullable byte[] data) {
    @C.ContentType int contentType = Util.inferContentTypeForUriAndMimeType(uri, mimeType);
    if (contentType == C.TYPE_DASH || contentType == C.TYPE_HLS || contentType == C.TYPE_SS) {
      Assertions.checkArgument(
          customCacheKey == null, "customCacheKey must be null for type: " + contentType);
    }
    this.id = id;
    this.uri = uri;
    this.mimeType = mimeType;
    ArrayList<StreamKey> mutableKeys = new ArrayList<>(streamKeys);
    Collections.sort(mutableKeys);
    this.streamKeys = Collections.unmodifiableList(mutableKeys);
    this.keySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
    this.customCacheKey = customCacheKey;
    this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;
  }

  /* package */ DownloadRequest(Parcel in) {
    id = castNonNull(in.readString());
    uri = Uri.parse(castNonNull(in.readString()));
    mimeType = in.readString();
    int streamKeyCount = in.readInt();
    ArrayList<StreamKey> mutableStreamKeys = new ArrayList<>(streamKeyCount);
    for (int i = 0; i < streamKeyCount; i++) {
      mutableStreamKeys.add(in.readParcelable(StreamKey.class.getClassLoader()));
    }
    streamKeys = Collections.unmodifiableList(mutableStreamKeys);
    keySetId = in.createByteArray();
    customCacheKey = in.readString();
    data = castNonNull(in.createByteArray());
  }

  /**
   * Returns a copy with the specified ID.
   *
   * @param id The ID of the copy.
   * @return The copy with the specified ID.
   */
  public DownloadRequest copyWithId(String id) {
    return new DownloadRequest(id, uri, mimeType, streamKeys, keySetId, customCacheKey, data);
  }

  /**
   * Returns a copy with the specified key set ID.
   *
   * @param keySetId The key set ID of the copy.
   * @return The copy with the specified key set ID.
   */
  public DownloadRequest copyWithKeySetId(@Nullable byte[] keySetId) {
    return new DownloadRequest(id, uri, mimeType, streamKeys, keySetId, customCacheKey, data);
  }

  /**
   * Returns the result of merging {@code newRequest} into this request. The requests must have the
   * same {@link #id}.
   *
   * <p>The resulting request contains the stream keys from both requests. For all other member
   * variables, those in {@code newRequest} are preferred.
   *
   * @param newRequest The request being merged.
   * @return The merged result.
   * @throws IllegalArgumentException If the requests do not have the same {@link #id}.
   */
  public DownloadRequest copyWithMergedRequest(DownloadRequest newRequest) {
    Assertions.checkArgument(id.equals(newRequest.id));
    List<StreamKey> mergedKeys;
    if (streamKeys.isEmpty() || newRequest.streamKeys.isEmpty()) {
      // If either streamKeys is empty then all streams should be downloaded.
      mergedKeys = Collections.emptyList();
    } else {
      mergedKeys = new ArrayList<>(streamKeys);
      for (int i = 0; i < newRequest.streamKeys.size(); i++) {
        StreamKey newKey = newRequest.streamKeys.get(i);
        if (!mergedKeys.contains(newKey)) {
          mergedKeys.add(newKey);
        }
      }
    }
    return new DownloadRequest(
        id,
        newRequest.uri,
        newRequest.mimeType,
        mergedKeys,
        newRequest.keySetId,
        newRequest.customCacheKey,
        newRequest.data);
  }

  /** Returns a {@link MediaItem} for the content defined by the request. */
  public MediaItem toMediaItem() {
    return new MediaItem.Builder()
        .setMediaId(id)
        .setUri(uri)
        .setCustomCacheKey(customCacheKey)
        .setMimeType(mimeType)
        .setStreamKeys(streamKeys)
        .build();
  }

  @Override
  public String toString() {
    return mimeType + ":" + id;
  }

  @Override
  public boolean equals(@Nullable Object o) {
    if (!(o instanceof DownloadRequest)) {
      return false;
    }
    DownloadRequest that = (DownloadRequest) o;
    return id.equals(that.id)
        && uri.equals(that.uri)
        && Util.areEqual(mimeType, that.mimeType)
        && streamKeys.equals(that.streamKeys)
        && Arrays.equals(keySetId, that.keySetId)
        && Util.areEqual(customCacheKey, that.customCacheKey)
        && Arrays.equals(data, that.data);
  }

  @Override
  public final int hashCode() {
    int result = 31 * id.hashCode();
    result = 31 * result + uri.hashCode();
    result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
    result = 31 * result + streamKeys.hashCode();
    result = 31 * result + Arrays.hashCode(keySetId);
    result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);
    result = 31 * result + Arrays.hashCode(data);
    return result;
  }

  // Parcelable implementation.

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(id);
    dest.writeString(uri.toString());
    dest.writeString(mimeType);
    dest.writeInt(streamKeys.size());
    for (int i = 0; i < streamKeys.size(); i++) {
      dest.writeParcelable(streamKeys.get(i), /* parcelableFlags= */ 0);
    }
    dest.writeByteArray(keySetId);
    dest.writeString(customCacheKey);
    dest.writeByteArray(data);
  }

  public static final Parcelable.Creator<DownloadRequest> CREATOR =
      new Parcelable.Creator<DownloadRequest>() {

        @Override
        public DownloadRequest createFromParcel(Parcel in) {
          return new DownloadRequest(in);
        }

        @Override
        public DownloadRequest[] newArray(int size) {
          return new DownloadRequest[size];
        }
      };
}