Offline refactor step 1a - Make DownloadAction concrete

1. Pull up all subclasses of DownloadAction into DownloadAction
2. Add DownloaderFactory for Downloader instantiation, and DefaultDownloaderFactory
   to replace the instantiation logic being removed from the DownloadAction
   subclasses.

This change will upgrade existing action files gracefully (i.e. it does not
lose compatibility with the existing offline implementation, other than some
minor breaking changes to the API).

TODOs:

1. Move test methods from the XDownloadActionTest classes into DownloadActionTest.
   This will be done in a subsequent CL. There's a lot of consolidation that can
   be done here, including de-duplicating some of the test code added in this CL.
2. Look at merging DownloaderConstructorHelper into DefaultDownloaderFactory.
3. Use customCacheKey in DASH/HLS/SS Downloaders, for completeness.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=222258983
This commit is contained in:
olly 2018-11-20 09:33:47 -08:00 committed by Oliver Woodman
parent 527f2cf730
commit 6ebb6124bb
30 changed files with 955 additions and 1178 deletions

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource;
@ -87,10 +88,10 @@ public class DemoApplication extends Application {
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager =
new DownloadManager(
downloaderConstructorHelper,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
new DefaultDownloaderFactory(downloaderConstructorHelper),
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
DownloadManager.DEFAULT_MIN_RETRY_COUNT);
downloadTracker =
new DownloadTracker(
/* context= */ this,

View file

@ -81,11 +81,7 @@ public class DownloadTracker implements DownloadManager.Listener {
private final ActionFile actionFile;
private final Handler actionFileWriteHandler;
public DownloadTracker(
Context context,
DataSource.Factory dataSourceFactory,
File actionFile,
DownloadAction.Deserializer... deserializers) {
public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) {
this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory;
this.actionFile = new ActionFile(actionFile);
@ -95,8 +91,7 @@ public class DownloadTracker implements DownloadManager.Listener {
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
actionFileWriteThread.start();
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
loadTrackedActions(
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
loadTrackedActions();
}
public void addListener(Listener listener) {
@ -158,9 +153,9 @@ public class DownloadTracker implements DownloadManager.Listener {
// Internal methods
private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) {
private void loadTrackedActions() {
try {
DownloadAction[] allActions = actionFile.load(deserializers);
DownloadAction[] allActions = actionFile.load();
for (DownloadAction action : allActions) {
trackedDownloadStates.put(action.uri, action);
}

View file

@ -30,10 +30,19 @@
<init>();
}
# Constructors accessed via reflection in DownloadAction
-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction
-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction
-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction
# Constructors accessed via reflection in DefaultDownloaderFactory
-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader
-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader {
<init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper);
}
-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader
-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader {
<init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper);
}
-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader
-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader {
<init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper);
}
# Don't warn about checkerframework
-dontwarn org.checkerframework.**

View file

@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.offline;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.util.AtomicFile;
import com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream;
@ -45,11 +44,10 @@ public final class ActionFile {
/**
* Loads {@link DownloadAction}s from file.
*
* @param deserializers {@link Deserializer}s to deserialize DownloadActions.
* @return Loaded DownloadActions. If the action file doesn't exists returns an empty array.
* @throws IOException If there is an error during loading.
*/
public DownloadAction[] load(Deserializer... deserializers) throws IOException {
public DownloadAction[] load() throws IOException {
if (!actionFile.exists()) {
return new DownloadAction[0];
}
@ -64,7 +62,7 @@ public final class ActionFile {
int actionCount = dataInputStream.readInt();
DownloadAction[] actions = new DownloadAction[actionCount];
for (int i = 0; i < actionCount; i++) {
actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream);
actions[i] = DownloadAction.deserializeFromStream(dataInputStream);
}
return actions;
} finally {
@ -85,7 +83,7 @@ public final class ActionFile {
output.writeInt(VERSION);
output.writeInt(downloadActions.length);
for (DownloadAction action : downloadActions) {
DownloadAction.serializeToStream(action, output);
action.serializeToStream(output);
}
atomicFile.endWrite(output);
// Avoid calling close twice.

View file

@ -0,0 +1,120 @@
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.util.List;
/**
* Default {@link DownloaderFactory}, supporting creation of progressive, DASH, HLS and
* SmoothStreaming downloaders. Note that for the latter three, the corresponding library module
* must be built into the application.
*/
public class DefaultDownloaderFactory implements DownloaderFactory {
@Nullable private static final Constructor<? extends Downloader> DASH_DOWNLOADER_CONSTRUCTOR;
@Nullable private static final Constructor<? extends Downloader> HLS_DOWNLOADER_CONSTRUCTOR;
@Nullable private static final Constructor<? extends Downloader> SS_DOWNLOADER_CONSTRUCTOR;
static {
Constructor<? extends Downloader> dashDownloaderConstructor = null;
try {
// LINT.IfChange
dashDownloaderConstructor =
getDownloaderConstructor(
Class.forName("com.google.android.exoplayer2.source.dash.offline.DashDownloader"));
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) {
// Expected if the app was built without the DASH module.
}
DASH_DOWNLOADER_CONSTRUCTOR = dashDownloaderConstructor;
Constructor<? extends Downloader> hlsDownloaderConstructor = null;
try {
// LINT.IfChange
hlsDownloaderConstructor =
getDownloaderConstructor(
Class.forName("com.google.android.exoplayer2.source.hls.offline.HlsDownloader"));
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) {
// Expected if the app was built without the HLS module.
}
HLS_DOWNLOADER_CONSTRUCTOR = hlsDownloaderConstructor;
Constructor<? extends Downloader> ssDownloaderConstructor = null;
try {
// LINT.IfChange
ssDownloaderConstructor =
getDownloaderConstructor(
Class.forName(
"com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader"));
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) {
// Expected if the app was built without the SmoothStreaming module.
}
SS_DOWNLOADER_CONSTRUCTOR = ssDownloaderConstructor;
}
private final DownloaderConstructorHelper downloaderConstructorHelper;
/** @param downloaderConstructorHelper A helper for instantiating downloaders. */
public DefaultDownloaderFactory(DownloaderConstructorHelper downloaderConstructorHelper) {
this.downloaderConstructorHelper = downloaderConstructorHelper;
}
@Override
public Downloader createDownloader(DownloadAction action) {
switch (action.type) {
case DownloadAction.TYPE_PROGRESSIVE:
return new ProgressiveDownloader(
action.uri, action.customCacheKey, downloaderConstructorHelper);
case DownloadAction.TYPE_DASH:
return createDownloader(action, DASH_DOWNLOADER_CONSTRUCTOR);
case DownloadAction.TYPE_HLS:
return createDownloader(action, HLS_DOWNLOADER_CONSTRUCTOR);
case DownloadAction.TYPE_SS:
return createDownloader(action, SS_DOWNLOADER_CONSTRUCTOR);
default:
throw new IllegalArgumentException("Unsupported type: " + action.type);
}
}
private Downloader createDownloader(
DownloadAction action, @Nullable Constructor<? extends Downloader> constructor) {
if (constructor == null) {
throw new IllegalStateException("Module missing for: " + action.type);
}
try {
// TODO: Support customCacheKey in DASH/HLS/SS, for completeness.
return constructor.newInstance(action.uri, action.getKeys(), downloaderConstructorHelper);
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate downloader for: " + action.type, e);
}
}
// LINT.IfChange
private static Constructor<? extends Downloader> getDownloaderConstructor(Class<?> clazz) {
try {
return clazz
.asSubclass(Downloader.class)
.getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class);
} catch (NoSuchMethodException e) {
// The downloader is present, but the expected constructor is missing.
throw new RuntimeException("DASH downloader constructor missing", e);
}
}
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
}

View file

@ -25,147 +25,124 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** Contains the necessary parameters for a download or remove action. */
public abstract class DownloadAction {
public final class DownloadAction {
/** Used to deserialize {@link DownloadAction}s. */
public abstract static class Deserializer {
/** Type for progressive downloads. */
public static final String TYPE_PROGRESSIVE = "progressive";
/** Type for DASH downloads. */
public static final String TYPE_DASH = "dash";
/** Type for HLS downloads. */
public static final String TYPE_HLS = "hls";
/** Type for SmoothStreaming downloads. */
public static final String TYPE_SS = "ss";
public final String type;
public final int version;
public Deserializer(String type, int version) {
this.type = type;
this.version = version;
}
/**
* Deserializes an action from the {@code input}.
*
* @param version The version of the serialized action.
* @param input The stream from which to read the action.
* @see DownloadAction#writeToStream(DataOutputStream)
*/
public abstract DownloadAction readFromStream(int version, DataInputStream input)
throws IOException;
}
private static @Nullable Deserializer[] defaultDeserializers;
/** Returns available default {@link Deserializer}s. */
public static synchronized Deserializer[] getDefaultDeserializers() {
if (defaultDeserializers != null) {
return defaultDeserializers;
}
Deserializer[] deserializers = new Deserializer[4];
int count = 0;
deserializers[count++] = ProgressiveDownloadAction.DESERIALIZER;
Class<?> clazz;
// Full class names used for constructor args so the LINT rule triggers if any of them move.
try {
// LINT.IfChange
clazz = Class.forName("com.google.android.exoplayer2.source.dash.offline.DashDownloadAction");
// LINT.ThenChange(../../../../../../../../../dash/proguard-rules.txt)
deserializers[count++] = getDeserializer(clazz);
} catch (Exception e) {
// Do nothing.
}
try {
// LINT.IfChange
clazz = Class.forName("com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction");
// LINT.ThenChange(../../../../../../../../../hls/proguard-rules.txt)
deserializers[count++] = getDeserializer(clazz);
} catch (Exception e) {
// Do nothing.
}
try {
// LINT.IfChange
clazz =
Class.forName(
"com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction");
// LINT.ThenChange(../../../../../../../../../smoothstreaming/proguard-rules.txt)
deserializers[count++] = getDeserializer(clazz);
} catch (Exception e) {
// Do nothing.
}
defaultDeserializers = Arrays.copyOf(Assertions.checkNotNull(deserializers), count);
return defaultDeserializers;
}
private static final int VERSION = 2;
/**
* Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction,
* OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the
* action's type.
* Deserializes one action that was serialized with {@link #serializeToStream(OutputStream)} from
* the {@code input}.
*
* <p>The caller is responsible for closing the given {@link InputStream}.
*
* @param deserializers {@link Deserializer}s for supported actions.
* @param input The stream from which to read the action.
* @param input The stream from which to read.
* @return The deserialized action.
* @throws IOException If there is an IO error reading from {@code input}, or if the action type
* isn't supported by any of the {@code deserializers}.
*/
public static DownloadAction deserializeFromStream(
Deserializer[] deserializers, InputStream input) throws IOException {
// Don't close the stream as it closes the underlying stream too.
DataInputStream dataInputStream = new DataInputStream(input);
String type = dataInputStream.readUTF();
int version = dataInputStream.readInt();
for (Deserializer deserializer : deserializers) {
if (type.equals(deserializer.type) && deserializer.version >= version) {
return deserializer.readFromStream(version, dataInputStream);
}
}
throw new DownloadException("No deserializer found for:" + type + ", " + version);
public static DownloadAction deserializeFromStream(InputStream input) throws IOException {
return readFromStream(new DataInputStream(input));
}
/** Serializes {@code action} type and data into the {@code output}. */
public static void serializeToStream(DownloadAction action, OutputStream output)
throws IOException {
// Don't close the stream as it closes the underlying stream too.
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(action.version);
action.writeToStream(dataOutputStream);
dataOutputStream.flush();
/**
* Creates a DASH download action.
*
* @param type The type of the action.
* @param uri The URI of the media to be downloaded.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
public static DownloadAction createDownloadAction(
String type,
Uri uri,
List<StreamKey> keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
return new DownloadAction(type, uri, /* isRemoveAction= */ false, keys, customCacheKey, data);
}
/**
* Creates a DASH remove action.
*
* @param type The type of the action.
* @param uri The URI of the media to be removed.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
public static DownloadAction createRemoveAction(
String type, Uri uri, @Nullable String customCacheKey, @Nullable byte[] data) {
return new DownloadAction(
type, uri, /* isRemoveAction= */ true, Collections.emptyList(), customCacheKey, data);
}
/** The type of the action. */
public final String type;
/** The action version. */
public final int version;
/** The uri being downloaded or removed. */
public final Uri uri;
/** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction;
/**
* Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action
* is a remove action.
*/
public final List<StreamKey> keys;
/** A custom key for cache indexing, or null. */
@Nullable public final String customCacheKey;
/** Custom data for this action. May be empty. */
public final byte[] data;
/**
* @param type The type of the action.
* @param version The action version.
* @param uri The uri being downloaded or removed.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if
* this action is a remove action.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Optional custom data for this action.
*/
protected DownloadAction(
String type, int version, Uri uri, boolean isRemoveAction, @Nullable byte[] data) {
private DownloadAction(
String type,
Uri uri,
boolean isRemoveAction,
List<StreamKey> keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
this.type = type;
this.version = version;
this.uri = uri;
this.isRemoveAction = isRemoveAction;
this.customCacheKey = customCacheKey;
this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY;
if (isRemoveAction) {
Assertions.checkArgument(keys.isEmpty());
this.keys = Collections.emptyList();
} else {
ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys);
Collections.sort(mutableKeys);
this.keys = Collections.unmodifiableList(mutableKeys);
}
}
/** Serializes itself into a byte array. */
public final byte[] toByteArray() {
public byte[] toByteArray() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
serializeToStream(this, output);
serializeToStream(output);
} catch (IOException e) {
// ByteArrayOutputStream shouldn't throw IOException.
throw new IllegalStateException();
@ -175,46 +152,118 @@ public abstract class DownloadAction {
/** Returns whether this is an action for the same media as the {@code other}. */
public boolean isSameMedia(DownloadAction other) {
return uri.equals(other.uri);
return customCacheKey == null
? other.customCacheKey == null && uri.equals(other.uri)
: customCacheKey.equals(other.customCacheKey);
}
/** Returns keys of tracks to be downloaded. */
public List<StreamKey> getKeys() {
return Collections.emptyList();
return keys;
}
/** Serializes itself into the {@code output}. */
protected abstract void writeToStream(DataOutputStream output) throws IOException;
/** Creates a {@link Downloader} with the given parameters. */
public abstract Downloader createDownloader(
DownloaderConstructorHelper downloaderConstructorHelper);
@SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(@Nullable Object o) {
if (o == null || getClass() != o.getClass()) {
if (!(o instanceof DownloadAction)) {
return false;
}
DownloadAction that = (DownloadAction) o;
return type.equals(that.type)
&& version == that.version
&& uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction
&& keys.equals(that.keys)
&& Util.areEqual(customCacheKey, that.customCacheKey)
&& Arrays.equals(data, that.data);
}
@Override
public int hashCode() {
int result = uri.hashCode();
public final int hashCode() {
int result = type.hashCode();
result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0);
result = 31 * result + keys.hashCode();
result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);
result = 31 * result + Arrays.hashCode(data);
return result;
}
private static Deserializer getDeserializer(Class<?> clazz)
throws NoSuchFieldException, IllegalAccessException {
Object value = clazz.getDeclaredField("DESERIALIZER").get(null);
return (Deserializer) Assertions.checkNotNull(value);
// Serialization.
/**
* Serializes this action into an {@link OutputStream}.
*
* @param output The stream to write to.
*/
public final void serializeToStream(OutputStream output) throws IOException {
// Don't close the stream as it closes the underlying stream too.
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(type);
dataOutputStream.writeInt(VERSION);
dataOutputStream.writeUTF(uri.toString());
dataOutputStream.writeBoolean(isRemoveAction);
dataOutputStream.writeInt(data.length);
dataOutputStream.write(data);
dataOutputStream.writeInt(keys.size());
for (int i = 0; i < keys.size(); i++) {
StreamKey key = keys.get(i);
dataOutputStream.writeInt(key.periodIndex);
dataOutputStream.writeInt(key.groupIndex);
dataOutputStream.writeInt(key.trackIndex);
}
dataOutputStream.writeBoolean(customCacheKey != null);
if (customCacheKey != null) {
dataOutputStream.writeUTF(customCacheKey);
}
dataOutputStream.flush();
}
private static DownloadAction readFromStream(DataInputStream input) throws IOException {
String type = input.readUTF();
int version = input.readInt();
Uri uri = Uri.parse(input.readUTF());
boolean isRemoveAction = input.readBoolean();
int dataLength = input.readInt();
byte[] data = new byte[dataLength];
input.readFully(data);
// Serialized version 0 progressive actions did not contain keys.
boolean isLegacyProgressive = version == 0 && TYPE_PROGRESSIVE.equals(type);
List<StreamKey> keys = new ArrayList<>();
if (!isLegacyProgressive) {
int keyCount = input.readInt();
for (int i = 0; i < keyCount; i++) {
keys.add(readKey(type, version, input));
}
}
// Serialized version 0 and 1 DASH/HLS/SS actions did not contain a custom cache key.
boolean isLegacySegmented =
version < 2 && (TYPE_DASH.equals(type) || TYPE_HLS.equals(type) || TYPE_SS.equals(type));
String customCacheKey = null;
if (!isLegacySegmented) {
customCacheKey = input.readBoolean() ? input.readUTF() : null;
}
return new DownloadAction(type, uri, isRemoveAction, keys, customCacheKey, data);
}
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 ((TYPE_HLS.equals(type) || 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);
}
}

View file

@ -28,9 +28,6 @@ import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@ -85,11 +82,10 @@ public final class DownloadManager {
private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false;
private final DownloaderConstructorHelper downloaderConstructorHelper;
private final int maxActiveDownloadTasks;
private final int minRetryCount;
private final ActionFile actionFile;
private final DownloadAction.Deserializer[] deserializers;
private final DownloaderFactory downloaderFactory;
private final ArrayList<Task> tasks;
private final ArrayList<Task> activeDownloadTasks;
private final Handler handler;
@ -102,71 +98,34 @@ public final class DownloadManager {
private boolean released;
private boolean downloadsStopped;
/**
* Creates a {@link DownloadManager}.
*
* @param cache Cache instance to be used to store downloaded data.
* @param upstreamDataSourceFactory A {@link DataSource.Factory} for creating data sources for
* downloading upstream data.
* @param actionSaveFile File to save active actions.
* @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link
* DownloadAction#getDefaultDeserializers()} is used instead.
*/
public DownloadManager(
Cache cache,
DataSource.Factory upstreamDataSourceFactory,
File actionSaveFile,
Deserializer... deserializers) {
this(
new DownloaderConstructorHelper(cache, upstreamDataSourceFactory),
actionSaveFile,
deserializers);
}
/**
* Constructs a {@link DownloadManager}.
*
* @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
* for downloading data.
* @param actionFile The file in which active actions are saved.
* @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link
* DownloadAction#getDefaultDeserializers()} is used instead.
* @param downloaderFactory A factory for creating {@link Downloader}s.
*/
public DownloadManager(
DownloaderConstructorHelper constructorHelper,
File actionFile,
Deserializer... deserializers) {
public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) {
this(
constructorHelper,
DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
DEFAULT_MIN_RETRY_COUNT,
actionFile,
deserializers);
actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT);
}
/**
* Constructs a {@link DownloadManager}.
*
* @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s
* for downloading data.
* @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s.
* @param maxSimultaneousDownloads The maximum number of simultaneous download tasks.
* @param minRetryCount The minimum number of times a task must be retried before failing.
* @param actionFile The file in which active actions are saved.
* @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link
* DownloadAction#getDefaultDeserializers()} is used instead.
*/
public DownloadManager(
DownloaderConstructorHelper constructorHelper,
int maxSimultaneousDownloads,
int minRetryCount,
File actionFile,
Deserializer... deserializers) {
this.downloaderConstructorHelper = constructorHelper;
DownloaderFactory downloaderFactory,
int maxSimultaneousDownloads,
int minRetryCount) {
this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory;
this.maxActiveDownloadTasks = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount;
this.actionFile = new ActionFile(actionFile);
this.deserializers =
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers();
this.downloadsStopped = true;
tasks = new ArrayList<>();
@ -239,7 +198,7 @@ public final class DownloadManager {
public int handleAction(byte[] actionData) throws IOException {
Assertions.checkState(!released);
ByteArrayInputStream input = new ByteArrayInputStream(actionData);
DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input);
DownloadAction action = DownloadAction.deserializeFromStream(input);
return handleAction(action);
}
@ -344,7 +303,7 @@ public final class DownloadManager {
}
private Task addTaskForAction(DownloadAction action) {
Task task = new Task(nextTaskId++, this, action, minRetryCount);
Task task = new Task(nextTaskId++, this, downloaderFactory, action, minRetryCount);
tasks.add(task);
logd("Task is added", task);
return task;
@ -450,7 +409,7 @@ public final class DownloadManager {
() -> {
DownloadAction[] loadedActions;
try {
loadedActions = actionFile.load(DownloadManager.this.deserializers);
loadedActions = actionFile.load();
logd("Action file is loaded.");
} catch (Throwable e) {
Log.e(TAG, "Action file loading failed.", e);
@ -642,6 +601,7 @@ public final class DownloadManager {
private final int id;
private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
private final DownloadAction action;
private final int minRetryCount;
private volatile @InternalState int currentState;
@ -650,9 +610,14 @@ public final class DownloadManager {
private Throwable error;
private Task(
int id, DownloadManager downloadManager, DownloadAction action, int minRetryCount) {
int id,
DownloadManager downloadManager,
DownloaderFactory downloaderFactory,
DownloadAction action,
int minRetryCount) {
this.id = id;
this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory;
this.action = action;
this.currentState = STATE_QUEUED;
this.minRetryCount = minRetryCount;
@ -807,7 +772,7 @@ public final class DownloadManager {
logd("Task is started", this);
Throwable error = null;
try {
downloader = action.createDownloader(downloadManager.downloaderConstructorHelper);
downloader = downloaderFactory.createDownloader(action);
if (action.isRemoveAction) {
downloader.remove();
} else {

View file

@ -0,0 +1,28 @@
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.offline;
/** Creates {@link Downloader Downloaders} for given {@link DownloadAction DownloadActions}. */
public interface DownloaderFactory {
/**
* Creates a {@link Downloader} to perform the given {@link DownloadAction}.
*
* @param action The action.
* @return The downloader.
*/
Downloader createDownloader(DownloadAction action);
}

View file

@ -1,138 +0,0 @@
/*
* 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 com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
/** An action to download or remove downloaded progressive streams. */
public final class ProgressiveDownloadAction extends DownloadAction {
private static final String TYPE = "progressive";
private static final int VERSION = 0;
public static final Deserializer DESERIALIZER =
new Deserializer(TYPE, VERSION) {
@Override
public ProgressiveDownloadAction readFromStream(int version, DataInputStream input)
throws IOException {
Uri uri = Uri.parse(input.readUTF());
boolean isRemoveAction = input.readBoolean();
int dataLength = input.readInt();
byte[] data = new byte[dataLength];
input.readFully(data);
String customCacheKey = input.readBoolean() ? input.readUTF() : null;
return new ProgressiveDownloadAction(uri, isRemoveAction, data, customCacheKey);
}
};
private final @Nullable String customCacheKey;
/**
* Creates a progressive stream download action.
*
* @param uri Uri of the data to be downloaded.
* @param data Optional custom data for this action.
* @param customCacheKey A custom key that uniquely identifies the original stream. If not null it
* is used for cache indexing.
*/
public static ProgressiveDownloadAction createDownloadAction(
Uri uri, @Nullable byte[] data, @Nullable String customCacheKey) {
return new ProgressiveDownloadAction(uri, /* isRemoveAction= */ false, data, customCacheKey);
}
/**
* Creates a progressive stream remove action.
*
* @param uri Uri of the data to be removed.
* @param data Optional custom data for this action.
* @param customCacheKey A custom key that uniquely identifies the original stream. If not null it
* is used for cache indexing.
*/
public static ProgressiveDownloadAction createRemoveAction(
Uri uri, @Nullable byte[] data, @Nullable String customCacheKey) {
return new ProgressiveDownloadAction(uri, /* isRemoveAction= */ true, data, customCacheKey);
}
/**
* @param uri Uri of the data to be downloaded.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action.
* @param data Optional custom data for this action.
* @param customCacheKey A custom key that uniquely identifies the original stream. If not null it
* is used for cache indexing.
* @deprecated Use {@link #createDownloadAction(Uri, byte[], String)} or {@link
* #createRemoveAction(Uri, byte[], String)}.
*/
@Deprecated
public ProgressiveDownloadAction(
Uri uri, boolean isRemoveAction, @Nullable byte[] data, @Nullable String customCacheKey) {
super(TYPE, VERSION, uri, isRemoveAction, data);
this.customCacheKey = customCacheKey;
}
@Override
public ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
return new ProgressiveDownloader(uri, customCacheKey, constructorHelper);
}
@Override
protected void writeToStream(DataOutputStream output) throws IOException {
output.writeUTF(uri.toString());
output.writeBoolean(isRemoveAction);
output.writeInt(data.length);
output.write(data);
boolean customCacheKeySet = customCacheKey != null;
output.writeBoolean(customCacheKeySet);
if (customCacheKeySet) {
output.writeUTF(customCacheKey);
}
}
@Override
public boolean isSameMedia(DownloadAction other) {
return ((other instanceof ProgressiveDownloadAction)
&& getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey()));
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!super.equals(o)) {
return false;
}
ProgressiveDownloadAction that = (ProgressiveDownloadAction) o;
return Util.areEqual(customCacheKey, that.customCacheKey);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);
return result;
}
private String getCacheKey() {
return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(uri);
}
}

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroupArray;
import java.util.Collections;
import java.util.List;
/** A {@link DownloadHelper} for progressive streams. */
@ -51,13 +52,18 @@ public final class ProgressiveDownloadHelper extends DownloadHelper {
}
@Override
public ProgressiveDownloadAction getDownloadAction(
@Nullable byte[] data, List<TrackKey> trackKeys) {
return ProgressiveDownloadAction.createDownloadAction(uri, data, customCacheKey);
public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri,
/* keys= */ Collections.emptyList(),
customCacheKey,
data);
}
@Override
public ProgressiveDownloadAction getRemoveAction(@Nullable byte[] data) {
return ProgressiveDownloadAction.createRemoveAction(uri, data, customCacheKey);
public DownloadAction getRemoveAction(@Nullable byte[] data) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey, data);
}
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache;
@ -47,7 +48,7 @@ public final class ProgressiveDownloader implements Downloader {
* @param constructorHelper A {@link DownloaderConstructorHelper} instance.
*/
public ProgressiveDownloader(
Uri uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) {
Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
this.dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, customCacheKey, 0);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.createCacheDataSource();

View file

@ -1,138 +0,0 @@
/*
* 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 com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** {@link DownloadAction} for {@link SegmentDownloader}s. */
public abstract class SegmentDownloadAction extends DownloadAction {
/** Base class for {@link SegmentDownloadAction} {@link Deserializer}s. */
protected abstract static class SegmentDownloadActionDeserializer extends Deserializer {
public SegmentDownloadActionDeserializer(String type, int version) {
super(type, version);
}
@Override
public final DownloadAction readFromStream(int version, DataInputStream input)
throws IOException {
Uri uri = Uri.parse(input.readUTF());
boolean isRemoveAction = input.readBoolean();
int dataLength = input.readInt();
byte[] data = new byte[dataLength];
input.readFully(data);
int keyCount = input.readInt();
List<StreamKey> keys = new ArrayList<>();
for (int i = 0; i < keyCount; i++) {
keys.add(readKey(version, input));
}
return createDownloadAction(uri, isRemoveAction, data, keys);
}
/** Deserializes a key from the {@code input}. */
protected StreamKey readKey(int version, DataInputStream input) throws IOException {
int periodIndex = input.readInt();
int groupIndex = input.readInt();
int trackIndex = input.readInt();
return new StreamKey(periodIndex, groupIndex, trackIndex);
}
/** Returns a {@link DownloadAction}. */
protected abstract DownloadAction createDownloadAction(
Uri manifestUri, boolean isRemoveAction, byte[] data, List<StreamKey> keys);
}
public final List<StreamKey> keys;
/**
* @param type The type of the action.
* @param version The action version.
* @param uri The URI of the media being downloaded.
* @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. If {@code
* removeAction} is true, {@code keys} must be empty.
*/
protected SegmentDownloadAction(
String type,
int version,
Uri uri,
boolean isRemoveAction,
@Nullable byte[] data,
List<StreamKey> keys) {
super(type, version, uri, isRemoveAction, data);
if (isRemoveAction) {
Assertions.checkArgument(keys.isEmpty());
this.keys = Collections.emptyList();
} else {
ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys);
Collections.sort(mutableKeys);
this.keys = Collections.unmodifiableList(mutableKeys);
}
}
@Override
public List<StreamKey> getKeys() {
return keys;
}
@Override
public final void writeToStream(DataOutputStream output) throws IOException {
output.writeUTF(uri.toString());
output.writeBoolean(isRemoveAction);
output.writeInt(data.length);
output.write(data);
output.writeInt(keys.size());
for (int i = 0; i < keys.size(); i++) {
writeKey(output, keys.get(i));
}
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!super.equals(o)) {
return false;
}
SegmentDownloadAction that = (SegmentDownloadAction) o;
return keys.equals(that.keys);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + keys.hashCode();
return result;
}
/** Serializes the {@code key} into the {@code output}. */
private void writeKey(DataOutputStream output, StreamKey key) throws IOException {
output.writeInt(key.periodIndex);
output.writeInt(key.groupIndex);
output.writeInt(key.trackIndex);
}
}

View file

@ -18,13 +18,13 @@ package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -38,10 +38,22 @@ import org.robolectric.RuntimeEnvironment;
public class ActionFileTest {
private File tempFile;
private DownloadAction action1;
private DownloadAction action2;
@Before
public void setUp() throws Exception {
tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
action1 =
buildAction(
DownloadAction.TYPE_PROGRESSIVE,
Uri.parse("http://test1.uri"),
TestUtil.buildTestData(16));
action2 =
buildAction(
DownloadAction.TYPE_PROGRESSIVE,
Uri.parse("http://test2.uri"),
TestUtil.buildTestData(32));
}
@After
@ -78,44 +90,32 @@ public class ActionFileTest {
@Test
public void testLoadAction() throws Exception {
byte[] data = Util.getUtf8Bytes("321");
DownloadAction[] actions =
loadActions(
new Object[] {
ActionFile.VERSION,
1, // Action count
"type2", // Action 1
FakeDownloadAction.VERSION,
data,
},
new FakeDeserializer("type2"));
action1
});
assertThat(actions).isNotNull();
assertThat(actions).hasLength(1);
assertAction(actions[0], "type2", FakeDownloadAction.VERSION, data);
assertThat(actions[0]).isEqualTo(action1);
}
@Test
public void testLoadActions() throws Exception {
byte[] data1 = Util.getUtf8Bytes("123");
byte[] data2 = Util.getUtf8Bytes("321");
DownloadAction[] actions =
loadActions(
new Object[] {
ActionFile.VERSION,
2, // Action count
"type1", // Action 1
FakeDownloadAction.VERSION,
data1,
"type2", // Action 2
FakeDownloadAction.VERSION,
data2,
},
new FakeDeserializer("type1"),
new FakeDeserializer("type2"));
action1,
action2,
});
assertThat(actions).isNotNull();
assertThat(actions).hasLength(2);
assertAction(actions[0], "type1", FakeDownloadAction.VERSION, data1);
assertAction(actions[1], "type2", FakeDownloadAction.VERSION, data2);
assertThat(actions[0]).isEqualTo(action1);
assertThat(actions[1]).isEqualTo(action2);
}
@Test
@ -125,90 +125,39 @@ public class ActionFileTest {
new Object[] {
ActionFile.VERSION + 1,
1, // Action count
"type2", // Action 1
FakeDownloadAction.VERSION,
Util.getUtf8Bytes("321"),
},
new FakeDeserializer("type2"));
action1,
});
Assert.fail();
} catch (IOException e) {
// Expected exception.
}
}
@Test
public void testLoadNotSupportedActionVersion() throws Exception {
try {
loadActions(
new Object[] {
ActionFile.VERSION,
1, // Action count
"type2", // Action 1
FakeDownloadAction.VERSION + 1,
Util.getUtf8Bytes("321"),
},
new FakeDeserializer("type2"));
Assert.fail();
} catch (IOException e) {
// Expected exception.
}
}
@Test
public void testLoadNotSupportedType() throws Exception {
try {
loadActions(
new Object[] {
ActionFile.VERSION,
1, // Action count
"type2", // Action 1
FakeDownloadAction.VERSION,
Util.getUtf8Bytes("321"),
},
new FakeDeserializer("type1"));
Assert.fail();
} catch (DownloadException e) {
// Expected exception.
}
}
@Test
public void testStoreAndLoadNoActions() throws Exception {
doTestSerializationRoundTrip(new DownloadAction[0]);
doTestSerializationRoundTrip();
}
@Test
public void testStoreAndLoadActions() throws Exception {
doTestSerializationRoundTrip(
new DownloadAction[] {
new FakeDownloadAction("type1", Util.getUtf8Bytes("123")),
new FakeDownloadAction("type2", Util.getUtf8Bytes("321")),
},
new FakeDeserializer("type1"),
new FakeDeserializer("type2"));
doTestSerializationRoundTrip(action1, action2);
}
private void doTestSerializationRoundTrip(DownloadAction[] actions,
Deserializer... deserializers) throws IOException {
private void doTestSerializationRoundTrip(DownloadAction... actions) throws IOException {
ActionFile actionFile = new ActionFile(tempFile);
actionFile.store(actions);
assertThat(actionFile.load(deserializers)).isEqualTo(actions);
assertThat(actionFile.load()).isEqualTo(actions);
}
private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers)
throws IOException {
private DownloadAction[] loadActions(Object[] values) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
try {
for (Object value : values) {
if (value instanceof Integer) {
dataOutputStream.writeInt((Integer) value);
} else if (value instanceof String) {
dataOutputStream.writeUTF((String) value);
} else if (value instanceof byte[]) {
byte[] data = (byte[]) value;
dataOutputStream.writeInt(data.length);
dataOutputStream.write(data);
} else if (value instanceof DownloadAction) {
((DownloadAction) value).serializeToStream(dataOutputStream);
} else {
throw new IllegalArgumentException();
}
@ -216,50 +165,11 @@ public class ActionFileTest {
} finally {
dataOutputStream.close();
}
return new ActionFile(tempFile).load(deserializers);
return new ActionFile(tempFile).load();
}
private static void assertAction(DownloadAction action, String type, int version, byte[] data) {
assertThat(action).isInstanceOf(FakeDownloadAction.class);
assertThat(action.type).isEqualTo(type);
assertThat(((FakeDownloadAction) action).version).isEqualTo(version);
assertThat(((FakeDownloadAction) action).data).isEqualTo(data);
private static DownloadAction buildAction(String type, Uri uri, byte[] data) {
return DownloadAction.createDownloadAction(
type, uri, /* keys= */ Collections.emptyList(), /* customCacheKey= */ null, data);
}
private static class FakeDeserializer extends Deserializer {
FakeDeserializer(String type) {
super(type, FakeDownloadAction.VERSION);
}
@Override
public DownloadAction readFromStream(int version, DataInputStream input) throws IOException {
int dataLength = input.readInt();
byte[] data = new byte[dataLength];
input.readFully(data);
return new FakeDownloadAction(type, data);
}
}
private static class FakeDownloadAction extends DownloadAction {
public static final int VERSION = 0;
private FakeDownloadAction(String type, byte[] data) {
super(type, VERSION, Uri.parse("http://test.com"), /* isRemoveAction= */ false, data);
}
@Override
protected void writeToStream(DataOutputStream output) throws IOException {
output.writeInt(data.length);
output.write(data);
}
@Override
public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) {
return null;
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link DefaultDownloaderFactory}. */
@RunWith(RobolectricTestRunner.class)
public final class DefaultDownloaderFactoryTest {
@Test
public void createProgressiveDownloader() throws Exception {
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
Downloader downloader =
factory.createDownloader(
DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
Uri.parse("https://www.test.com/download"),
/* keys= */ Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null));
assertThat(downloader).isInstanceOf(ProgressiveDownloader.class);
}
}

View file

@ -25,19 +25,17 @@ import com.google.android.exoplayer2.offline.DownloadManager.TaskState.State;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Util;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@ -63,6 +61,7 @@ public class DownloadManagerTest {
private DummyMainThread dummyMainThread;
private File actionFile;
private TestDownloadManagerListener downloadManagerListener;
private FakeDownloaderFactory downloaderFactory;
private DownloadManager downloadManager;
@Before
@ -73,6 +72,7 @@ public class DownloadManagerTest {
uri3 = Uri.parse("http://abc.com/media3");
dummyMainThread = new DummyMainThread();
actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
downloaderFactory = new FakeDownloaderFactory();
setUpDownloadManager(100);
}
@ -85,24 +85,24 @@ public class DownloadManagerTest {
@Test
public void testDownloadActionRuns() throws Throwable {
doTestActionRuns(createDownloadAction(uri1));
doTestDownloaderRuns(createDownloadRunner(uri1));
}
@Test
public void testRemoveActionRuns() throws Throwable {
doTestActionRuns(createRemoveAction(uri1));
doTestDownloaderRuns(createRemoveRunner(uri1));
}
@Test
public void testDownloadRetriesThenFails() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1);
downloadAction.post();
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader();
DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true;
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock();
}
downloadAction.assertFailed();
downloadRunner.assertFailed();
downloadManagerListener.clearDownloadError();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
@ -110,23 +110,23 @@ public class DownloadManagerTest {
@Test
public void testDownloadNoRetryWhenCanceled() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts();
downloadAction.getFakeDownloader().enableDownloadIOException = true;
downloadAction.post().assertStarted();
DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
downloadRunner.downloader.enableDownloadIOException = true;
downloadRunner.postAction().assertStarted();
FakeDownloadAction removeAction = createRemoveAction(uri1).post();
DownloadRunner removeRunner = createRemoveRunner(uri1).postAction();
downloadAction.unblock().assertCanceled();
removeAction.unblock();
downloadRunner.unblock().assertCanceled();
removeRunner.unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDownloadRetriesThenContinues() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1);
downloadAction.post();
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader();
DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true;
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY);
@ -135,7 +135,7 @@ public class DownloadManagerTest {
}
fakeDownloader.unblock();
}
downloadAction.assertCompleted();
downloadRunner.assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@ -143,9 +143,9 @@ public class DownloadManagerTest {
@Test
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public void testDownloadRetryCountResetsOnProgress() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1);
downloadAction.post();
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader();
DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true;
fakeDownloader.downloadedBytes = 0;
for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) {
@ -156,61 +156,61 @@ public class DownloadManagerTest {
}
fakeDownloader.unblock();
}
downloadAction.assertCompleted();
downloadRunner.assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri2));
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri2));
}
@Test
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction(uri1), createRemoveAction(uri2));
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createRemoveRunner(uri2));
}
@Test
public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri1));
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri1));
}
@Test
public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
doTestActionsRunSequentially(createDownloadAction(uri1), createRemoveAction(uri1));
doTestDownloadersRunSequentially(createDownloadRunner(uri1), createRemoveRunner(uri1));
}
@Test
public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction(uri1), createDownloadAction(uri1));
doTestDownloadersRunSequentially(createRemoveRunner(uri1), createDownloadRunner(uri1));
}
@Test
public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction(uri1), createRemoveAction(uri1));
doTestDownloadersRunSequentially(createRemoveRunner(uri1), createRemoveRunner(uri1));
}
@Test
public void testSameMediaMultipleActions() throws Throwable {
FakeDownloadAction downloadAction1 = createDownloadAction(uri1).ignoreInterrupts();
FakeDownloadAction downloadAction2 = createDownloadAction(uri1).ignoreInterrupts();
FakeDownloadAction removeAction1 = createRemoveAction(uri1);
FakeDownloadAction downloadAction3 = createDownloadAction(uri1);
FakeDownloadAction removeAction2 = createRemoveAction(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner downloadAction2 = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner removeAction1 = createRemoveRunner(uri1);
DownloadRunner downloadAction3 = createDownloadRunner(uri1);
DownloadRunner removeAction2 = createRemoveRunner(uri1);
// Two download actions run in parallel.
downloadAction1.post().assertStarted();
downloadAction2.post().assertStarted();
downloadAction1.postAction().assertStarted();
downloadAction2.postAction().assertStarted();
// removeAction1 is added. It interrupts the two download actions' threads but they are
// configured to ignore it so removeAction1 doesn't start.
removeAction1.post().assertDoesNotStart();
removeAction1.postAction().assertDoesNotStart();
// downloadAction2 finishes but it isn't enough to start removeAction1.
downloadAction2.unblock().assertCanceled();
removeAction1.assertDoesNotStart();
// downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish.
downloadAction3.post().assertDoesNotStart();
// downloadAction3 is postAction to DownloadManager but it waits for removeAction1 to finish.
downloadAction3.postAction().assertDoesNotStart();
// When downloadAction1 finishes, removeAction1 starts.
downloadAction1.unblock().assertCanceled();
@ -220,7 +220,7 @@ public class DownloadManagerTest {
// removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2
// starts immediately.
removeAction2.post();
removeAction2.postAction();
removeAction1.assertCanceled();
downloadAction3.assertCanceled();
removeAction2.assertStarted().unblock().assertCompleted();
@ -229,13 +229,13 @@ public class DownloadManagerTest {
@Test
public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable {
FakeDownloadAction removeAction1 = createRemoveAction(uri1).ignoreInterrupts();
FakeDownloadAction removeAction2 = createRemoveAction(uri1);
FakeDownloadAction removeAction3 = createRemoveAction(uri1);
DownloadRunner removeAction1 = createRemoveRunner(uri1).ignoreInterrupts();
DownloadRunner removeAction2 = createRemoveRunner(uri1);
DownloadRunner removeAction3 = createRemoveRunner(uri1);
removeAction1.post().assertStarted();
removeAction2.post().assertDoesNotStart();
removeAction3.post().assertDoesNotStart();
removeAction1.postAction().assertStarted();
removeAction2.postAction().assertDoesNotStart();
removeAction3.postAction().assertDoesNotStart();
removeAction2.assertCanceled();
@ -247,30 +247,30 @@ public class DownloadManagerTest {
@Test
public void testGetTasks() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction(uri1);
FakeDownloadAction downloadAction1 = createDownloadAction(uri1);
FakeDownloadAction downloadAction2 = createDownloadAction(uri1);
DownloadRunner removeAction = createRemoveRunner(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1);
DownloadRunner downloadAction2 = createDownloadRunner(uri1);
removeAction.post().assertStarted();
downloadAction1.post().assertDoesNotStart();
downloadAction2.post().assertDoesNotStart();
removeAction.postAction().assertStarted();
downloadAction1.postAction().assertDoesNotStart();
downloadAction2.postAction().assertDoesNotStart();
TaskState[] states = downloadManager.getAllTaskStates();
assertThat(states).hasLength(3);
assertThat(states[0].action).isEqualTo(removeAction);
assertThat(states[1].action).isEqualTo(downloadAction1);
assertThat(states[2].action).isEqualTo(downloadAction2);
assertThat(states[0].action).isEqualTo(removeAction.action);
assertThat(states[1].action).isEqualTo(downloadAction1.action);
assertThat(states[2].action).isEqualTo(downloadAction2.action);
}
@Test
public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction(uri1);
FakeDownloadAction downloadAction1 = createDownloadAction(uri1);
FakeDownloadAction downloadAction2 = createDownloadAction(uri1);
DownloadRunner removeAction = createRemoveRunner(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1);
DownloadRunner downloadAction2 = createDownloadRunner(uri1);
removeAction.post().assertStarted();
downloadAction1.post().assertDoesNotStart();
downloadAction2.post().assertDoesNotStart();
removeAction.postAction().assertStarted();
downloadAction1.postAction().assertDoesNotStart();
downloadAction2.postAction().assertDoesNotStart();
removeAction.unblock().assertCompleted();
downloadAction1.assertStarted();
@ -283,75 +283,75 @@ public class DownloadManagerTest {
@Test
public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction(uri1).ignoreInterrupts();
FakeDownloadAction downloadAction1 = createDownloadAction(uri1);
FakeDownloadAction downloadAction2 = createDownloadAction(uri2);
DownloadRunner removeRunner = createRemoveRunner(uri1).ignoreInterrupts();
DownloadRunner downloadRunner1 = createDownloadRunner(uri1);
DownloadRunner downloadRunner2 = createDownloadRunner(uri2);
removeAction.post().assertStarted();
downloadAction1.post().assertDoesNotStart();
downloadAction2.post().assertDoesNotStart();
removeRunner.postAction().assertStarted();
downloadRunner1.postAction().assertDoesNotStart();
downloadRunner2.postAction().assertDoesNotStart();
removeAction.unblock().assertCompleted();
downloadAction1.assertStarted();
downloadAction2.assertStarted();
downloadAction1.unblock().assertCompleted();
downloadAction2.unblock().assertCompleted();
removeRunner.unblock().assertCompleted();
downloadRunner1.assertStarted();
downloadRunner2.assertStarted();
downloadRunner1.unblock().assertCompleted();
downloadRunner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts();
FakeDownloadAction removeAction1 = createRemoveAction(uri1);
FakeDownloadAction removeAction2 = createRemoveAction(uri2);
DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner removeRunner1 = createRemoveRunner(uri1);
DownloadRunner removeRunner2 = createRemoveRunner(uri2);
downloadAction.post().assertStarted();
removeAction1.post().assertDoesNotStart();
removeAction2.post().assertStarted();
downloadRunner.postAction().assertStarted();
removeRunner1.postAction().assertDoesNotStart();
removeRunner2.postAction().assertStarted();
downloadAction.unblock().assertCanceled();
removeAction2.unblock().assertCompleted();
downloadRunner.unblock().assertCanceled();
removeRunner2.unblock().assertCompleted();
removeAction1.assertStarted();
removeAction1.unblock().assertCompleted();
removeRunner1.assertStarted();
removeRunner1.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testStopAndResume() throws Throwable {
FakeDownloadAction download1Action = createDownloadAction(uri1);
FakeDownloadAction remove2Action = createRemoveAction(uri2);
FakeDownloadAction download2Action = createDownloadAction(uri2);
FakeDownloadAction remove1Action = createRemoveAction(uri1);
FakeDownloadAction download3Action = createDownloadAction(uri3);
DownloadRunner download1Runner = createDownloadRunner(uri1);
DownloadRunner remove2Runner = createRemoveRunner(uri2);
DownloadRunner download2Runner = createDownloadRunner(uri2);
DownloadRunner remove1Runner = createRemoveRunner(uri1);
DownloadRunner download3Runner = createDownloadRunner(uri3);
download1Action.post().assertStarted();
remove2Action.post().assertStarted();
download2Action.post().assertDoesNotStart();
download1Runner.postAction().assertStarted();
remove2Runner.postAction().assertStarted();
download2Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.stopDownloads());
download1Action.assertStopped();
download1Runner.assertStopped();
// remove actions aren't stopped.
remove2Action.unblock().assertCompleted();
// Although remove2Action is finished, download2Action doesn't start.
download2Action.assertDoesNotStart();
remove2Runner.unblock().assertCompleted();
// Although remove2 is finished, download2 doesn't start.
download2Runner.assertDoesNotStart();
// When a new remove action is added, it cancels stopped download actions with the same media.
remove1Action.post();
download1Action.assertCanceled();
remove1Action.assertStarted().unblock().assertCompleted();
remove1Runner.postAction();
download1Runner.assertCanceled();
remove1Runner.assertStarted().unblock().assertCompleted();
// New download actions can be added but they don't start.
download3Action.post().assertDoesNotStart();
download3Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.startDownloads());
download2Action.assertStarted().unblock().assertCompleted();
download3Action.assertStarted().unblock().assertCompleted();
download2Runner.assertStarted().unblock().assertCompleted();
download3Runner.assertStarted().unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@ -359,34 +359,34 @@ public class DownloadManagerTest {
@Test
public void testResumeBeforeTotallyStopped() throws Throwable {
setUpDownloadManager(2);
FakeDownloadAction download1Action = createDownloadAction(uri1).ignoreInterrupts();
FakeDownloadAction download2Action = createDownloadAction(uri2);
FakeDownloadAction download3Action = createDownloadAction(uri3);
DownloadRunner download1Runner = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner download2Runner = createDownloadRunner(uri2);
DownloadRunner download3Runner = createDownloadRunner(uri3);
download1Action.post().assertStarted();
download2Action.post().assertStarted();
// download3Action doesn't start as DM was configured to run two downloads in parallel.
download3Action.post().assertDoesNotStart();
download1Runner.postAction().assertStarted();
download2Runner.postAction().assertStarted();
// download3 doesn't start as DM was configured to run two downloads in parallel.
download3Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.stopDownloads());
// download1Action doesn't stop yet as it ignores interrupts.
download2Action.assertStopped();
// download1 doesn't stop yet as it ignores interrupts.
download2Runner.assertStopped();
runOnMainThread(() -> downloadManager.startDownloads());
// download2Action starts immediately.
download2Action.assertStarted();
// download2 starts immediately.
download2Runner.assertStarted();
// download3Action doesn't start as download1Action still holds its slot.
download3Action.assertDoesNotStart();
// download3 doesn't start as download1 still holds its slot.
download3Runner.assertDoesNotStart();
// when unblocked download1Action stops and starts immediately.
download1Action.unblock().assertStopped().assertStarted();
// when unblocked download1 stops and starts immediately.
download1Runner.unblock().assertStopped().assertStarted();
download1Action.unblock();
download2Action.unblock();
download3Action.unblock();
download1Runner.unblock();
download2Runner.unblock();
download3Runner.unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@ -400,12 +400,7 @@ public class DownloadManagerTest {
() -> {
downloadManager =
new DownloadManager(
new DownloaderConstructorHelper(
Mockito.mock(Cache.class), DummyDataSource.FACTORY),
maxActiveDownloadTasks,
MIN_RETRY_COUNT,
actionFile,
ProgressiveDownloadAction.DESERIALIZER);
actionFile, downloaderFactory, maxActiveDownloadTasks, MIN_RETRY_COUNT);
downloadManagerListener =
new TestDownloadManagerListener(downloadManager, dummyMainThread);
downloadManager.addListener(downloadManagerListener);
@ -424,104 +419,104 @@ public class DownloadManagerTest {
}
}
private void doTestActionRuns(FakeDownloadAction action) throws Throwable {
action.post().assertStarted().unblock().assertCompleted();
private void doTestDownloaderRuns(DownloadRunner runner) throws Throwable {
runner.postAction().assertStarted().unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2)
private void doTestDownloadersRunSequentially(DownloadRunner runner1, DownloadRunner runner2)
throws Throwable {
action1.ignoreInterrupts().post().assertStarted();
action2.post().assertDoesNotStart();
runner1.ignoreInterrupts().postAction().assertStarted();
runner2.postAction().assertDoesNotStart();
action1.unblock();
action2.assertStarted();
runner1.unblock();
runner2.assertStarted();
action2.unblock().assertCompleted();
runner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2)
private void doTestDownloadersRunInParallel(DownloadRunner runner1, DownloadRunner runner2)
throws Throwable {
action1.post().assertStarted();
action2.post().assertStarted();
action1.unblock().assertCompleted();
action2.unblock().assertCompleted();
runner1.postAction().assertStarted();
runner2.postAction().assertStarted();
runner1.unblock().assertCompleted();
runner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private FakeDownloadAction createDownloadAction(Uri uri) {
return new FakeDownloadAction(uri, /* isRemoveAction= */ false);
private DownloadRunner createDownloadRunner(Uri uri) {
return new DownloadRunner(uri, /* isRemoveAction= */ false);
}
private FakeDownloadAction createRemoveAction(Uri uri) {
return new FakeDownloadAction(uri, /* isRemoveAction= */ true);
private DownloadRunner createRemoveRunner(Uri uri) {
return new DownloadRunner(uri, /* isRemoveAction= */ true);
}
private void runOnMainThread(final Runnable r) {
dummyMainThread.runOnMainThread(r);
}
private class FakeDownloadAction extends DownloadAction {
private class DownloadRunner {
private final FakeDownloader downloader;
public final DownloadAction action;
public final FakeDownloader downloader;
private FakeDownloadAction(Uri uri, boolean isRemoveAction) {
super("Fake", /* version= */ 0, uri, isRemoveAction, /* data= */ null);
this.downloader = new FakeDownloader(isRemoveAction);
private DownloadRunner(Uri uri, boolean isRemoveAction) {
action =
isRemoveAction
? DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE,
uri,
/* customCacheKey= */ null,
/* data= */ null)
: DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri,
/* keys= */ Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
downloader = new FakeDownloader(isRemoveAction);
downloaderFactory.putFakeDownloader(action, downloader);
}
@Override
protected void writeToStream(DataOutputStream output) {
// do nothing.
}
@Override
public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) {
return downloader;
}
private FakeDownloader getFakeDownloader() {
return downloader;
}
private FakeDownloadAction post() {
runOnMainThread(() -> downloadManager.handleAction(FakeDownloadAction.this));
private DownloadRunner postAction() {
runOnMainThread(() -> downloadManager.handleAction(action));
return this;
}
private FakeDownloadAction assertDoesNotStart() throws InterruptedException {
private DownloadRunner assertDoesNotStart() throws InterruptedException {
Thread.sleep(ASSERT_FALSE_TIME);
assertThat(downloader.started.getCount()).isEqualTo(1);
return this;
}
private FakeDownloadAction assertStarted() throws InterruptedException {
private DownloadRunner assertStarted() throws InterruptedException {
downloader.assertStarted(ASSERT_TRUE_TIMEOUT);
return assertState(TaskState.STATE_STARTED);
}
private FakeDownloadAction assertCompleted() {
private DownloadRunner assertCompleted() {
return assertState(TaskState.STATE_COMPLETED);
}
private FakeDownloadAction assertFailed() {
private DownloadRunner assertFailed() {
return assertState(TaskState.STATE_FAILED);
}
private FakeDownloadAction assertCanceled() {
private DownloadRunner assertCanceled() {
return assertState(TaskState.STATE_CANCELED);
}
private FakeDownloadAction assertStopped() {
private DownloadRunner assertStopped() {
return assertState(TaskState.STATE_QUEUED);
}
private FakeDownloadAction assertState(@State int expectedState) {
private DownloadRunner assertState(@State int expectedState) {
while (true) {
Integer state = null;
try {
state = downloadManagerListener.pollStateChange(this, ASSERT_TRUE_TIMEOUT);
state = downloadManagerListener.pollStateChange(action, ASSERT_TRUE_TIMEOUT);
} catch (InterruptedException e) {
fail(e.getMessage());
}
@ -531,36 +526,54 @@ public class DownloadManagerTest {
}
}
private FakeDownloadAction unblock() {
private DownloadRunner unblock() {
downloader.unblock();
return this;
}
private FakeDownloadAction ignoreInterrupts() {
private DownloadRunner ignoreInterrupts() {
downloader.ignoreInterrupts = true;
return this;
}
}
private static class FakeDownloaderFactory implements DownloaderFactory {
public IdentityHashMap<DownloadAction, FakeDownloader> downloaders;
public FakeDownloaderFactory() {
downloaders = new IdentityHashMap<>();
}
public void putFakeDownloader(DownloadAction action, FakeDownloader downloader) {
downloaders.put(action, downloader);
}
@Override
public Downloader createDownloader(DownloadAction action) {
return downloaders.get(action);
}
}
private static class FakeDownloader implements Downloader {
private final com.google.android.exoplayer2.util.ConditionVariable blocker;
private final boolean isRemoveAction;
private final boolean isRemove;
private CountDownLatch started;
private boolean ignoreInterrupts;
private volatile boolean enableDownloadIOException;
private volatile int downloadedBytes = C.LENGTH_UNSET;
private FakeDownloader(boolean isRemoveAction) {
this.isRemoveAction = isRemoveAction;
private FakeDownloader(boolean isRemove) {
this.isRemove = isRemove;
this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
}
@Override
public void download() throws InterruptedException, IOException {
assertThat(isRemoveAction).isFalse();
assertThat(isRemove).isFalse();
started.countDown();
block();
if (enableDownloadIOException) {
@ -575,7 +588,7 @@ public class DownloadManagerTest {
@Override
public void remove() throws InterruptedException {
assertThat(isRemoveAction).isTrue();
assertThat(isRemove).isTrue();
started.countDown();
block();
}

View file

@ -18,21 +18,19 @@ package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link ProgressiveDownloadAction}. */
// TODO: Merge into DownloadActionTest
/** Unit tests for progressive {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class)
public class ProgressiveDownloadActionTest {
@ -57,15 +55,6 @@ public class ProgressiveDownloadActionTest {
assertThat(action2.isRemoveAction).isTrue();
}
@Test
public void testCreateDownloader() throws Exception {
MockitoAnnotations.initMocks(this);
DownloadAction action = createDownloadAction(uri1, null);
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
assertThat(action.createDownloader(constructorHelper)).isNotNull();
}
@Test
public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception {
DownloadAction action1 = createRemoveAction(uri1, null);
@ -139,6 +128,13 @@ public class ProgressiveDownloadActionTest {
doTestSerializationRoundTrip(createRemoveAction(uri2, "key"));
}
@Test
public void testSerializerVersion0() throws Exception {
doTestLegacySerializationRoundTrip(createDownloadAction(uri1, "key"));
doTestLegacySerializationRoundTrip(createRemoveAction(uri1, "key"));
doTestLegacySerializationRoundTrip(createDownloadAction(uri2, "key"));
}
private void assertSameMedia(DownloadAction action1, DownloadAction action2) {
assertThat(action1.isSameMedia(action2)).isTrue();
assertThat(action2.isSameMedia(action1)).isTrue();
@ -149,25 +145,61 @@ public class ProgressiveDownloadActionTest {
assertThat(action2.isSameMedia(action1)).isFalse();
}
private static void assertEqual(DownloadAction action1, DownloadAction action2) {
assertThat(action1).isEqualTo(action2);
assertThat(action2).isEqualTo(action1);
}
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output);
action.serializeToStream(output);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in);
DownloadAction action2 =
DownloadAction.deserializeFromStream(
new DownloadAction.Deserializer[] {ProgressiveDownloadAction.DESERIALIZER}, input);
DownloadAction action2 = DownloadAction.deserializeFromStream(input);
assertThat(action2).isEqualTo(action);
}
private static void doTestLegacySerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(/* version= */ 0);
dataOutputStream.writeUTF(action.uri.toString());
dataOutputStream.writeBoolean(action.isRemoveAction);
dataOutputStream.writeInt(action.data.length);
dataOutputStream.write(action.data);
boolean customCacheKeySet = action.customCacheKey != null;
output.writeBoolean(customCacheKeySet);
if (customCacheKeySet) {
output.writeUTF(action.customCacheKey);
}
dataOutputStream.flush();
assertEqual(action, deserializeActionFromStream(out));
}
private static DownloadAction deserializeActionFromStream(ByteArrayOutputStream out)
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in);
return DownloadAction.deserializeFromStream(input);
}
private static DownloadAction createDownloadAction(Uri uri1, String customCacheKey) {
return ProgressiveDownloadAction.createDownloadAction(uri1, null, customCacheKey);
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri1,
/* keys= */ Collections.emptyList(),
customCacheKey,
/* data= */ null);
}
private static DownloadAction createRemoveAction(Uri uri1, String customCacheKey) {
return ProgressiveDownloadAction.createRemoveAction(uri1, null, customCacheKey);
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri1, customCacheKey, /* data= */ null);
}
}

View file

@ -1,84 +0,0 @@
/*
* 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 com.google.android.exoplayer2.source.dash.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
import com.google.android.exoplayer2.offline.StreamKey;
import java.util.Collections;
import java.util.List;
/** An action to download or remove downloaded DASH streams. */
public final class DashDownloadAction extends SegmentDownloadAction {
private static final String TYPE = "dash";
private static final int VERSION = 0;
public static final Deserializer DESERIALIZER =
new SegmentDownloadActionDeserializer(TYPE, VERSION) {
@Override
protected DownloadAction createDownloadAction(
Uri uri, boolean isRemoveAction, byte[] data, List<StreamKey> keys) {
return new DashDownloadAction(uri, isRemoveAction, data, keys);
}
};
/**
* Creates a DASH download action.
*
* @param uri The URI of the media to be downloaded.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
*/
public static DashDownloadAction createDownloadAction(
Uri uri, @Nullable byte[] data, List<StreamKey> keys) {
return new DashDownloadAction(uri, /* isRemoveAction= */ false, data, keys);
}
/**
* Creates a DASH remove action.
*
* @param uri The URI of the media to be removed.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
public static DashDownloadAction createRemoveAction(Uri uri, @Nullable byte[] data) {
return new DashDownloadAction(uri, /* isRemoveAction= */ true, data, Collections.emptyList());
}
/**
* @param uri The DASH manifest URI.
* @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded.
* @param data Optional custom data for this action.
* @param keys Keys of representations to be downloaded. If empty, all representations are
* downloaded. If {@code removeAction} is true, {@code keys} must be empty.
* @deprecated Use {@link #createDownloadAction(Uri, byte[], List)} or {@link
* #createRemoveAction(Uri, byte[])}.
*/
@Deprecated
public DashDownloadAction(
Uri uri, boolean isRemoveAction, @Nullable byte[] data, List<StreamKey> keys) {
super(TYPE, VERSION, uri, isRemoveAction, data, keys);
}
@Override
public DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
return new DashDownloader(uri, keys, constructorHelper);
}
}

View file

@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
@ -86,13 +87,15 @@ public final class DashDownloadHelper extends DownloadHelper {
}
@Override
public DashDownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return DashDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys));
public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data);
}
@Override
public DashDownloadAction getRemoveAction(@Nullable byte[] data) {
return DashDownloadAction.createRemoveAction(uri, data);
public DownloadAction getRemoveAction(@Nullable byte[] data) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data);
}
private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {

View file

@ -19,10 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@ -33,11 +30,10 @@ import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link DashDownloadAction}. */
// TODO: Merge into DownloadActionTest
/** Unit tests for DASH {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class)
public class DashDownloadActionTest {
@ -62,15 +58,6 @@ public class DashDownloadActionTest {
assertThat(action2.isRemoveAction).isTrue();
}
@Test
public void testCreateDownloader() {
MockitoAnnotations.initMocks(this);
DownloadAction action = createDownloadAction(uri1);
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
assertThat(action.createDownloader(constructorHelper)).isNotNull();
}
@Test
public void testSameUriDifferentAction_IsSameMedia() {
DownloadAction action1 = createRemoveAction(uri1);
@ -141,6 +128,14 @@ public class DashDownloadActionTest {
createDownloadAction(uri2, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1)));
}
@Test
public void testSerializerVersion0() throws Exception {
doTestLegacySerializationRoundTrip(createDownloadAction(uri1));
doTestLegacySerializationRoundTrip(createRemoveAction(uri1));
doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1)));
}
private static void assertNotEqual(DownloadAction action1, DownloadAction action2) {
assertThat(action1).isNotEqualTo(action2);
assertThat(action2).isNotEqualTo(action1);
@ -154,24 +149,53 @@ public class DashDownloadActionTest {
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output);
action.serializeToStream(output);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in);
DownloadAction action2 =
DownloadAction.deserializeFromStream(
new DownloadAction.Deserializer[] {DashDownloadAction.DESERIALIZER}, input);
DownloadAction action2 = DownloadAction.deserializeFromStream(input);
assertThat(action).isEqualTo(action2);
}
private static void doTestLegacySerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(/* version= */ 0);
dataOutputStream.writeUTF(action.uri.toString());
dataOutputStream.writeBoolean(action.isRemoveAction);
dataOutputStream.writeInt(action.data.length);
dataOutputStream.write(action.data);
dataOutputStream.writeInt(action.keys.size());
for (int i = 0; i < action.keys.size(); i++) {
StreamKey key = action.keys.get(i);
dataOutputStream.writeInt(key.periodIndex);
dataOutputStream.writeInt(key.groupIndex);
dataOutputStream.writeInt(key.trackIndex);
}
dataOutputStream.flush();
assertEqual(action, deserializeActionFromStream(out));
}
private static DownloadAction deserializeActionFromStream(ByteArrayOutputStream out)
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in);
return DownloadAction.deserializeFromStream(input);
}
private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
ArrayList<StreamKey> keysList = new ArrayList<>();
Collections.addAll(keysList, keys);
return DashDownloadAction.createDownloadAction(uri, null, keysList);
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, /* data= */ null);
}
private static DownloadAction createRemoveAction(Uri uri) {
return DashDownloadAction.createRemoveAction(uri, null);
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, /* data= */ null);
}
}

View file

@ -25,14 +25,21 @@ import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadException;
import com.google.android.exoplayer2.offline.Downloader;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
@ -44,6 +51,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@ -67,6 +75,23 @@ public class DashDownloaderTest {
Util.recursiveDelete(tempFolder);
}
@Test
public void testCreateWithDefaultDownloaderFactory() throws Exception {
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
Downloader downloader =
factory.createDownloader(
DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH,
Uri.parse("https://www.test.com/download"),
Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)),
/* customCacheKey= */ null,
/* data= */ null));
assertThat(downloader).isInstanceOf(DashDownloader.class);
}
@Test
public void testDownloadRepresentation() throws Exception {
FakeDataSet fakeDataSet =

View file

@ -25,6 +25,7 @@ import android.content.Context;
import android.net.Uri;
import android.os.ConditionVariable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
@ -238,11 +239,11 @@ public class DownloadManagerDashTest {
Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet);
downloadManager =
new DownloadManager(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory),
/* maxSimultaneousDownloads= */ 1,
/* minRetryCount= */ 3,
actionFile,
DashDownloadAction.DESERIALIZER);
new DefaultDownloaderFactory(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory)),
/* maxSimultaneousDownloads= */ 1,
/* minRetryCount= */ 3);
downloadManagerListener =
new TestDownloadManagerListener(downloadManager, dummyMainThread);
@ -257,9 +258,13 @@ public class DownloadManagerDashTest {
Collections.addAll(keysList, keys);
DownloadAction result;
if (isRemoveAction) {
result = DashDownloadAction.createRemoveAction(uri, data);
result =
DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data);
} else {
result = DashDownloadAction.createDownloadAction(uri, data, keysList);
result =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, data);
}
return result;
}

View file

@ -24,6 +24,7 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService;
@ -116,11 +117,11 @@ public class DownloadServiceDashTest {
actionFile.delete();
final DownloadManager dashDownloadManager =
new DownloadManager(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory),
1,
3,
actionFile,
DashDownloadAction.DESERIALIZER);
new DefaultDownloaderFactory(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory)),
/* maxSimultaneousDownloads= */ 1,
/* minRetryCount= */ 3);
downloadManagerListener =
new TestDownloadManagerListener(dashDownloadManager, dummyMainThread);
dashDownloadManager.addListener(downloadManagerListener);
@ -211,9 +212,13 @@ public class DownloadServiceDashTest {
Collections.addAll(keysList, keys);
DownloadAction result;
if (isRemoveAction) {
result = DashDownloadAction.createRemoveAction(uri, data);
result =
DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data);
} else {
result = DashDownloadAction.createDownloadAction(uri, data, keysList);
result =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, data);
}
return result;
}

View file

@ -1,97 +0,0 @@
/*
* 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 com.google.android.exoplayer2.source.hls.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
import com.google.android.exoplayer2.offline.StreamKey;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/** An action to download or remove downloaded HLS streams. */
public final class HlsDownloadAction extends SegmentDownloadAction {
private static final String TYPE = "hls";
private static final int VERSION = 1;
public static final Deserializer DESERIALIZER =
new SegmentDownloadActionDeserializer(TYPE, VERSION) {
@Override
protected StreamKey readKey(int version, DataInputStream input) throws IOException {
if (version > 0) {
return super.readKey(version, input);
}
int renditionGroup = input.readInt();
int trackIndex = input.readInt();
return new StreamKey(renditionGroup, trackIndex);
}
@Override
protected DownloadAction createDownloadAction(
Uri uri, boolean isRemoveAction, byte[] data, List<StreamKey> keys) {
return new HlsDownloadAction(uri, isRemoveAction, data, keys);
}
};
/**
* Creates a HLS download action.
*
* @param uri The URI of the media to be downloaded.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
*/
public static HlsDownloadAction createDownloadAction(
Uri uri, @Nullable byte[] data, List<StreamKey> keys) {
return new HlsDownloadAction(uri, /* isRemoveAction= */ false, data, keys);
}
/**
* Creates a HLS remove action.
*
* @param uri The URI of the media to be removed.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
public static HlsDownloadAction createRemoveAction(Uri uri, @Nullable byte[] data) {
return new HlsDownloadAction(uri, /* isRemoveAction= */ true, data, Collections.emptyList());
}
/**
* @param uri The HLS playlist URI.
* @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded.
* @param data Optional custom data for this action.
* @param keys Keys of renditions to be downloaded. If empty, all renditions are downloaded. If
* {@code removeAction} is true, {@code keys} must empty.
* @deprecated Use {@link #createDownloadAction(Uri, byte[], List)} or {@link
* #createRemoveAction(Uri, byte[])}.
*/
@Deprecated
public HlsDownloadAction(
Uri uri, boolean isRemoveAction, @Nullable byte[] data, List<StreamKey> keys) {
super(TYPE, VERSION, uri, isRemoveAction, data, keys);
}
@Override
public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
return new HlsDownloader(uri, keys, constructorHelper);
}
}

View file

@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
@ -97,15 +98,20 @@ public final class HlsDownloadHelper extends DownloadHelper {
}
@Override
public HlsDownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
Assertions.checkNotNull(renditionGroups);
return HlsDownloadAction.createDownloadAction(
uri, data, toStreamKeys(trackKeys, renditionGroups));
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_HLS,
uri,
toStreamKeys(trackKeys, renditionGroups),
/* customCacheKey= */ null,
data);
}
@Override
public HlsDownloadAction getRemoveAction(@Nullable byte[] data) {
return HlsDownloadAction.createRemoveAction(uri, data);
public DownloadAction getRemoveAction(@Nullable byte[] data) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null, data);
}
private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) {

View file

@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@ -33,11 +31,10 @@ import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link HlsDownloadAction}. */
// TODO: Merge into DownloadActionTest
/** Unit tests for HLS {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class)
public class HlsDownloadActionTest {
@ -62,15 +59,6 @@ public class HlsDownloadActionTest {
assertThat(action2.isRemoveAction).isTrue();
}
@Test
public void testCreateDownloader() {
MockitoAnnotations.initMocks(this);
DownloadAction action = createDownloadAction(uri1);
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
assertThat(action.createDownloader(constructorHelper)).isNotNull();
}
@Test
public void testSameUriDifferentAction_IsSameMedia() {
DownloadAction action1 = createRemoveAction(uri1);
@ -140,10 +128,18 @@ public class HlsDownloadActionTest {
@Test
public void testSerializerVersion0() throws Exception {
doTestSerializationV0RoundTrip(createDownloadAction(uri1));
doTestSerializationV0RoundTrip(createRemoveAction(uri1));
doTestSerializationV0RoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)));
doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 0);
doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 0);
doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 0);
}
@Test
public void testSerializerVersion1() throws Exception {
doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 1);
doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 1);
doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 1);
}
private static void assertNotEqual(DownloadAction action1, DownloadAction action2) {
@ -159,17 +155,19 @@ public class HlsDownloadActionTest {
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output);
action.serializeToStream(output);
assertEqual(action, deserializeActionFromStream(out));
}
private static void doTestSerializationV0RoundTrip(HlsDownloadAction action) throws IOException {
private static void doTestLegacySerializationRoundTrip(DownloadAction action, int version)
throws IOException {
Assertions.checkState(version == 0 || version == 1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(/* version */ 0);
dataOutputStream.writeInt(version);
dataOutputStream.writeUTF(action.uri.toString());
dataOutputStream.writeBoolean(action.isRemoveAction);
dataOutputStream.writeInt(action.data.length);
@ -177,6 +175,9 @@ public class HlsDownloadActionTest {
dataOutputStream.writeInt(action.keys.size());
for (int i = 0; i < action.keys.size(); i++) {
StreamKey key = action.keys.get(i);
if (version == 1) {
dataOutputStream.writeInt(key.periodIndex);
}
dataOutputStream.writeInt(key.groupIndex);
dataOutputStream.writeInt(key.trackIndex);
}
@ -189,17 +190,18 @@ public class HlsDownloadActionTest {
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in);
return DownloadAction.deserializeFromStream(
new DownloadAction.Deserializer[] {HlsDownloadAction.DESERIALIZER}, input);
return DownloadAction.deserializeFromStream(input);
}
private static HlsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
ArrayList<StreamKey> keysList = new ArrayList<>();
Collections.addAll(keysList, keys);
return HlsDownloadAction.createDownloadAction(uri, null, keysList);
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_HLS, uri, keysList, /* customCacheKey= */ null, /* data= */ null);
}
private static HlsDownloadAction createRemoveAction(Uri uri) {
return HlsDownloadAction.createRemoveAction(uri, null);
private static DownloadAction createRemoveAction(Uri uri) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null, /* data= */ null);
}
}

View file

@ -35,21 +35,29 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.Downloader;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@ -84,6 +92,23 @@ public class HlsDownloaderTest {
Util.recursiveDelete(tempFolder);
}
@Test
public void testCreateWithDefaultDownloaderFactory() throws Exception {
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
Downloader downloader =
factory.createDownloader(
DownloadAction.createDownloadAction(
DownloadAction.TYPE_HLS,
Uri.parse("https://www.test.com/download"),
Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)),
/* customCacheKey= */ null,
/* data= */ null));
assertThat(downloader).isInstanceOf(HlsDownloader.class);
}
@Test
public void testCounterMethods() throws Exception {
HlsDownloader downloader =

View file

@ -1,97 +0,0 @@
/*
* 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 com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
import com.google.android.exoplayer2.offline.StreamKey;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/** An action to download or remove downloaded SmoothStreaming streams. */
public final class SsDownloadAction extends SegmentDownloadAction {
private static final String TYPE = "ss";
private static final int VERSION = 1;
public static final Deserializer DESERIALIZER =
new SegmentDownloadActionDeserializer(TYPE, VERSION) {
@Override
protected StreamKey readKey(int version, DataInputStream input) throws IOException {
if (version > 0) {
return super.readKey(version, input);
}
int groupIndex = input.readInt();
int trackIndex = input.readInt();
return new StreamKey(groupIndex, trackIndex);
}
@Override
protected DownloadAction createDownloadAction(
Uri uri, boolean isRemoveAction, byte[] data, List<StreamKey> keys) {
return new SsDownloadAction(uri, isRemoveAction, data, keys);
}
};
/**
* Creates a SmoothStreaming download action.
*
* @param uri The URI of the media to be downloaded.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
*/
public static SsDownloadAction createDownloadAction(
Uri uri, @Nullable byte[] data, List<StreamKey> keys) {
return new SsDownloadAction(uri, /* isRemoveAction= */ false, data, keys);
}
/**
* Creates a SmoothStreaming remove action.
*
* @param uri The URI of the media to be removed.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
public static SsDownloadAction createRemoveAction(Uri uri, @Nullable byte[] data) {
return new SsDownloadAction(uri, /* isRemoveAction= */ true, data, Collections.emptyList());
}
/**
* @param uri The SmoothStreaming manifest URI.
* @param isRemoveAction Whether the data will be removed. If {@code false} it will be downloaded.
* @param data Optional custom data for this action.
* @param keys Keys of streams to be downloaded. If empty, all streams are downloaded. If {@code
* removeAction} is true, {@code keys} must be empty.
* @deprecated Use {@link #createDownloadAction(Uri, byte[], List)} or {@link
* #createRemoveAction(Uri, byte[])}.
*/
@Deprecated
public SsDownloadAction(
Uri uri, boolean isRemoveAction, @Nullable byte[] data, List<StreamKey> keys) {
super(TYPE, VERSION, uri, isRemoveAction, data, keys);
}
@Override
public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) {
return new SsDownloader(uri, keys, constructorHelper);
}
}

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
@ -76,13 +77,15 @@ public final class SsDownloadHelper extends DownloadHelper {
}
@Override
public SsDownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return SsDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys));
public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_SS, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data);
}
@Override
public SsDownloadAction getRemoveAction(@Nullable byte[] data) {
return SsDownloadAction.createRemoveAction(uri, data);
public DownloadAction getRemoveAction(@Nullable byte[] data) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null, data);
}
private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {

View file

@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@ -33,11 +31,10 @@ import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link SsDownloadAction}. */
// TODO: Merge into DownloadActionTest
/** Unit tests for SmoothStreaming {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class)
public class SsDownloadActionTest {
@ -62,15 +59,6 @@ public class SsDownloadActionTest {
assertThat(action2.isRemoveAction).isTrue();
}
@Test
public void testCreateDownloader() {
MockitoAnnotations.initMocks(this);
DownloadAction action = createDownloadAction(uri1);
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
assertThat(action.createDownloader(constructorHelper)).isNotNull();
}
@Test
public void testSameUriDifferentAction_IsSameMedia() {
DownloadAction action1 = createRemoveAction(uri1);
@ -140,10 +128,18 @@ public class SsDownloadActionTest {
@Test
public void testSerializerVersion0() throws Exception {
doTestSerializationV0RoundTrip(createDownloadAction(uri1));
doTestSerializationV0RoundTrip(createRemoveAction(uri1));
doTestSerializationV0RoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)));
doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 0);
doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 0);
doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 0);
}
@Test
public void testSerializerVersion1() throws Exception {
doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 1);
doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 1);
doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 1);
}
private static void assertNotEqual(DownloadAction action1, DownloadAction action2) {
@ -159,17 +155,19 @@ public class SsDownloadActionTest {
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output);
action.serializeToStream(output);
assertEqual(action, deserializeActionFromStream(out));
}
private static void doTestSerializationV0RoundTrip(SsDownloadAction action) throws IOException {
private static void doTestLegacySerializationRoundTrip(DownloadAction action, int version)
throws IOException {
Assertions.checkState(version == 0 || version == 1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out);
DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(/* version */ 0);
dataOutputStream.writeInt(version);
dataOutputStream.writeUTF(action.uri.toString());
dataOutputStream.writeBoolean(action.isRemoveAction);
dataOutputStream.writeInt(action.data.length);
@ -177,6 +175,9 @@ public class SsDownloadActionTest {
dataOutputStream.writeInt(action.keys.size());
for (int i = 0; i < action.keys.size(); i++) {
StreamKey key = action.keys.get(i);
if (version == 1) {
dataOutputStream.writeInt(key.periodIndex);
}
dataOutputStream.writeInt(key.groupIndex);
dataOutputStream.writeInt(key.trackIndex);
}
@ -189,17 +190,18 @@ public class SsDownloadActionTest {
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in);
return DownloadAction.deserializeFromStream(
new DownloadAction.Deserializer[] {SsDownloadAction.DESERIALIZER}, input);
return DownloadAction.deserializeFromStream(input);
}
private static SsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
ArrayList<StreamKey> keysList = new ArrayList<>();
Collections.addAll(keysList, keys);
return SsDownloadAction.createDownloadAction(uri, null, keysList);
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_SS, uri, keysList, /* customCacheKey= */ null, /* data= */ null);
}
private static SsDownloadAction createRemoveAction(Uri uri) {
return SsDownloadAction.createRemoveAction(uri, null);
private static DownloadAction createRemoveAction(Uri uri) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null, /* data= */ null);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.source.smoothstreaming.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.Downloader;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link SsDownloader}. */
@RunWith(RobolectricTestRunner.class)
public final class SsDownloaderTest {
@Test
public void createWithDefaultDownloaderFactory() throws Exception {
DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
Downloader downloader =
factory.createDownloader(
DownloadAction.createDownloadAction(
DownloadAction.TYPE_SS,
Uri.parse("https://www.test.com/download"),
Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)),
/* customCacheKey= */ null,
/* data= */ null));
assertThat(downloader).isInstanceOf(SsDownloader.class);
}
}