ActionFile.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 android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.AtomicFile;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.offline.DownloadRequest.UnsupportedRequestException;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Loads {@link DownloadRequest DownloadRequests} from legacy action files.
*
* @deprecated Legacy action files should be merged into download indices using {@link
* ActionFileUpgradeUtil}.
*/
@Deprecated
/* package */ final class ActionFile {
private static final int VERSION = 0;
private static final String DOWNLOAD_TYPE_PROGRESSIVE = "progressive";
private static final String DOWNLOAD_TYPE_DASH = "dash";
private static final String DOWNLOAD_TYPE_HLS = "hls";
private static final String DOWNLOAD_TYPE_SS = "ss";
private final AtomicFile atomicFile;
/**
* @param actionFile The file from which {@link DownloadRequest DownloadRequests} will be loaded.
*/
public ActionFile(File actionFile) {
atomicFile = new AtomicFile(actionFile);
}
/** Returns whether the file or its backup exists. */
public boolean exists() {
return atomicFile.exists();
}
/** Deletes the action file and its backup. */
public void delete() {
atomicFile.delete();
}
/**
* Loads {@link DownloadRequest DownloadRequests} from the file.
*
* @return The loaded {@link DownloadRequest DownloadRequests}, or an empty array if the file does
* not exist.
* @throws IOException If there is an error reading the file.
*/
public DownloadRequest[] load() throws IOException {
if (!exists()) {
return new DownloadRequest[0];
}
@Nullable InputStream inputStream = null;
try {
inputStream = atomicFile.openRead();
DataInputStream dataInputStream = new DataInputStream(inputStream);
int version = dataInputStream.readInt();
if (version > VERSION) {
throw new IOException("Unsupported action file version: " + version);
}
int actionCount = dataInputStream.readInt();
ArrayList<DownloadRequest> actions = new ArrayList<>();
for (int i = 0; i < actionCount; i++) {
try {
actions.add(readDownloadRequest(dataInputStream));
} catch (UnsupportedRequestException e) {
// remove DownloadRequest is not supported. Ignore and continue loading rest.
}
}
return actions.toArray(new DownloadRequest[0]);
} finally {
Util.closeQuietly(inputStream);
}
}
private static DownloadRequest readDownloadRequest(DataInputStream input) throws IOException {
String downloadType = input.readUTF();
int version = input.readInt();
Uri uri = Uri.parse(input.readUTF());
boolean isRemoveAction = input.readBoolean();
int dataLength = input.readInt();
@Nullable byte[] data;
if (dataLength != 0) {
data = new byte[dataLength];
input.readFully(data);
} else {
data = null;
}
// Serialized version 0 progressive actions did not contain keys.
boolean isLegacyProgressive = version == 0 && DOWNLOAD_TYPE_PROGRESSIVE.equals(downloadType);
List<StreamKey> keys = new ArrayList<>();
if (!isLegacyProgressive) {
int keyCount = input.readInt();
for (int i = 0; i < keyCount; i++) {
keys.add(readKey(downloadType, version, input));
}
}
// Serialized version 0 and 1 DASH/HLS/SS actions did not contain a custom cache key.
boolean isLegacySegmented =
version < 2
&& (DOWNLOAD_TYPE_DASH.equals(downloadType)
|| DOWNLOAD_TYPE_HLS.equals(downloadType)
|| DOWNLOAD_TYPE_SS.equals(downloadType));
@Nullable String customCacheKey = null;
if (!isLegacySegmented) {
customCacheKey = input.readBoolean() ? input.readUTF() : null;
}
// Serialized version 0, 1 and 2 did not contain an id. We need to generate one.
String id = version < 3 ? generateDownloadId(uri, customCacheKey) : input.readUTF();
if (isRemoveAction) {
// Remove actions are not supported anymore.
throw new UnsupportedRequestException();
}
return new DownloadRequest.Builder(id, uri)
.setMimeType(inferMimeType(downloadType))
.setStreamKeys(keys)
.setCustomCacheKey(customCacheKey)
.setData(data)
.build();
}
private static StreamKey readKey(String type, int version, DataInputStream input)
throws IOException {
int periodIndex;
int groupIndex;
int trackIndex;
// Serialized version 0 HLS/SS actions did not contain a period index.
if ((DOWNLOAD_TYPE_HLS.equals(type) || DOWNLOAD_TYPE_SS.equals(type)) && version == 0) {
periodIndex = 0;
groupIndex = input.readInt();
trackIndex = input.readInt();
} else {
periodIndex = input.readInt();
groupIndex = input.readInt();
trackIndex = input.readInt();
}
return new StreamKey(periodIndex, groupIndex, trackIndex);
}
private static String inferMimeType(String downloadType) {
switch (downloadType) {
case DOWNLOAD_TYPE_DASH:
return MimeTypes.APPLICATION_MPD;
case DOWNLOAD_TYPE_HLS:
return MimeTypes.APPLICATION_M3U8;
case DOWNLOAD_TYPE_SS:
return MimeTypes.APPLICATION_SS;
case DOWNLOAD_TYPE_PROGRESSIVE:
default:
return MimeTypes.VIDEO_UNKNOWN;
}
}
private static String generateDownloadId(Uri uri, @Nullable String customCacheKey) {
return customCacheKey != null ? customCacheKey : uri.toString();
}
}