Commit 6ebb6124 by olly Committed by Oliver Woodman

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
parent 527f2cf7
Showing with 957 additions and 1180 deletions
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
...@@ -87,10 +88,10 @@ public class DemoApplication extends Application { ...@@ -87,10 +88,10 @@ public class DemoApplication extends Application {
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
downloaderConstructorHelper, new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
new DefaultDownloaderFactory(downloaderConstructorHelper),
MAX_SIMULTANEOUS_DOWNLOADS, MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT, DownloadManager.DEFAULT_MIN_RETRY_COUNT);
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
downloadTracker = downloadTracker =
new DownloadTracker( new DownloadTracker(
/* context= */ this, /* context= */ this,
......
...@@ -81,11 +81,7 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -81,11 +81,7 @@ public class DownloadTracker implements DownloadManager.Listener {
private final ActionFile actionFile; private final ActionFile actionFile;
private final Handler actionFileWriteHandler; private final Handler actionFileWriteHandler;
public DownloadTracker( public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) {
Context context,
DataSource.Factory dataSourceFactory,
File actionFile,
DownloadAction.Deserializer... deserializers) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.actionFile = new ActionFile(actionFile); this.actionFile = new ActionFile(actionFile);
...@@ -95,8 +91,7 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -95,8 +91,7 @@ public class DownloadTracker implements DownloadManager.Listener {
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
actionFileWriteThread.start(); actionFileWriteThread.start();
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
loadTrackedActions( loadTrackedActions();
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
} }
public void addListener(Listener listener) { public void addListener(Listener listener) {
...@@ -158,9 +153,9 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -158,9 +153,9 @@ public class DownloadTracker implements DownloadManager.Listener {
// Internal methods // Internal methods
private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { private void loadTrackedActions() {
try { try {
DownloadAction[] allActions = actionFile.load(deserializers); DownloadAction[] allActions = actionFile.load();
for (DownloadAction action : allActions) { for (DownloadAction action : allActions) {
trackedDownloadStates.put(action.uri, action); trackedDownloadStates.put(action.uri, action);
} }
......
...@@ -30,10 +30,19 @@ ...@@ -30,10 +30,19 @@
<init>(); <init>();
} }
# Constructors accessed via reflection in DownloadAction # Constructors accessed via reflection in DefaultDownloaderFactory
-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction -dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader
-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction -keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader {
-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction <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 # Don't warn about checkerframework
-dontwarn org.checkerframework.** -dontwarn org.checkerframework.**
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.offline; 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.AtomicFile;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream; import java.io.DataInputStream;
...@@ -45,11 +44,10 @@ public final class ActionFile { ...@@ -45,11 +44,10 @@ public final class ActionFile {
/** /**
* Loads {@link DownloadAction}s from file. * 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. * @return Loaded DownloadActions. If the action file doesn't exists returns an empty array.
* @throws IOException If there is an error during loading. * @throws IOException If there is an error during loading.
*/ */
public DownloadAction[] load(Deserializer... deserializers) throws IOException { public DownloadAction[] load() throws IOException {
if (!actionFile.exists()) { if (!actionFile.exists()) {
return new DownloadAction[0]; return new DownloadAction[0];
} }
...@@ -64,7 +62,7 @@ public final class ActionFile { ...@@ -64,7 +62,7 @@ public final class ActionFile {
int actionCount = dataInputStream.readInt(); int actionCount = dataInputStream.readInt();
DownloadAction[] actions = new DownloadAction[actionCount]; DownloadAction[] actions = new DownloadAction[actionCount];
for (int i = 0; i < actionCount; i++) { for (int i = 0; i < actionCount; i++) {
actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream); actions[i] = DownloadAction.deserializeFromStream(dataInputStream);
} }
return actions; return actions;
} finally { } finally {
...@@ -85,7 +83,7 @@ public final class ActionFile { ...@@ -85,7 +83,7 @@ public final class ActionFile {
output.writeInt(VERSION); output.writeInt(VERSION);
output.writeInt(downloadActions.length); output.writeInt(downloadActions.length);
for (DownloadAction action : downloadActions) { for (DownloadAction action : downloadActions) {
DownloadAction.serializeToStream(action, output); action.serializeToStream(output);
} }
atomicFile.endWrite(output); atomicFile.endWrite(output);
// Avoid calling close twice. // Avoid calling close twice.
......
/*
* 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)
}
...@@ -25,147 +25,124 @@ import java.io.DataOutputStream; ...@@ -25,147 +25,124 @@ import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
/** Contains the necessary parameters for a download or remove action. */ /** Contains the necessary parameters for a download or remove action. */
public abstract class DownloadAction { public final class DownloadAction {
/** Used to deserialize {@link DownloadAction}s. */ /** Type for progressive downloads. */
public abstract static class Deserializer { 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; private static final int VERSION = 2;
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;
}
/** /**
* Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction, * Deserializes one action that was serialized with {@link #serializeToStream(OutputStream)} from
* OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the * the {@code input}.
* action's type.
* *
* <p>The caller is responsible for closing the given {@link InputStream}. * <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.
* @param input The stream from which to read the action.
* @return The deserialized action. * @return The deserialized action.
* @throws IOException If there is an IO error reading from {@code input}, or if the action type * @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}. * isn't supported by any of the {@code deserializers}.
*/ */
public static DownloadAction deserializeFromStream( public static DownloadAction deserializeFromStream(InputStream input) throws IOException {
Deserializer[] deserializers, InputStream input) throws IOException { return readFromStream(new DataInputStream(input));
// 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);
} }
/** Serializes {@code action} type and data into the {@code output}. */ /**
public static void serializeToStream(DownloadAction action, OutputStream output) * Creates a DASH download action.
throws IOException { *
// Don't close the stream as it closes the underlying stream too. * @param type The type of the action.
DataOutputStream dataOutputStream = new DataOutputStream(output); * @param uri The URI of the media to be downloaded.
dataOutputStream.writeUTF(action.type); * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
dataOutputStream.writeInt(action.version); * @param customCacheKey A custom key for cache indexing, or null.
action.writeToStream(dataOutputStream); * @param data Optional custom data for this action. If {@code null} an empty array will be used.
dataOutputStream.flush(); */
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. */ /** The type of the action. */
public final String type; public final String type;
/** The action version. */
public final int version;
/** The uri being downloaded or removed. */ /** The uri being downloaded or removed. */
public final Uri uri; public final Uri uri;
/** Whether this is a remove action. If false, this is a download action. */ /** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction; 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. */ /** Custom data for this action. May be empty. */
public final byte[] data; public final byte[] data;
/** /**
* @param type The type of the action. * @param type The type of the action.
* @param version The action version.
* @param uri The uri being downloaded or removed. * @param uri The uri being downloaded or removed.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action. * @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. * @param data Optional custom data for this action.
*/ */
protected DownloadAction( private DownloadAction(
String type, int version, Uri uri, boolean isRemoveAction, @Nullable byte[] data) { String type,
Uri uri,
boolean isRemoveAction,
List<StreamKey> keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
this.type = type; this.type = type;
this.version = version;
this.uri = uri; this.uri = uri;
this.isRemoveAction = isRemoveAction; this.isRemoveAction = isRemoveAction;
this.customCacheKey = customCacheKey;
this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY; 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. */ /** Serializes itself into a byte array. */
public final byte[] toByteArray() { public byte[] toByteArray() {
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
try { try {
serializeToStream(this, output); serializeToStream(output);
} catch (IOException e) { } catch (IOException e) {
// ByteArrayOutputStream shouldn't throw IOException. // ByteArrayOutputStream shouldn't throw IOException.
throw new IllegalStateException(); throw new IllegalStateException();
...@@ -175,46 +152,118 @@ public abstract class DownloadAction { ...@@ -175,46 +152,118 @@ public abstract class DownloadAction {
/** Returns whether this is an action for the same media as the {@code other}. */ /** Returns whether this is an action for the same media as the {@code other}. */
public boolean isSameMedia(DownloadAction 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. */ /** Returns keys of tracks to be downloaded. */
public List<StreamKey> getKeys() { 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 @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
if (o == null || getClass() != o.getClass()) { if (!(o instanceof DownloadAction)) {
return false; return false;
} }
DownloadAction that = (DownloadAction) o; DownloadAction that = (DownloadAction) o;
return type.equals(that.type) return type.equals(that.type)
&& version == that.version
&& uri.equals(that.uri) && uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction && isRemoveAction == that.isRemoveAction
&& keys.equals(that.keys)
&& Util.areEqual(customCacheKey, that.customCacheKey)
&& Arrays.equals(data, that.data); && Arrays.equals(data, that.data);
} }
@Override @Override
public int hashCode() { public final int hashCode() {
int result = uri.hashCode(); int result = type.hashCode();
result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0); 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); result = 31 * result + Arrays.hashCode(data);
return result; return result;
} }
private static Deserializer getDeserializer(Class<?> clazz) // Serialization.
throws NoSuchFieldException, IllegalAccessException {
Object value = clazz.getDeclaredField("DESERIALIZER").get(null); /**
return (Deserializer) Assertions.checkNotNull(value); * 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);
} }
} }
...@@ -28,9 +28,6 @@ import android.os.Looper; ...@@ -28,9 +28,6 @@ import android.os.Looper;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; 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.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -85,11 +82,10 @@ public final class DownloadManager { ...@@ -85,11 +82,10 @@ public final class DownloadManager {
private static final String TAG = "DownloadManager"; private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private final DownloaderConstructorHelper downloaderConstructorHelper;
private final int maxActiveDownloadTasks; private final int maxActiveDownloadTasks;
private final int minRetryCount; private final int minRetryCount;
private final ActionFile actionFile; private final ActionFile actionFile;
private final DownloadAction.Deserializer[] deserializers; private final DownloaderFactory downloaderFactory;
private final ArrayList<Task> tasks; private final ArrayList<Task> tasks;
private final ArrayList<Task> activeDownloadTasks; private final ArrayList<Task> activeDownloadTasks;
private final Handler handler; private final Handler handler;
...@@ -103,70 +99,33 @@ public final class DownloadManager { ...@@ -103,70 +99,33 @@ public final class DownloadManager {
private boolean downloadsStopped; 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}. * 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 actionFile The file in which active actions are saved.
* @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link * @param downloaderFactory A factory for creating {@link Downloader}s.
* DownloadAction#getDefaultDeserializers()} is used instead.
*/ */
public DownloadManager( public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) {
DownloaderConstructorHelper constructorHelper,
File actionFile,
Deserializer... deserializers) {
this( this(
constructorHelper, actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT);
DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
DEFAULT_MIN_RETRY_COUNT,
actionFile,
deserializers);
} }
/** /**
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
* *
* @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s * @param actionFile The file in which active actions are saved.
* for downloading data. * @param downloaderFactory A factory for creating {@link Downloader}s.
* @param maxSimultaneousDownloads The maximum number of simultaneous download tasks. * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks.
* @param minRetryCount The minimum number of times a task must be retried before failing. * @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( public DownloadManager(
DownloaderConstructorHelper constructorHelper,
int maxSimultaneousDownloads,
int minRetryCount,
File actionFile, File actionFile,
Deserializer... deserializers) { DownloaderFactory downloaderFactory,
this.downloaderConstructorHelper = constructorHelper; int maxSimultaneousDownloads,
int minRetryCount) {
this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory;
this.maxActiveDownloadTasks = maxSimultaneousDownloads; this.maxActiveDownloadTasks = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.actionFile = new ActionFile(actionFile);
this.deserializers =
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers();
this.downloadsStopped = true; this.downloadsStopped = true;
tasks = new ArrayList<>(); tasks = new ArrayList<>();
...@@ -239,7 +198,7 @@ public final class DownloadManager { ...@@ -239,7 +198,7 @@ public final class DownloadManager {
public int handleAction(byte[] actionData) throws IOException { public int handleAction(byte[] actionData) throws IOException {
Assertions.checkState(!released); Assertions.checkState(!released);
ByteArrayInputStream input = new ByteArrayInputStream(actionData); ByteArrayInputStream input = new ByteArrayInputStream(actionData);
DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input); DownloadAction action = DownloadAction.deserializeFromStream(input);
return handleAction(action); return handleAction(action);
} }
...@@ -344,7 +303,7 @@ public final class DownloadManager { ...@@ -344,7 +303,7 @@ public final class DownloadManager {
} }
private Task addTaskForAction(DownloadAction action) { 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); tasks.add(task);
logd("Task is added", task); logd("Task is added", task);
return task; return task;
...@@ -450,7 +409,7 @@ public final class DownloadManager { ...@@ -450,7 +409,7 @@ public final class DownloadManager {
() -> { () -> {
DownloadAction[] loadedActions; DownloadAction[] loadedActions;
try { try {
loadedActions = actionFile.load(DownloadManager.this.deserializers); loadedActions = actionFile.load();
logd("Action file is loaded."); logd("Action file is loaded.");
} catch (Throwable e) { } catch (Throwable e) {
Log.e(TAG, "Action file loading failed.", e); Log.e(TAG, "Action file loading failed.", e);
...@@ -642,6 +601,7 @@ public final class DownloadManager { ...@@ -642,6 +601,7 @@ public final class DownloadManager {
private final int id; private final int id;
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
private final DownloadAction action; private final DownloadAction action;
private final int minRetryCount; private final int minRetryCount;
private volatile @InternalState int currentState; private volatile @InternalState int currentState;
...@@ -650,9 +610,14 @@ public final class DownloadManager { ...@@ -650,9 +610,14 @@ public final class DownloadManager {
private Throwable error; private Throwable error;
private Task( private Task(
int id, DownloadManager downloadManager, DownloadAction action, int minRetryCount) { int id,
DownloadManager downloadManager,
DownloaderFactory downloaderFactory,
DownloadAction action,
int minRetryCount) {
this.id = id; this.id = id;
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory;
this.action = action; this.action = action;
this.currentState = STATE_QUEUED; this.currentState = STATE_QUEUED;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
...@@ -807,7 +772,7 @@ public final class DownloadManager { ...@@ -807,7 +772,7 @@ public final class DownloadManager {
logd("Task is started", this); logd("Task is started", this);
Throwable error = null; Throwable error = null;
try { try {
downloader = action.createDownloader(downloadManager.downloaderConstructorHelper); downloader = downloaderFactory.createDownloader(action);
if (action.isRemoveAction) { if (action.isRemoveAction) {
downloader.remove(); downloader.remove();
} else { } else {
......
/*
* 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);
}
/*
* 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);
}
}
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import java.util.Collections;
import java.util.List; import java.util.List;
/** A {@link DownloadHelper} for progressive streams. */ /** A {@link DownloadHelper} for progressive streams. */
...@@ -51,13 +52,18 @@ public final class ProgressiveDownloadHelper extends DownloadHelper { ...@@ -51,13 +52,18 @@ public final class ProgressiveDownloadHelper extends DownloadHelper {
} }
@Override @Override
public ProgressiveDownloadAction getDownloadAction( public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
@Nullable byte[] data, List<TrackKey> trackKeys) { return DownloadAction.createDownloadAction(
return ProgressiveDownloadAction.createDownloadAction(uri, data, customCacheKey); DownloadAction.TYPE_PROGRESSIVE,
uri,
/* keys= */ Collections.emptyList(),
customCacheKey,
data);
} }
@Override @Override
public ProgressiveDownloadAction getRemoveAction(@Nullable byte[] data) { public DownloadAction getRemoveAction(@Nullable byte[] data) {
return ProgressiveDownloadAction.createRemoveAction(uri, data, customCacheKey); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey, data);
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
...@@ -47,7 +48,7 @@ public final class ProgressiveDownloader implements Downloader { ...@@ -47,7 +48,7 @@ public final class ProgressiveDownloader implements Downloader {
* @param constructorHelper A {@link DownloaderConstructorHelper} instance. * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
*/ */
public ProgressiveDownloader( 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.dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, customCacheKey, 0);
this.cache = constructorHelper.getCache(); this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.createCacheDataSource(); this.dataSource = constructorHelper.createCacheDataSource();
......
/*
* 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);
}
}
...@@ -18,13 +18,13 @@ package com.google.android.exoplayer2.offline; ...@@ -18,13 +18,13 @@ package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; 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 com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
...@@ -38,10 +38,22 @@ import org.robolectric.RuntimeEnvironment; ...@@ -38,10 +38,22 @@ import org.robolectric.RuntimeEnvironment;
public class ActionFileTest { public class ActionFileTest {
private File tempFile; private File tempFile;
private DownloadAction action1;
private DownloadAction action2;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); 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 @After
...@@ -78,44 +90,32 @@ public class ActionFileTest { ...@@ -78,44 +90,32 @@ public class ActionFileTest {
@Test @Test
public void testLoadAction() throws Exception { public void testLoadAction() throws Exception {
byte[] data = Util.getUtf8Bytes("321");
DownloadAction[] actions = DownloadAction[] actions =
loadActions( loadActions(
new Object[] { new Object[] {
ActionFile.VERSION, ActionFile.VERSION,
1, // Action count 1, // Action count
"type2", // Action 1 action1
FakeDownloadAction.VERSION, });
data,
},
new FakeDeserializer("type2"));
assertThat(actions).isNotNull(); assertThat(actions).isNotNull();
assertThat(actions).hasLength(1); assertThat(actions).hasLength(1);
assertAction(actions[0], "type2", FakeDownloadAction.VERSION, data); assertThat(actions[0]).isEqualTo(action1);
} }
@Test @Test
public void testLoadActions() throws Exception { public void testLoadActions() throws Exception {
byte[] data1 = Util.getUtf8Bytes("123");
byte[] data2 = Util.getUtf8Bytes("321");
DownloadAction[] actions = DownloadAction[] actions =
loadActions( loadActions(
new Object[] { new Object[] {
ActionFile.VERSION, ActionFile.VERSION,
2, // Action count 2, // Action count
"type1", // Action 1 action1,
FakeDownloadAction.VERSION, action2,
data1, });
"type2", // Action 2
FakeDownloadAction.VERSION,
data2,
},
new FakeDeserializer("type1"),
new FakeDeserializer("type2"));
assertThat(actions).isNotNull(); assertThat(actions).isNotNull();
assertThat(actions).hasLength(2); assertThat(actions).hasLength(2);
assertAction(actions[0], "type1", FakeDownloadAction.VERSION, data1); assertThat(actions[0]).isEqualTo(action1);
assertAction(actions[1], "type2", FakeDownloadAction.VERSION, data2); assertThat(actions[1]).isEqualTo(action2);
} }
@Test @Test
...@@ -125,11 +125,8 @@ public class ActionFileTest { ...@@ -125,11 +125,8 @@ public class ActionFileTest {
new Object[] { new Object[] {
ActionFile.VERSION + 1, ActionFile.VERSION + 1,
1, // Action count 1, // Action count
"type2", // Action 1 action1,
FakeDownloadAction.VERSION, });
Util.getUtf8Bytes("321"),
},
new FakeDeserializer("type2"));
Assert.fail(); Assert.fail();
} catch (IOException e) { } catch (IOException e) {
// Expected exception. // Expected exception.
...@@ -137,78 +134,30 @@ public class ActionFileTest { ...@@ -137,78 +134,30 @@ public class ActionFileTest {
} }
@Test @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 { public void testStoreAndLoadNoActions() throws Exception {
doTestSerializationRoundTrip(new DownloadAction[0]); doTestSerializationRoundTrip();
} }
@Test @Test
public void testStoreAndLoadActions() throws Exception { public void testStoreAndLoadActions() throws Exception {
doTestSerializationRoundTrip( doTestSerializationRoundTrip(action1, action2);
new DownloadAction[] {
new FakeDownloadAction("type1", Util.getUtf8Bytes("123")),
new FakeDownloadAction("type2", Util.getUtf8Bytes("321")),
},
new FakeDeserializer("type1"),
new FakeDeserializer("type2"));
} }
private void doTestSerializationRoundTrip(DownloadAction[] actions, private void doTestSerializationRoundTrip(DownloadAction... actions) throws IOException {
Deserializer... deserializers) throws IOException {
ActionFile actionFile = new ActionFile(tempFile); ActionFile actionFile = new ActionFile(tempFile);
actionFile.store(actions); actionFile.store(actions);
assertThat(actionFile.load(deserializers)).isEqualTo(actions); assertThat(actionFile.load()).isEqualTo(actions);
} }
private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) private DownloadAction[] loadActions(Object[] values) throws IOException {
throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(tempFile); FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
try { try {
for (Object value : values) { for (Object value : values) {
if (value instanceof Integer) { if (value instanceof Integer) {
dataOutputStream.writeInt((Integer) value); dataOutputStream.writeInt((Integer) value);
} else if (value instanceof String) { } else if (value instanceof DownloadAction) {
dataOutputStream.writeUTF((String) value); ((DownloadAction) value).serializeToStream(dataOutputStream);
} else if (value instanceof byte[]) {
byte[] data = (byte[]) value;
dataOutputStream.writeInt(data.length);
dataOutputStream.write(data);
} else { } else {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
...@@ -216,50 +165,11 @@ public class ActionFileTest { ...@@ -216,50 +165,11 @@ public class ActionFileTest {
} finally { } finally {
dataOutputStream.close(); 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) { private static DownloadAction buildAction(String type, Uri uri, byte[] data) {
assertThat(action).isInstanceOf(FakeDownloadAction.class); return DownloadAction.createDownloadAction(
assertThat(action.type).isEqualTo(type); type, uri, /* keys= */ Collections.emptyList(), /* customCacheKey= */ null, data);
assertThat(((FakeDownloadAction) action).version).isEqualTo(version);
assertThat(((FakeDownloadAction) action).data).isEqualTo(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;
}
}
} }
/*
* 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);
}
}
...@@ -25,19 +25,17 @@ import com.google.android.exoplayer2.offline.DownloadManager.TaskState.State; ...@@ -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.DummyMainThread;
import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; 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 com.google.android.exoplayer2.util.Util;
import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
...@@ -63,6 +61,7 @@ public class DownloadManagerTest { ...@@ -63,6 +61,7 @@ public class DownloadManagerTest {
private DummyMainThread dummyMainThread; private DummyMainThread dummyMainThread;
private File actionFile; private File actionFile;
private TestDownloadManagerListener downloadManagerListener; private TestDownloadManagerListener downloadManagerListener;
private FakeDownloaderFactory downloaderFactory;
private DownloadManager downloadManager; private DownloadManager downloadManager;
@Before @Before
...@@ -73,6 +72,7 @@ public class DownloadManagerTest { ...@@ -73,6 +72,7 @@ public class DownloadManagerTest {
uri3 = Uri.parse("http://abc.com/media3"); uri3 = Uri.parse("http://abc.com/media3");
dummyMainThread = new DummyMainThread(); dummyMainThread = new DummyMainThread();
actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
downloaderFactory = new FakeDownloaderFactory();
setUpDownloadManager(100); setUpDownloadManager(100);
} }
...@@ -85,24 +85,24 @@ public class DownloadManagerTest { ...@@ -85,24 +85,24 @@ public class DownloadManagerTest {
@Test @Test
public void testDownloadActionRuns() throws Throwable { public void testDownloadActionRuns() throws Throwable {
doTestActionRuns(createDownloadAction(uri1)); doTestDownloaderRuns(createDownloadRunner(uri1));
} }
@Test @Test
public void testRemoveActionRuns() throws Throwable { public void testRemoveActionRuns() throws Throwable {
doTestActionRuns(createRemoveAction(uri1)); doTestDownloaderRuns(createRemoveRunner(uri1));
} }
@Test @Test
public void testDownloadRetriesThenFails() throws Throwable { public void testDownloadRetriesThenFails() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1); DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadAction.post(); downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true; fakeDownloader.enableDownloadIOException = true;
for (int i = 0; i <= MIN_RETRY_COUNT; i++) { for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock(); fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock();
} }
downloadAction.assertFailed(); downloadRunner.assertFailed();
downloadManagerListener.clearDownloadError(); downloadManagerListener.clearDownloadError();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
...@@ -110,23 +110,23 @@ public class DownloadManagerTest { ...@@ -110,23 +110,23 @@ public class DownloadManagerTest {
@Test @Test
public void testDownloadNoRetryWhenCanceled() throws Throwable { public void testDownloadNoRetryWhenCanceled() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
downloadAction.getFakeDownloader().enableDownloadIOException = true; downloadRunner.downloader.enableDownloadIOException = true;
downloadAction.post().assertStarted(); downloadRunner.postAction().assertStarted();
FakeDownloadAction removeAction = createRemoveAction(uri1).post(); DownloadRunner removeRunner = createRemoveRunner(uri1).postAction();
downloadAction.unblock().assertCanceled(); downloadRunner.unblock().assertCanceled();
removeAction.unblock(); removeRunner.unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testDownloadRetriesThenContinues() throws Throwable { public void testDownloadRetriesThenContinues() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1); DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadAction.post(); downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true; fakeDownloader.enableDownloadIOException = true;
for (int i = 0; i <= MIN_RETRY_COUNT; i++) { for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY); fakeDownloader.assertStarted(MAX_RETRY_DELAY);
...@@ -135,7 +135,7 @@ public class DownloadManagerTest { ...@@ -135,7 +135,7 @@ public class DownloadManagerTest {
} }
fakeDownloader.unblock(); fakeDownloader.unblock();
} }
downloadAction.assertCompleted(); downloadRunner.assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
...@@ -143,9 +143,9 @@ public class DownloadManagerTest { ...@@ -143,9 +143,9 @@ public class DownloadManagerTest {
@Test @Test
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public void testDownloadRetryCountResetsOnProgress() throws Throwable { public void testDownloadRetryCountResetsOnProgress() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1); DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadAction.post(); downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true; fakeDownloader.enableDownloadIOException = true;
fakeDownloader.downloadedBytes = 0; fakeDownloader.downloadedBytes = 0;
for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) { for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) {
...@@ -156,61 +156,61 @@ public class DownloadManagerTest { ...@@ -156,61 +156,61 @@ public class DownloadManagerTest {
} }
fakeDownloader.unblock(); fakeDownloader.unblock();
} }
downloadAction.assertCompleted(); downloadRunner.assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri2)); doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri2));
} }
@Test @Test
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction(uri1), createRemoveAction(uri2)); doTestDownloadersRunInParallel(createDownloadRunner(uri1), createRemoveRunner(uri2));
} }
@Test @Test
public void testSameMediaDownloadActionsStartInParallel() throws Throwable { public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri1)); doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri1));
} }
@Test @Test
public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
doTestActionsRunSequentially(createDownloadAction(uri1), createRemoveAction(uri1)); doTestDownloadersRunSequentially(createDownloadRunner(uri1), createRemoveRunner(uri1));
} }
@Test @Test
public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction(uri1), createDownloadAction(uri1)); doTestDownloadersRunSequentially(createRemoveRunner(uri1), createDownloadRunner(uri1));
} }
@Test @Test
public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction(uri1), createRemoveAction(uri1)); doTestDownloadersRunSequentially(createRemoveRunner(uri1), createRemoveRunner(uri1));
} }
@Test @Test
public void testSameMediaMultipleActions() throws Throwable { public void testSameMediaMultipleActions() throws Throwable {
FakeDownloadAction downloadAction1 = createDownloadAction(uri1).ignoreInterrupts(); DownloadRunner downloadAction1 = createDownloadRunner(uri1).ignoreInterrupts();
FakeDownloadAction downloadAction2 = createDownloadAction(uri1).ignoreInterrupts(); DownloadRunner downloadAction2 = createDownloadRunner(uri1).ignoreInterrupts();
FakeDownloadAction removeAction1 = createRemoveAction(uri1); DownloadRunner removeAction1 = createRemoveRunner(uri1);
FakeDownloadAction downloadAction3 = createDownloadAction(uri1); DownloadRunner downloadAction3 = createDownloadRunner(uri1);
FakeDownloadAction removeAction2 = createRemoveAction(uri1); DownloadRunner removeAction2 = createRemoveRunner(uri1);
// Two download actions run in parallel. // Two download actions run in parallel.
downloadAction1.post().assertStarted(); downloadAction1.postAction().assertStarted();
downloadAction2.post().assertStarted(); downloadAction2.postAction().assertStarted();
// removeAction1 is added. It interrupts the two download actions' threads but they are // removeAction1 is added. It interrupts the two download actions' threads but they are
// configured to ignore it so removeAction1 doesn't start. // 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 finishes but it isn't enough to start removeAction1.
downloadAction2.unblock().assertCanceled(); downloadAction2.unblock().assertCanceled();
removeAction1.assertDoesNotStart(); removeAction1.assertDoesNotStart();
// downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. // downloadAction3 is postAction to DownloadManager but it waits for removeAction1 to finish.
downloadAction3.post().assertDoesNotStart(); downloadAction3.postAction().assertDoesNotStart();
// When downloadAction1 finishes, removeAction1 starts. // When downloadAction1 finishes, removeAction1 starts.
downloadAction1.unblock().assertCanceled(); downloadAction1.unblock().assertCanceled();
...@@ -220,7 +220,7 @@ public class DownloadManagerTest { ...@@ -220,7 +220,7 @@ public class DownloadManagerTest {
// removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2
// starts immediately. // starts immediately.
removeAction2.post(); removeAction2.postAction();
removeAction1.assertCanceled(); removeAction1.assertCanceled();
downloadAction3.assertCanceled(); downloadAction3.assertCanceled();
removeAction2.assertStarted().unblock().assertCompleted(); removeAction2.assertStarted().unblock().assertCompleted();
...@@ -229,13 +229,13 @@ public class DownloadManagerTest { ...@@ -229,13 +229,13 @@ public class DownloadManagerTest {
@Test @Test
public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable {
FakeDownloadAction removeAction1 = createRemoveAction(uri1).ignoreInterrupts(); DownloadRunner removeAction1 = createRemoveRunner(uri1).ignoreInterrupts();
FakeDownloadAction removeAction2 = createRemoveAction(uri1); DownloadRunner removeAction2 = createRemoveRunner(uri1);
FakeDownloadAction removeAction3 = createRemoveAction(uri1); DownloadRunner removeAction3 = createRemoveRunner(uri1);
removeAction1.post().assertStarted(); removeAction1.postAction().assertStarted();
removeAction2.post().assertDoesNotStart(); removeAction2.postAction().assertDoesNotStart();
removeAction3.post().assertDoesNotStart(); removeAction3.postAction().assertDoesNotStart();
removeAction2.assertCanceled(); removeAction2.assertCanceled();
...@@ -247,30 +247,30 @@ public class DownloadManagerTest { ...@@ -247,30 +247,30 @@ public class DownloadManagerTest {
@Test @Test
public void testGetTasks() throws Throwable { public void testGetTasks() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction(uri1); DownloadRunner removeAction = createRemoveRunner(uri1);
FakeDownloadAction downloadAction1 = createDownloadAction(uri1); DownloadRunner downloadAction1 = createDownloadRunner(uri1);
FakeDownloadAction downloadAction2 = createDownloadAction(uri1); DownloadRunner downloadAction2 = createDownloadRunner(uri1);
removeAction.post().assertStarted(); removeAction.postAction().assertStarted();
downloadAction1.post().assertDoesNotStart(); downloadAction1.postAction().assertDoesNotStart();
downloadAction2.post().assertDoesNotStart(); downloadAction2.postAction().assertDoesNotStart();
TaskState[] states = downloadManager.getAllTaskStates(); TaskState[] states = downloadManager.getAllTaskStates();
assertThat(states).hasLength(3); assertThat(states).hasLength(3);
assertThat(states[0].action).isEqualTo(removeAction); assertThat(states[0].action).isEqualTo(removeAction.action);
assertThat(states[1].action).isEqualTo(downloadAction1); assertThat(states[1].action).isEqualTo(downloadAction1.action);
assertThat(states[2].action).isEqualTo(downloadAction2); assertThat(states[2].action).isEqualTo(downloadAction2.action);
} }
@Test @Test
public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction(uri1); DownloadRunner removeAction = createRemoveRunner(uri1);
FakeDownloadAction downloadAction1 = createDownloadAction(uri1); DownloadRunner downloadAction1 = createDownloadRunner(uri1);
FakeDownloadAction downloadAction2 = createDownloadAction(uri1); DownloadRunner downloadAction2 = createDownloadRunner(uri1);
removeAction.post().assertStarted(); removeAction.postAction().assertStarted();
downloadAction1.post().assertDoesNotStart(); downloadAction1.postAction().assertDoesNotStart();
downloadAction2.post().assertDoesNotStart(); downloadAction2.postAction().assertDoesNotStart();
removeAction.unblock().assertCompleted(); removeAction.unblock().assertCompleted();
downloadAction1.assertStarted(); downloadAction1.assertStarted();
...@@ -283,75 +283,75 @@ public class DownloadManagerTest { ...@@ -283,75 +283,75 @@ public class DownloadManagerTest {
@Test @Test
public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable {
FakeDownloadAction removeAction = createRemoveAction(uri1).ignoreInterrupts(); DownloadRunner removeRunner = createRemoveRunner(uri1).ignoreInterrupts();
FakeDownloadAction downloadAction1 = createDownloadAction(uri1); DownloadRunner downloadRunner1 = createDownloadRunner(uri1);
FakeDownloadAction downloadAction2 = createDownloadAction(uri2); DownloadRunner downloadRunner2 = createDownloadRunner(uri2);
removeAction.post().assertStarted(); removeRunner.postAction().assertStarted();
downloadAction1.post().assertDoesNotStart(); downloadRunner1.postAction().assertDoesNotStart();
downloadAction2.post().assertDoesNotStart(); downloadRunner2.postAction().assertDoesNotStart();
removeAction.unblock().assertCompleted(); removeRunner.unblock().assertCompleted();
downloadAction1.assertStarted(); downloadRunner1.assertStarted();
downloadAction2.assertStarted(); downloadRunner2.assertStarted();
downloadAction1.unblock().assertCompleted(); downloadRunner1.unblock().assertCompleted();
downloadAction2.unblock().assertCompleted(); downloadRunner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable {
FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
FakeDownloadAction removeAction1 = createRemoveAction(uri1); DownloadRunner removeRunner1 = createRemoveRunner(uri1);
FakeDownloadAction removeAction2 = createRemoveAction(uri2); DownloadRunner removeRunner2 = createRemoveRunner(uri2);
downloadAction.post().assertStarted(); downloadRunner.postAction().assertStarted();
removeAction1.post().assertDoesNotStart(); removeRunner1.postAction().assertDoesNotStart();
removeAction2.post().assertStarted(); removeRunner2.postAction().assertStarted();
downloadAction.unblock().assertCanceled(); downloadRunner.unblock().assertCanceled();
removeAction2.unblock().assertCompleted(); removeRunner2.unblock().assertCompleted();
removeAction1.assertStarted(); removeRunner1.assertStarted();
removeAction1.unblock().assertCompleted(); removeRunner1.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
@Test @Test
public void testStopAndResume() throws Throwable { public void testStopAndResume() throws Throwable {
FakeDownloadAction download1Action = createDownloadAction(uri1); DownloadRunner download1Runner = createDownloadRunner(uri1);
FakeDownloadAction remove2Action = createRemoveAction(uri2); DownloadRunner remove2Runner = createRemoveRunner(uri2);
FakeDownloadAction download2Action = createDownloadAction(uri2); DownloadRunner download2Runner = createDownloadRunner(uri2);
FakeDownloadAction remove1Action = createRemoveAction(uri1); DownloadRunner remove1Runner = createRemoveRunner(uri1);
FakeDownloadAction download3Action = createDownloadAction(uri3); DownloadRunner download3Runner = createDownloadRunner(uri3);
download1Action.post().assertStarted(); download1Runner.postAction().assertStarted();
remove2Action.post().assertStarted(); remove2Runner.postAction().assertStarted();
download2Action.post().assertDoesNotStart(); download2Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.stopDownloads()); runOnMainThread(() -> downloadManager.stopDownloads());
download1Action.assertStopped(); download1Runner.assertStopped();
// remove actions aren't stopped. // remove actions aren't stopped.
remove2Action.unblock().assertCompleted(); remove2Runner.unblock().assertCompleted();
// Although remove2Action is finished, download2Action doesn't start. // Although remove2 is finished, download2 doesn't start.
download2Action.assertDoesNotStart(); download2Runner.assertDoesNotStart();
// When a new remove action is added, it cancels stopped download actions with the same media. // When a new remove action is added, it cancels stopped download actions with the same media.
remove1Action.post(); remove1Runner.postAction();
download1Action.assertCanceled(); download1Runner.assertCanceled();
remove1Action.assertStarted().unblock().assertCompleted(); remove1Runner.assertStarted().unblock().assertCompleted();
// New download actions can be added but they don't start. // New download actions can be added but they don't start.
download3Action.post().assertDoesNotStart(); download3Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.startDownloads()); runOnMainThread(() -> downloadManager.startDownloads());
download2Action.assertStarted().unblock().assertCompleted(); download2Runner.assertStarted().unblock().assertCompleted();
download3Action.assertStarted().unblock().assertCompleted(); download3Runner.assertStarted().unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
...@@ -359,34 +359,34 @@ public class DownloadManagerTest { ...@@ -359,34 +359,34 @@ public class DownloadManagerTest {
@Test @Test
public void testResumeBeforeTotallyStopped() throws Throwable { public void testResumeBeforeTotallyStopped() throws Throwable {
setUpDownloadManager(2); setUpDownloadManager(2);
FakeDownloadAction download1Action = createDownloadAction(uri1).ignoreInterrupts(); DownloadRunner download1Runner = createDownloadRunner(uri1).ignoreInterrupts();
FakeDownloadAction download2Action = createDownloadAction(uri2); DownloadRunner download2Runner = createDownloadRunner(uri2);
FakeDownloadAction download3Action = createDownloadAction(uri3); DownloadRunner download3Runner = createDownloadRunner(uri3);
download1Action.post().assertStarted(); download1Runner.postAction().assertStarted();
download2Action.post().assertStarted(); download2Runner.postAction().assertStarted();
// download3Action doesn't start as DM was configured to run two downloads in parallel. // download3 doesn't start as DM was configured to run two downloads in parallel.
download3Action.post().assertDoesNotStart(); download3Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.stopDownloads()); runOnMainThread(() -> downloadManager.stopDownloads());
// download1Action doesn't stop yet as it ignores interrupts. // download1 doesn't stop yet as it ignores interrupts.
download2Action.assertStopped(); download2Runner.assertStopped();
runOnMainThread(() -> downloadManager.startDownloads()); runOnMainThread(() -> downloadManager.startDownloads());
// download2Action starts immediately. // download2 starts immediately.
download2Action.assertStarted(); download2Runner.assertStarted();
// download3Action doesn't start as download1Action still holds its slot. // download3 doesn't start as download1 still holds its slot.
download3Action.assertDoesNotStart(); download3Runner.assertDoesNotStart();
// when unblocked download1Action stops and starts immediately. // when unblocked download1 stops and starts immediately.
download1Action.unblock().assertStopped().assertStarted(); download1Runner.unblock().assertStopped().assertStarted();
download1Action.unblock(); download1Runner.unblock();
download2Action.unblock(); download2Runner.unblock();
download3Action.unblock(); download3Runner.unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
...@@ -400,12 +400,7 @@ public class DownloadManagerTest { ...@@ -400,12 +400,7 @@ public class DownloadManagerTest {
() -> { () -> {
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
new DownloaderConstructorHelper( actionFile, downloaderFactory, maxActiveDownloadTasks, MIN_RETRY_COUNT);
Mockito.mock(Cache.class), DummyDataSource.FACTORY),
maxActiveDownloadTasks,
MIN_RETRY_COUNT,
actionFile,
ProgressiveDownloadAction.DESERIALIZER);
downloadManagerListener = downloadManagerListener =
new TestDownloadManagerListener(downloadManager, dummyMainThread); new TestDownloadManagerListener(downloadManager, dummyMainThread);
downloadManager.addListener(downloadManagerListener); downloadManager.addListener(downloadManagerListener);
...@@ -424,104 +419,104 @@ public class DownloadManagerTest { ...@@ -424,104 +419,104 @@ public class DownloadManagerTest {
} }
} }
private void doTestActionRuns(FakeDownloadAction action) throws Throwable { private void doTestDownloaderRuns(DownloadRunner runner) throws Throwable {
action.post().assertStarted().unblock().assertCompleted(); runner.postAction().assertStarted().unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2) private void doTestDownloadersRunSequentially(DownloadRunner runner1, DownloadRunner runner2)
throws Throwable { throws Throwable {
action1.ignoreInterrupts().post().assertStarted(); runner1.ignoreInterrupts().postAction().assertStarted();
action2.post().assertDoesNotStart(); runner2.postAction().assertDoesNotStart();
action1.unblock(); runner1.unblock();
action2.assertStarted(); runner2.assertStarted();
action2.unblock().assertCompleted(); runner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2) private void doTestDownloadersRunInParallel(DownloadRunner runner1, DownloadRunner runner2)
throws Throwable { throws Throwable {
action1.post().assertStarted(); runner1.postAction().assertStarted();
action2.post().assertStarted(); runner2.postAction().assertStarted();
action1.unblock().assertCompleted(); runner1.unblock().assertCompleted();
action2.unblock().assertCompleted(); runner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
} }
private FakeDownloadAction createDownloadAction(Uri uri) { private DownloadRunner createDownloadRunner(Uri uri) {
return new FakeDownloadAction(uri, /* isRemoveAction= */ false); return new DownloadRunner(uri, /* isRemoveAction= */ false);
} }
private FakeDownloadAction createRemoveAction(Uri uri) { private DownloadRunner createRemoveRunner(Uri uri) {
return new FakeDownloadAction(uri, /* isRemoveAction= */ true); return new DownloadRunner(uri, /* isRemoveAction= */ true);
} }
private void runOnMainThread(final Runnable r) { private void runOnMainThread(final Runnable r) {
dummyMainThread.runOnMainThread(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); private DownloadRunner(Uri uri, boolean isRemoveAction) {
this.downloader = new FakeDownloader(isRemoveAction); action =
} isRemoveAction
? DownloadAction.createRemoveAction(
@Override DownloadAction.TYPE_PROGRESSIVE,
protected void writeToStream(DataOutputStream output) { uri,
// do nothing. /* customCacheKey= */ null,
} /* data= */ null)
: DownloadAction.createDownloadAction(
@Override DownloadAction.TYPE_PROGRESSIVE,
public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { uri,
return downloader; /* keys= */ Collections.emptyList(),
} /* customCacheKey= */ null,
/* data= */ null);
private FakeDownloader getFakeDownloader() { downloader = new FakeDownloader(isRemoveAction);
return downloader; downloaderFactory.putFakeDownloader(action, downloader);
} }
private FakeDownloadAction post() { private DownloadRunner postAction() {
runOnMainThread(() -> downloadManager.handleAction(FakeDownloadAction.this)); runOnMainThread(() -> downloadManager.handleAction(action));
return this; return this;
} }
private FakeDownloadAction assertDoesNotStart() throws InterruptedException { private DownloadRunner assertDoesNotStart() throws InterruptedException {
Thread.sleep(ASSERT_FALSE_TIME); Thread.sleep(ASSERT_FALSE_TIME);
assertThat(downloader.started.getCount()).isEqualTo(1); assertThat(downloader.started.getCount()).isEqualTo(1);
return this; return this;
} }
private FakeDownloadAction assertStarted() throws InterruptedException { private DownloadRunner assertStarted() throws InterruptedException {
downloader.assertStarted(ASSERT_TRUE_TIMEOUT); downloader.assertStarted(ASSERT_TRUE_TIMEOUT);
return assertState(TaskState.STATE_STARTED); return assertState(TaskState.STATE_STARTED);
} }
private FakeDownloadAction assertCompleted() { private DownloadRunner assertCompleted() {
return assertState(TaskState.STATE_COMPLETED); return assertState(TaskState.STATE_COMPLETED);
} }
private FakeDownloadAction assertFailed() { private DownloadRunner assertFailed() {
return assertState(TaskState.STATE_FAILED); return assertState(TaskState.STATE_FAILED);
} }
private FakeDownloadAction assertCanceled() { private DownloadRunner assertCanceled() {
return assertState(TaskState.STATE_CANCELED); return assertState(TaskState.STATE_CANCELED);
} }
private FakeDownloadAction assertStopped() { private DownloadRunner assertStopped() {
return assertState(TaskState.STATE_QUEUED); return assertState(TaskState.STATE_QUEUED);
} }
private FakeDownloadAction assertState(@State int expectedState) { private DownloadRunner assertState(@State int expectedState) {
while (true) { while (true) {
Integer state = null; Integer state = null;
try { try {
state = downloadManagerListener.pollStateChange(this, ASSERT_TRUE_TIMEOUT); state = downloadManagerListener.pollStateChange(action, ASSERT_TRUE_TIMEOUT);
} catch (InterruptedException e) { } catch (InterruptedException e) {
fail(e.getMessage()); fail(e.getMessage());
} }
...@@ -531,36 +526,54 @@ public class DownloadManagerTest { ...@@ -531,36 +526,54 @@ public class DownloadManagerTest {
} }
} }
private FakeDownloadAction unblock() { private DownloadRunner unblock() {
downloader.unblock(); downloader.unblock();
return this; return this;
} }
private FakeDownloadAction ignoreInterrupts() { private DownloadRunner ignoreInterrupts() {
downloader.ignoreInterrupts = true; downloader.ignoreInterrupts = true;
return this; 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 static class FakeDownloader implements Downloader {
private final com.google.android.exoplayer2.util.ConditionVariable blocker; private final com.google.android.exoplayer2.util.ConditionVariable blocker;
private final boolean isRemoveAction; private final boolean isRemove;
private CountDownLatch started; private CountDownLatch started;
private boolean ignoreInterrupts; private boolean ignoreInterrupts;
private volatile boolean enableDownloadIOException; private volatile boolean enableDownloadIOException;
private volatile int downloadedBytes = C.LENGTH_UNSET; private volatile int downloadedBytes = C.LENGTH_UNSET;
private FakeDownloader(boolean isRemoveAction) { private FakeDownloader(boolean isRemove) {
this.isRemoveAction = isRemoveAction; this.isRemove = isRemove;
this.started = new CountDownLatch(1); this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
} }
@Override @Override
public void download() throws InterruptedException, IOException { public void download() throws InterruptedException, IOException {
assertThat(isRemoveAction).isFalse(); assertThat(isRemove).isFalse();
started.countDown(); started.countDown();
block(); block();
if (enableDownloadIOException) { if (enableDownloadIOException) {
...@@ -575,7 +588,7 @@ public class DownloadManagerTest { ...@@ -575,7 +588,7 @@ public class DownloadManagerTest {
@Override @Override
public void remove() throws InterruptedException { public void remove() throws InterruptedException {
assertThat(isRemoveAction).isTrue(); assertThat(isRemove).isTrue();
started.countDown(); started.countDown();
block(); block();
} }
......
...@@ -18,21 +18,19 @@ package com.google.android.exoplayer2.offline; ...@@ -18,21 +18,19 @@ package com.google.android.exoplayer2.offline;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link ProgressiveDownloadAction}. */ // TODO: Merge into DownloadActionTest
/** Unit tests for progressive {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class ProgressiveDownloadActionTest { public class ProgressiveDownloadActionTest {
...@@ -58,15 +56,6 @@ public class ProgressiveDownloadActionTest { ...@@ -58,15 +56,6 @@ public class ProgressiveDownloadActionTest {
} }
@Test @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 { public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception {
DownloadAction action1 = createRemoveAction(uri1, null); DownloadAction action1 = createRemoveAction(uri1, null);
DownloadAction action2 = createDownloadAction(uri1, null); DownloadAction action2 = createDownloadAction(uri1, null);
...@@ -139,6 +128,13 @@ public class ProgressiveDownloadActionTest { ...@@ -139,6 +128,13 @@ public class ProgressiveDownloadActionTest {
doTestSerializationRoundTrip(createRemoveAction(uri2, "key")); 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) { private void assertSameMedia(DownloadAction action1, DownloadAction action2) {
assertThat(action1.isSameMedia(action2)).isTrue(); assertThat(action1.isSameMedia(action2)).isTrue();
assertThat(action2.isSameMedia(action1)).isTrue(); assertThat(action2.isSameMedia(action1)).isTrue();
...@@ -149,25 +145,61 @@ public class ProgressiveDownloadActionTest { ...@@ -149,25 +145,61 @@ public class ProgressiveDownloadActionTest {
assertThat(action2.isSameMedia(action1)).isFalse(); 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 { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out); DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output); action.serializeToStream(output);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in); DataInputStream input = new DataInputStream(in);
DownloadAction action2 = DownloadAction action2 = DownloadAction.deserializeFromStream(input);
DownloadAction.deserializeFromStream(
new DownloadAction.Deserializer[] {ProgressiveDownloadAction.DESERIALIZER}, input);
assertThat(action2).isEqualTo(action); 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) { 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) { private static DownloadAction createRemoveAction(Uri uri1, String customCacheKey) {
return ProgressiveDownloadAction.createRemoveAction(uri1, null, customCacheKey); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri1, customCacheKey, /* data= */ null);
} }
} }
/*
* 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);
}
}
...@@ -19,6 +19,7 @@ import android.net.Uri; ...@@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.offline.TrackKey;
...@@ -86,13 +87,15 @@ public final class DashDownloadHelper extends DownloadHelper { ...@@ -86,13 +87,15 @@ public final class DashDownloadHelper extends DownloadHelper {
} }
@Override @Override
public DashDownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) { public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return DashDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys)); return DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data);
} }
@Override @Override
public DashDownloadAction getRemoveAction(@Nullable byte[] data) { public DownloadAction getRemoveAction(@Nullable byte[] data) {
return DashDownloadAction.createRemoveAction(uri, data); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data);
} }
private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
......
...@@ -19,10 +19,7 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,10 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction; 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.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
...@@ -33,11 +30,10 @@ import java.util.Collections; ...@@ -33,11 +30,10 @@ import java.util.Collections;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link DashDownloadAction}. */ // TODO: Merge into DownloadActionTest
/** Unit tests for DASH {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class DashDownloadActionTest { public class DashDownloadActionTest {
...@@ -63,15 +59,6 @@ public class DashDownloadActionTest { ...@@ -63,15 +59,6 @@ public class DashDownloadActionTest {
} }
@Test @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() { public void testSameUriDifferentAction_IsSameMedia() {
DownloadAction action1 = createRemoveAction(uri1); DownloadAction action1 = createRemoveAction(uri1);
DownloadAction action2 = createDownloadAction(uri1); DownloadAction action2 = createDownloadAction(uri1);
...@@ -141,6 +128,14 @@ public class DashDownloadActionTest { ...@@ -141,6 +128,14 @@ public class DashDownloadActionTest {
createDownloadAction(uri2, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1))); 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) { private static void assertNotEqual(DownloadAction action1, DownloadAction action2) {
assertThat(action1).isNotEqualTo(action2); assertThat(action1).isNotEqualTo(action2);
assertThat(action2).isNotEqualTo(action1); assertThat(action2).isNotEqualTo(action1);
...@@ -154,24 +149,53 @@ public class DashDownloadActionTest { ...@@ -154,24 +149,53 @@ public class DashDownloadActionTest {
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out); DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output); action.serializeToStream(output);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in); DataInputStream input = new DataInputStream(in);
DownloadAction action2 = DownloadAction action2 = DownloadAction.deserializeFromStream(input);
DownloadAction.deserializeFromStream(
new DownloadAction.Deserializer[] {DashDownloadAction.DESERIALIZER}, input);
assertThat(action).isEqualTo(action2); 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) { private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
ArrayList<StreamKey> keysList = new ArrayList<>(); ArrayList<StreamKey> keysList = new ArrayList<>();
Collections.addAll(keysList, keys); 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) { private static DownloadAction createRemoveAction(Uri uri) {
return DashDownloadAction.createRemoveAction(uri, null); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, /* data= */ null);
} }
} }
...@@ -25,14 +25,21 @@ import static org.junit.Assert.fail; ...@@ -25,14 +25,21 @@ import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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.DownloadException;
import com.google.android.exoplayer2.offline.Downloader;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; 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.offline.StreamKey;
import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec; 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.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -44,6 +51,7 @@ import org.junit.After; ...@@ -44,6 +51,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
...@@ -68,6 +76,23 @@ public class DashDownloaderTest { ...@@ -68,6 +76,23 @@ public class DashDownloaderTest {
} }
@Test @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 { public void testDownloadRepresentation() throws Exception {
FakeDataSet fakeDataSet = FakeDataSet fakeDataSet =
new FakeDataSet() new FakeDataSet()
......
...@@ -25,6 +25,7 @@ import android.content.Context; ...@@ -25,6 +25,7 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.support.annotation.Nullable; 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.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
...@@ -238,11 +239,11 @@ public class DownloadManagerDashTest { ...@@ -238,11 +239,11 @@ public class DownloadManagerDashTest {
Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet);
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory),
/* maxSimultaneousDownloads= */ 1,
/* minRetryCount= */ 3,
actionFile, actionFile,
DashDownloadAction.DESERIALIZER); new DefaultDownloaderFactory(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory)),
/* maxSimultaneousDownloads= */ 1,
/* minRetryCount= */ 3);
downloadManagerListener = downloadManagerListener =
new TestDownloadManagerListener(downloadManager, dummyMainThread); new TestDownloadManagerListener(downloadManager, dummyMainThread);
...@@ -257,9 +258,13 @@ public class DownloadManagerDashTest { ...@@ -257,9 +258,13 @@ public class DownloadManagerDashTest {
Collections.addAll(keysList, keys); Collections.addAll(keysList, keys);
DownloadAction result; DownloadAction result;
if (isRemoveAction) { if (isRemoveAction) {
result = DashDownloadAction.createRemoveAction(uri, data); result =
DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data);
} else { } else {
result = DashDownloadAction.createDownloadAction(uri, data, keysList); result =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, data);
} }
return result; return result;
} }
......
...@@ -24,6 +24,7 @@ import android.content.Context; ...@@ -24,6 +24,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; 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.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
...@@ -116,11 +117,11 @@ public class DownloadServiceDashTest { ...@@ -116,11 +117,11 @@ public class DownloadServiceDashTest {
actionFile.delete(); actionFile.delete();
final DownloadManager dashDownloadManager = final DownloadManager dashDownloadManager =
new DownloadManager( new DownloadManager(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory),
1,
3,
actionFile, actionFile,
DashDownloadAction.DESERIALIZER); new DefaultDownloaderFactory(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory)),
/* maxSimultaneousDownloads= */ 1,
/* minRetryCount= */ 3);
downloadManagerListener = downloadManagerListener =
new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); new TestDownloadManagerListener(dashDownloadManager, dummyMainThread);
dashDownloadManager.addListener(downloadManagerListener); dashDownloadManager.addListener(downloadManagerListener);
...@@ -211,9 +212,13 @@ public class DownloadServiceDashTest { ...@@ -211,9 +212,13 @@ public class DownloadServiceDashTest {
Collections.addAll(keysList, keys); Collections.addAll(keysList, keys);
DownloadAction result; DownloadAction result;
if (isRemoveAction) { if (isRemoveAction) {
result = DashDownloadAction.createRemoveAction(uri, data); result =
DownloadAction.createRemoveAction(
DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data);
} else { } else {
result = DashDownloadAction.createDownloadAction(uri, data, keysList); result =
DownloadAction.createDownloadAction(
DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, data);
} }
return result; return result;
} }
......
/*
* 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);
}
}
...@@ -19,6 +19,7 @@ import android.net.Uri; ...@@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.offline.TrackKey;
...@@ -97,15 +98,20 @@ public final class HlsDownloadHelper extends DownloadHelper { ...@@ -97,15 +98,20 @@ public final class HlsDownloadHelper extends DownloadHelper {
} }
@Override @Override
public HlsDownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) { public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
Assertions.checkNotNull(renditionGroups); Assertions.checkNotNull(renditionGroups);
return HlsDownloadAction.createDownloadAction( return DownloadAction.createDownloadAction(
uri, data, toStreamKeys(trackKeys, renditionGroups)); DownloadAction.TYPE_HLS,
uri,
toStreamKeys(trackKeys, renditionGroups),
/* customCacheKey= */ null,
data);
} }
@Override @Override
public HlsDownloadAction getRemoveAction(@Nullable byte[] data) { public DownloadAction getRemoveAction(@Nullable byte[] data) {
return HlsDownloadAction.createRemoveAction(uri, data); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null, data);
} }
private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) { private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) {
......
...@@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction; 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.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
...@@ -33,11 +31,10 @@ import java.util.Collections; ...@@ -33,11 +31,10 @@ import java.util.Collections;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link HlsDownloadAction}. */ // TODO: Merge into DownloadActionTest
/** Unit tests for HLS {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class HlsDownloadActionTest { public class HlsDownloadActionTest {
...@@ -63,15 +60,6 @@ public class HlsDownloadActionTest { ...@@ -63,15 +60,6 @@ public class HlsDownloadActionTest {
} }
@Test @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() { public void testSameUriDifferentAction_IsSameMedia() {
DownloadAction action1 = createRemoveAction(uri1); DownloadAction action1 = createRemoveAction(uri1);
DownloadAction action2 = createDownloadAction(uri1); DownloadAction action2 = createDownloadAction(uri1);
...@@ -140,10 +128,18 @@ public class HlsDownloadActionTest { ...@@ -140,10 +128,18 @@ public class HlsDownloadActionTest {
@Test @Test
public void testSerializerVersion0() throws Exception { public void testSerializerVersion0() throws Exception {
doTestSerializationV0RoundTrip(createDownloadAction(uri1)); doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 0);
doTestSerializationV0RoundTrip(createRemoveAction(uri1)); doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 0);
doTestSerializationV0RoundTrip( doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); 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) { private static void assertNotEqual(DownloadAction action1, DownloadAction action2) {
...@@ -159,17 +155,19 @@ public class HlsDownloadActionTest { ...@@ -159,17 +155,19 @@ public class HlsDownloadActionTest {
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out); DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output); action.serializeToStream(output);
assertEqual(action, deserializeActionFromStream(out)); 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(); ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out); DataOutputStream output = new DataOutputStream(out);
DataOutputStream dataOutputStream = new DataOutputStream(output); DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type); dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(/* version */ 0); dataOutputStream.writeInt(version);
dataOutputStream.writeUTF(action.uri.toString()); dataOutputStream.writeUTF(action.uri.toString());
dataOutputStream.writeBoolean(action.isRemoveAction); dataOutputStream.writeBoolean(action.isRemoveAction);
dataOutputStream.writeInt(action.data.length); dataOutputStream.writeInt(action.data.length);
...@@ -177,6 +175,9 @@ public class HlsDownloadActionTest { ...@@ -177,6 +175,9 @@ public class HlsDownloadActionTest {
dataOutputStream.writeInt(action.keys.size()); dataOutputStream.writeInt(action.keys.size());
for (int i = 0; i < action.keys.size(); i++) { for (int i = 0; i < action.keys.size(); i++) {
StreamKey key = action.keys.get(i); StreamKey key = action.keys.get(i);
if (version == 1) {
dataOutputStream.writeInt(key.periodIndex);
}
dataOutputStream.writeInt(key.groupIndex); dataOutputStream.writeInt(key.groupIndex);
dataOutputStream.writeInt(key.trackIndex); dataOutputStream.writeInt(key.trackIndex);
} }
...@@ -189,17 +190,18 @@ public class HlsDownloadActionTest { ...@@ -189,17 +190,18 @@ public class HlsDownloadActionTest {
throws IOException { throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in); DataInputStream input = new DataInputStream(in);
return DownloadAction.deserializeFromStream( return DownloadAction.deserializeFromStream(input);
new DownloadAction.Deserializer[] {HlsDownloadAction.DESERIALIZER}, input);
} }
private static HlsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) { private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
ArrayList<StreamKey> keysList = new ArrayList<>(); ArrayList<StreamKey> keysList = new ArrayList<>();
Collections.addAll(keysList, keys); 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) { private static DownloadAction createRemoveAction(Uri uri) {
return HlsDownloadAction.createRemoveAction(uri, null); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null, /* data= */ null);
} }
} }
...@@ -35,21 +35,29 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa ...@@ -35,21 +35,29 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; 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.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; 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.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
...@@ -85,6 +93,23 @@ public class HlsDownloaderTest { ...@@ -85,6 +93,23 @@ public class HlsDownloaderTest {
} }
@Test @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 { public void testCounterMethods() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
......
/*
* 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);
}
}
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; 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.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey; import com.google.android.exoplayer2.offline.TrackKey;
...@@ -76,13 +77,15 @@ public final class SsDownloadHelper extends DownloadHelper { ...@@ -76,13 +77,15 @@ public final class SsDownloadHelper extends DownloadHelper {
} }
@Override @Override
public SsDownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) { public DownloadAction getDownloadAction(@Nullable byte[] data, List<TrackKey> trackKeys) {
return SsDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys)); return DownloadAction.createDownloadAction(
DownloadAction.TYPE_SS, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data);
} }
@Override @Override
public SsDownloadAction getRemoveAction(@Nullable byte[] data) { public DownloadAction getRemoveAction(@Nullable byte[] data) {
return SsDownloadAction.createRemoveAction(uri, data); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null, data);
} }
private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
......
...@@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.offline.DownloadAction; 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.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.upstream.cache.Cache;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
...@@ -33,11 +31,10 @@ import java.util.Collections; ...@@ -33,11 +31,10 @@ import java.util.Collections;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
/** Unit tests for {@link SsDownloadAction}. */ // TODO: Merge into DownloadActionTest
/** Unit tests for SmoothStreaming {@link DownloadAction}s. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class SsDownloadActionTest { public class SsDownloadActionTest {
...@@ -63,15 +60,6 @@ public class SsDownloadActionTest { ...@@ -63,15 +60,6 @@ public class SsDownloadActionTest {
} }
@Test @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() { public void testSameUriDifferentAction_IsSameMedia() {
DownloadAction action1 = createRemoveAction(uri1); DownloadAction action1 = createRemoveAction(uri1);
DownloadAction action2 = createDownloadAction(uri1); DownloadAction action2 = createDownloadAction(uri1);
...@@ -140,10 +128,18 @@ public class SsDownloadActionTest { ...@@ -140,10 +128,18 @@ public class SsDownloadActionTest {
@Test @Test
public void testSerializerVersion0() throws Exception { public void testSerializerVersion0() throws Exception {
doTestSerializationV0RoundTrip(createDownloadAction(uri1)); doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 0);
doTestSerializationV0RoundTrip(createRemoveAction(uri1)); doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 0);
doTestSerializationV0RoundTrip( doTestLegacySerializationRoundTrip(
createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); 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) { private static void assertNotEqual(DownloadAction action1, DownloadAction action2) {
...@@ -159,17 +155,19 @@ public class SsDownloadActionTest { ...@@ -159,17 +155,19 @@ public class SsDownloadActionTest {
private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out); DataOutputStream output = new DataOutputStream(out);
DownloadAction.serializeToStream(action, output); action.serializeToStream(output);
assertEqual(action, deserializeActionFromStream(out)); 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(); ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(out); DataOutputStream output = new DataOutputStream(out);
DataOutputStream dataOutputStream = new DataOutputStream(output); DataOutputStream dataOutputStream = new DataOutputStream(output);
dataOutputStream.writeUTF(action.type); dataOutputStream.writeUTF(action.type);
dataOutputStream.writeInt(/* version */ 0); dataOutputStream.writeInt(version);
dataOutputStream.writeUTF(action.uri.toString()); dataOutputStream.writeUTF(action.uri.toString());
dataOutputStream.writeBoolean(action.isRemoveAction); dataOutputStream.writeBoolean(action.isRemoveAction);
dataOutputStream.writeInt(action.data.length); dataOutputStream.writeInt(action.data.length);
...@@ -177,6 +175,9 @@ public class SsDownloadActionTest { ...@@ -177,6 +175,9 @@ public class SsDownloadActionTest {
dataOutputStream.writeInt(action.keys.size()); dataOutputStream.writeInt(action.keys.size());
for (int i = 0; i < action.keys.size(); i++) { for (int i = 0; i < action.keys.size(); i++) {
StreamKey key = action.keys.get(i); StreamKey key = action.keys.get(i);
if (version == 1) {
dataOutputStream.writeInt(key.periodIndex);
}
dataOutputStream.writeInt(key.groupIndex); dataOutputStream.writeInt(key.groupIndex);
dataOutputStream.writeInt(key.trackIndex); dataOutputStream.writeInt(key.trackIndex);
} }
...@@ -189,17 +190,18 @@ public class SsDownloadActionTest { ...@@ -189,17 +190,18 @@ public class SsDownloadActionTest {
throws IOException { throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
DataInputStream input = new DataInputStream(in); DataInputStream input = new DataInputStream(in);
return DownloadAction.deserializeFromStream( return DownloadAction.deserializeFromStream(input);
new DownloadAction.Deserializer[] {SsDownloadAction.DESERIALIZER}, input);
} }
private static SsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) { private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
ArrayList<StreamKey> keysList = new ArrayList<>(); ArrayList<StreamKey> keysList = new ArrayList<>();
Collections.addAll(keysList, keys); 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) { private static DownloadAction createRemoveAction(Uri uri) {
return SsDownloadAction.createRemoveAction(uri, null); return DownloadAction.createRemoveAction(
DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null, /* data= */ null);
} }
} }
/*
* 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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment