diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index ac8be7dc16..65d5096f30 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.demo; import android.app.Application; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.upstream.DataSource; @@ -87,10 +88,10 @@ public class DemoApplication extends Application { new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = new DownloadManager( - downloaderConstructorHelper, + new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), + new DefaultDownloaderFactory(downloaderConstructorHelper), MAX_SIMULTANEOUS_DOWNLOADS, - DownloadManager.DEFAULT_MIN_RETRY_COUNT, - new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE)); + DownloadManager.DEFAULT_MIN_RETRY_COUNT); downloadTracker = new DownloadTracker( /* context= */ this, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index b0619a82fd..422411d849 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -81,11 +81,7 @@ public class DownloadTracker implements DownloadManager.Listener { private final ActionFile actionFile; private final Handler actionFileWriteHandler; - public DownloadTracker( - Context context, - DataSource.Factory dataSourceFactory, - File actionFile, - DownloadAction.Deserializer... deserializers) { + public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) { this.context = context.getApplicationContext(); this.dataSourceFactory = dataSourceFactory; this.actionFile = new ActionFile(actionFile); @@ -95,8 +91,7 @@ public class DownloadTracker implements DownloadManager.Listener { HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); actionFileWriteThread.start(); actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); - loadTrackedActions( - deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers()); + loadTrackedActions(); } public void addListener(Listener listener) { @@ -158,9 +153,9 @@ public class DownloadTracker implements DownloadManager.Listener { // Internal methods - private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) { + private void loadTrackedActions() { try { - DownloadAction[] allActions = actionFile.load(deserializers); + DownloadAction[] allActions = actionFile.load(); for (DownloadAction action : allActions) { trackedDownloadStates.put(action.uri, action); } diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index a5c50a78f5..5aec86ba8c 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -30,10 +30,19 @@ (); } -# Constructors accessed via reflection in DownloadAction --dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction --dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction --dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction +# Constructors accessed via reflection in DefaultDownloaderFactory +-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader +-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader { + (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 { + (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 { + (android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper); +} # Don't warn about checkerframework -dontwarn org.checkerframework.** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java index e37e09a090..f77c83b33b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.offline; -import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.Util; import java.io.DataInputStream; @@ -45,11 +44,10 @@ public final class ActionFile { /** * Loads {@link DownloadAction}s from file. * - * @param deserializers {@link Deserializer}s to deserialize DownloadActions. * @return Loaded DownloadActions. If the action file doesn't exists returns an empty array. * @throws IOException If there is an error during loading. */ - public DownloadAction[] load(Deserializer... deserializers) throws IOException { + public DownloadAction[] load() throws IOException { if (!actionFile.exists()) { return new DownloadAction[0]; } @@ -64,7 +62,7 @@ public final class ActionFile { int actionCount = dataInputStream.readInt(); DownloadAction[] actions = new DownloadAction[actionCount]; for (int i = 0; i < actionCount; i++) { - actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream); + actions[i] = DownloadAction.deserializeFromStream(dataInputStream); } return actions; } finally { @@ -85,7 +83,7 @@ public final class ActionFile { output.writeInt(VERSION); output.writeInt(downloadActions.length); for (DownloadAction action : downloadActions) { - DownloadAction.serializeToStream(action, output); + action.serializeToStream(output); } atomicFile.endWrite(output); // Avoid calling close twice. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java new file mode 100644 index 0000000000..63ec72f358 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * Default {@link DownloaderFactory}, supporting creation of progressive, DASH, HLS and + * SmoothStreaming downloaders. Note that for the latter three, the corresponding library module + * must be built into the application. + */ +public class DefaultDownloaderFactory implements DownloaderFactory { + + @Nullable private static final Constructor DASH_DOWNLOADER_CONSTRUCTOR; + @Nullable private static final Constructor HLS_DOWNLOADER_CONSTRUCTOR; + @Nullable private static final Constructor SS_DOWNLOADER_CONSTRUCTOR; + + static { + Constructor 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 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 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 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 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) +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index e9868ceea0..46f5c78e55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -25,147 +25,124 @@ import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** Contains the necessary parameters for a download or remove action. */ -public abstract class DownloadAction { +public final class DownloadAction { - /** Used to deserialize {@link DownloadAction}s. */ - public abstract static class Deserializer { + /** Type for progressive downloads. */ + public static final String TYPE_PROGRESSIVE = "progressive"; + /** Type for DASH downloads. */ + public static final String TYPE_DASH = "dash"; + /** Type for HLS downloads. */ + public static final String TYPE_HLS = "hls"; + /** Type for SmoothStreaming downloads. */ + public static final String TYPE_SS = "ss"; - public final String type; - public final int version; - - public Deserializer(String type, int version) { - this.type = type; - this.version = version; - } - - /** - * Deserializes an action from the {@code input}. - * - * @param version The version of the serialized action. - * @param input The stream from which to read the action. - * @see DownloadAction#writeToStream(DataOutputStream) - */ - public abstract DownloadAction readFromStream(int version, DataInputStream input) - throws IOException; - } - - private static @Nullable Deserializer[] defaultDeserializers; - - /** Returns available default {@link Deserializer}s. */ - public static synchronized Deserializer[] getDefaultDeserializers() { - if (defaultDeserializers != null) { - return defaultDeserializers; - } - Deserializer[] deserializers = new Deserializer[4]; - int count = 0; - deserializers[count++] = ProgressiveDownloadAction.DESERIALIZER; - Class clazz; - // Full class names used for constructor args so the LINT rule triggers if any of them move. - try { - // LINT.IfChange - clazz = Class.forName("com.google.android.exoplayer2.source.dash.offline.DashDownloadAction"); - // LINT.ThenChange(../../../../../../../../../dash/proguard-rules.txt) - deserializers[count++] = getDeserializer(clazz); - } catch (Exception e) { - // Do nothing. - } - try { - // LINT.IfChange - clazz = Class.forName("com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction"); - // LINT.ThenChange(../../../../../../../../../hls/proguard-rules.txt) - deserializers[count++] = getDeserializer(clazz); - } catch (Exception e) { - // Do nothing. - } - try { - // LINT.IfChange - clazz = - Class.forName( - "com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction"); - // LINT.ThenChange(../../../../../../../../../smoothstreaming/proguard-rules.txt) - deserializers[count++] = getDeserializer(clazz); - } catch (Exception e) { - // Do nothing. - } - defaultDeserializers = Arrays.copyOf(Assertions.checkNotNull(deserializers), count); - return defaultDeserializers; - } + private static final int VERSION = 2; /** - * Deserializes one action that was serialized with {@link #serializeToStream(DownloadAction, - * OutputStream)} from the {@code input}, using the {@link Deserializer}s that supports the - * action's type. + * Deserializes one action that was serialized with {@link #serializeToStream(OutputStream)} from + * the {@code input}. * *

The caller is responsible for closing the given {@link InputStream}. * - * @param deserializers {@link Deserializer}s for supported actions. - * @param input The stream from which to read the action. + * @param input The stream from which to read. * @return The deserialized action. * @throws IOException If there is an IO error reading from {@code input}, or if the action type * isn't supported by any of the {@code deserializers}. */ - public static DownloadAction deserializeFromStream( - Deserializer[] deserializers, InputStream input) throws IOException { - // Don't close the stream as it closes the underlying stream too. - DataInputStream dataInputStream = new DataInputStream(input); - String type = dataInputStream.readUTF(); - int version = dataInputStream.readInt(); - for (Deserializer deserializer : deserializers) { - if (type.equals(deserializer.type) && deserializer.version >= version) { - return deserializer.readFromStream(version, dataInputStream); - } - } - throw new DownloadException("No deserializer found for:" + type + ", " + version); + public static DownloadAction deserializeFromStream(InputStream input) throws IOException { + return readFromStream(new DataInputStream(input)); } - /** Serializes {@code action} type and data into the {@code output}. */ - public static void serializeToStream(DownloadAction action, OutputStream output) - throws IOException { - // Don't close the stream as it closes the underlying stream too. - DataOutputStream dataOutputStream = new DataOutputStream(output); - dataOutputStream.writeUTF(action.type); - dataOutputStream.writeInt(action.version); - action.writeToStream(dataOutputStream); - dataOutputStream.flush(); + /** + * Creates a DASH download action. + * + * @param type The type of the action. + * @param uri The URI of the media to be downloaded. + * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. + * @param customCacheKey A custom key for cache indexing, or null. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + */ + public static DownloadAction createDownloadAction( + String type, + Uri uri, + List keys, + @Nullable String customCacheKey, + @Nullable byte[] data) { + return new DownloadAction(type, uri, /* isRemoveAction= */ false, keys, customCacheKey, data); + } + + /** + * Creates a DASH remove action. + * + * @param type The type of the action. + * @param uri The URI of the media to be removed. + * @param customCacheKey A custom key for cache indexing, or null. + * @param data Optional custom data for this action. If {@code null} an empty array will be used. + */ + public static DownloadAction createRemoveAction( + String type, Uri uri, @Nullable String customCacheKey, @Nullable byte[] data) { + return new DownloadAction( + type, uri, /* isRemoveAction= */ true, Collections.emptyList(), customCacheKey, data); } /** The type of the action. */ public final String type; - /** The action version. */ - public final int version; /** The uri being downloaded or removed. */ public final Uri uri; /** Whether this is a remove action. If false, this is a download action. */ public final boolean isRemoveAction; + /** + * Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action + * is a remove action. + */ + public final List keys; + /** A custom key for cache indexing, or null. */ + @Nullable public final String customCacheKey; /** Custom data for this action. May be empty. */ public final byte[] data; /** * @param type The type of the action. - * @param version The action version. * @param uri The uri being downloaded or removed. * @param isRemoveAction Whether this is a remove action. If false, this is a download action. + * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if + * this action is a remove action. + * @param customCacheKey A custom key for cache indexing, or null. * @param data Optional custom data for this action. */ - protected DownloadAction( - String type, int version, Uri uri, boolean isRemoveAction, @Nullable byte[] data) { + private DownloadAction( + String type, + Uri uri, + boolean isRemoveAction, + List keys, + @Nullable String customCacheKey, + @Nullable byte[] data) { this.type = type; - this.version = version; this.uri = uri; this.isRemoveAction = isRemoveAction; + this.customCacheKey = customCacheKey; this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY; + if (isRemoveAction) { + Assertions.checkArgument(keys.isEmpty()); + this.keys = Collections.emptyList(); + } else { + ArrayList mutableKeys = new ArrayList<>(keys); + Collections.sort(mutableKeys); + this.keys = Collections.unmodifiableList(mutableKeys); + } } /** Serializes itself into a byte array. */ - public final byte[] toByteArray() { + public byte[] toByteArray() { ByteArrayOutputStream output = new ByteArrayOutputStream(); try { - serializeToStream(this, output); + serializeToStream(output); } catch (IOException e) { // ByteArrayOutputStream shouldn't throw IOException. throw new IllegalStateException(); @@ -175,46 +152,118 @@ public abstract class DownloadAction { /** Returns whether this is an action for the same media as the {@code other}. */ public boolean isSameMedia(DownloadAction other) { - return uri.equals(other.uri); + return customCacheKey == null + ? other.customCacheKey == null && uri.equals(other.uri) + : customCacheKey.equals(other.customCacheKey); } /** Returns keys of tracks to be downloaded. */ public List getKeys() { - return Collections.emptyList(); + return keys; } - /** Serializes itself into the {@code output}. */ - protected abstract void writeToStream(DataOutputStream output) throws IOException; - - /** Creates a {@link Downloader} with the given parameters. */ - public abstract Downloader createDownloader( - DownloaderConstructorHelper downloaderConstructorHelper); - - @SuppressWarnings("EqualsGetClass") @Override public boolean equals(@Nullable Object o) { - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof DownloadAction)) { return false; } DownloadAction that = (DownloadAction) o; return type.equals(that.type) - && version == that.version && uri.equals(that.uri) && isRemoveAction == that.isRemoveAction + && keys.equals(that.keys) + && Util.areEqual(customCacheKey, that.customCacheKey) && Arrays.equals(data, that.data); } @Override - public int hashCode() { - int result = uri.hashCode(); + public final int hashCode() { + int result = type.hashCode(); + result = 31 * result + uri.hashCode(); result = 31 * result + (isRemoveAction ? 1 : 0); + result = 31 * result + keys.hashCode(); + result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); result = 31 * result + Arrays.hashCode(data); return result; } - private static Deserializer getDeserializer(Class clazz) - throws NoSuchFieldException, IllegalAccessException { - Object value = clazz.getDeclaredField("DESERIALIZER").get(null); - return (Deserializer) Assertions.checkNotNull(value); + // Serialization. + + /** + * Serializes this action into an {@link OutputStream}. + * + * @param output The stream to write to. + */ + public final void serializeToStream(OutputStream output) throws IOException { + // Don't close the stream as it closes the underlying stream too. + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(type); + dataOutputStream.writeInt(VERSION); + dataOutputStream.writeUTF(uri.toString()); + dataOutputStream.writeBoolean(isRemoveAction); + dataOutputStream.writeInt(data.length); + dataOutputStream.write(data); + dataOutputStream.writeInt(keys.size()); + for (int i = 0; i < keys.size(); i++) { + StreamKey key = keys.get(i); + dataOutputStream.writeInt(key.periodIndex); + dataOutputStream.writeInt(key.groupIndex); + dataOutputStream.writeInt(key.trackIndex); + } + dataOutputStream.writeBoolean(customCacheKey != null); + if (customCacheKey != null) { + dataOutputStream.writeUTF(customCacheKey); + } + dataOutputStream.flush(); + } + + private static DownloadAction readFromStream(DataInputStream input) throws IOException { + String type = input.readUTF(); + int version = input.readInt(); + + Uri uri = Uri.parse(input.readUTF()); + boolean isRemoveAction = input.readBoolean(); + int dataLength = input.readInt(); + byte[] data = new byte[dataLength]; + input.readFully(data); + + // Serialized version 0 progressive actions did not contain keys. + boolean isLegacyProgressive = version == 0 && TYPE_PROGRESSIVE.equals(type); + List 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); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 3757517f5d..9437a410fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -28,9 +28,6 @@ import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -85,11 +82,10 @@ public final class DownloadManager { private static final String TAG = "DownloadManager"; private static final boolean DEBUG = false; - private final DownloaderConstructorHelper downloaderConstructorHelper; private final int maxActiveDownloadTasks; private final int minRetryCount; private final ActionFile actionFile; - private final DownloadAction.Deserializer[] deserializers; + private final DownloaderFactory downloaderFactory; private final ArrayList tasks; private final ArrayList activeDownloadTasks; private final Handler handler; @@ -102,71 +98,34 @@ public final class DownloadManager { private boolean released; private boolean downloadsStopped; - /** - * Creates a {@link DownloadManager}. - * - * @param cache Cache instance to be used to store downloaded data. - * @param upstreamDataSourceFactory A {@link DataSource.Factory} for creating data sources for - * downloading upstream data. - * @param actionSaveFile File to save active actions. - * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link - * DownloadAction#getDefaultDeserializers()} is used instead. - */ - public DownloadManager( - Cache cache, - DataSource.Factory upstreamDataSourceFactory, - File actionSaveFile, - Deserializer... deserializers) { - this( - new DownloaderConstructorHelper(cache, upstreamDataSourceFactory), - actionSaveFile, - deserializers); - } - /** * Constructs a {@link DownloadManager}. * - * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s - * for downloading data. * @param actionFile The file in which active actions are saved. - * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link - * DownloadAction#getDefaultDeserializers()} is used instead. + * @param downloaderFactory A factory for creating {@link Downloader}s. */ - public DownloadManager( - DownloaderConstructorHelper constructorHelper, - File actionFile, - Deserializer... deserializers) { + public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) { this( - constructorHelper, - DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, - DEFAULT_MIN_RETRY_COUNT, - actionFile, - deserializers); + actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT); } /** * Constructs a {@link DownloadManager}. * - * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s - * for downloading data. + * @param actionFile The file in which active actions are saved. + * @param downloaderFactory A factory for creating {@link Downloader}s. * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks. * @param minRetryCount The minimum number of times a task must be retried before failing. - * @param actionFile The file in which active actions are saved. - * @param deserializers Used to deserialize {@link DownloadAction}s. If empty, {@link - * DownloadAction#getDefaultDeserializers()} is used instead. */ public DownloadManager( - DownloaderConstructorHelper constructorHelper, - int maxSimultaneousDownloads, - int minRetryCount, File actionFile, - Deserializer... deserializers) { - this.downloaderConstructorHelper = constructorHelper; + DownloaderFactory downloaderFactory, + int maxSimultaneousDownloads, + int minRetryCount) { + this.actionFile = new ActionFile(actionFile); + this.downloaderFactory = downloaderFactory; this.maxActiveDownloadTasks = maxSimultaneousDownloads; this.minRetryCount = minRetryCount; - this.actionFile = new ActionFile(actionFile); - this.deserializers = - deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers(); this.downloadsStopped = true; tasks = new ArrayList<>(); @@ -239,7 +198,7 @@ public final class DownloadManager { public int handleAction(byte[] actionData) throws IOException { Assertions.checkState(!released); ByteArrayInputStream input = new ByteArrayInputStream(actionData); - DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input); + DownloadAction action = DownloadAction.deserializeFromStream(input); return handleAction(action); } @@ -344,7 +303,7 @@ public final class DownloadManager { } private Task addTaskForAction(DownloadAction action) { - Task task = new Task(nextTaskId++, this, action, minRetryCount); + Task task = new Task(nextTaskId++, this, downloaderFactory, action, minRetryCount); tasks.add(task); logd("Task is added", task); return task; @@ -450,7 +409,7 @@ public final class DownloadManager { () -> { DownloadAction[] loadedActions; try { - loadedActions = actionFile.load(DownloadManager.this.deserializers); + loadedActions = actionFile.load(); logd("Action file is loaded."); } catch (Throwable e) { Log.e(TAG, "Action file loading failed.", e); @@ -642,6 +601,7 @@ public final class DownloadManager { private final int id; private final DownloadManager downloadManager; + private final DownloaderFactory downloaderFactory; private final DownloadAction action; private final int minRetryCount; private volatile @InternalState int currentState; @@ -650,9 +610,14 @@ public final class DownloadManager { private Throwable error; private Task( - int id, DownloadManager downloadManager, DownloadAction action, int minRetryCount) { + int id, + DownloadManager downloadManager, + DownloaderFactory downloaderFactory, + DownloadAction action, + int minRetryCount) { this.id = id; this.downloadManager = downloadManager; + this.downloaderFactory = downloaderFactory; this.action = action; this.currentState = STATE_QUEUED; this.minRetryCount = minRetryCount; @@ -807,7 +772,7 @@ public final class DownloadManager { logd("Task is started", this); Throwable error = null; try { - downloader = action.createDownloader(downloadManager.downloaderConstructorHelper); + downloader = downloaderFactory.createDownloader(action); if (action.isRemoveAction) { downloader.remove(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderFactory.java new file mode 100644 index 0000000000..a2699e560f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +/** Creates {@link Downloader Downloaders} for given {@link DownloadAction DownloadActions}. */ +public interface DownloaderFactory { + + /** + * Creates a {@link Downloader} to perform the given {@link DownloadAction}. + * + * @param action The action. + * @return The downloader. + */ + Downloader createDownloader(DownloadAction action); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java deleted file mode 100644 index 7ced2fa41b..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.util.Util; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** An action to download or remove downloaded progressive streams. */ -public final class ProgressiveDownloadAction extends DownloadAction { - - private static final String TYPE = "progressive"; - private static final int VERSION = 0; - - public static final Deserializer DESERIALIZER = - new Deserializer(TYPE, VERSION) { - @Override - public ProgressiveDownloadAction readFromStream(int version, DataInputStream input) - throws IOException { - Uri uri = Uri.parse(input.readUTF()); - boolean isRemoveAction = input.readBoolean(); - int dataLength = input.readInt(); - byte[] data = new byte[dataLength]; - input.readFully(data); - String customCacheKey = input.readBoolean() ? input.readUTF() : null; - return new ProgressiveDownloadAction(uri, isRemoveAction, data, customCacheKey); - } - }; - - private final @Nullable String customCacheKey; - - /** - * Creates a progressive stream download action. - * - * @param uri Uri of the data to be downloaded. - * @param data Optional custom data for this action. - * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it - * is used for cache indexing. - */ - public static ProgressiveDownloadAction createDownloadAction( - Uri uri, @Nullable byte[] data, @Nullable String customCacheKey) { - return new ProgressiveDownloadAction(uri, /* isRemoveAction= */ false, data, customCacheKey); - } - - /** - * Creates a progressive stream remove action. - * - * @param uri Uri of the data to be removed. - * @param data Optional custom data for this action. - * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it - * is used for cache indexing. - */ - public static ProgressiveDownloadAction createRemoveAction( - Uri uri, @Nullable byte[] data, @Nullable String customCacheKey) { - return new ProgressiveDownloadAction(uri, /* isRemoveAction= */ true, data, customCacheKey); - } - - /** - * @param uri Uri of the data to be downloaded. - * @param isRemoveAction Whether this is a remove action. If false, this is a download action. - * @param data Optional custom data for this action. - * @param customCacheKey A custom key that uniquely identifies the original stream. If not null it - * is used for cache indexing. - * @deprecated Use {@link #createDownloadAction(Uri, byte[], String)} or {@link - * #createRemoveAction(Uri, byte[], String)}. - */ - @Deprecated - public ProgressiveDownloadAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, @Nullable String customCacheKey) { - super(TYPE, VERSION, uri, isRemoveAction, data); - this.customCacheKey = customCacheKey; - } - - @Override - public ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - return new ProgressiveDownloader(uri, customCacheKey, constructorHelper); - } - - @Override - protected void writeToStream(DataOutputStream output) throws IOException { - output.writeUTF(uri.toString()); - output.writeBoolean(isRemoveAction); - output.writeInt(data.length); - output.write(data); - boolean customCacheKeySet = customCacheKey != null; - output.writeBoolean(customCacheKeySet); - if (customCacheKeySet) { - output.writeUTF(customCacheKey); - } - } - - @Override - public boolean isSameMedia(DownloadAction other) { - return ((other instanceof ProgressiveDownloadAction) - && getCacheKey().equals(((ProgressiveDownloadAction) other).getCacheKey())); - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!super.equals(o)) { - return false; - } - ProgressiveDownloadAction that = (ProgressiveDownloadAction) o; - return Util.areEqual(customCacheKey, that.customCacheKey); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); - return result; - } - - private String getCacheKey() { - return customCacheKey != null ? customCacheKey : CacheUtil.generateKey(uri); - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java index 473209803a..f2022c3b46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.TrackGroupArray; +import java.util.Collections; import java.util.List; /** A {@link DownloadHelper} for progressive streams. */ @@ -51,13 +52,18 @@ public final class ProgressiveDownloadHelper extends DownloadHelper { } @Override - public ProgressiveDownloadAction getDownloadAction( - @Nullable byte[] data, List trackKeys) { - return ProgressiveDownloadAction.createDownloadAction(uri, data, customCacheKey); + public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_PROGRESSIVE, + uri, + /* keys= */ Collections.emptyList(), + customCacheKey, + data); } @Override - public ProgressiveDownloadAction getRemoveAction(@Nullable byte[] data) { - return ProgressiveDownloadAction.createRemoveAction(uri, data, customCacheKey); + public DownloadAction getRemoveAction(@Nullable byte[] data) { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey, data); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index adb1c80b0e..eedb5f7b00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.Cache; @@ -47,7 +48,7 @@ public final class ProgressiveDownloader implements Downloader { * @param constructorHelper A {@link DownloaderConstructorHelper} instance. */ public ProgressiveDownloader( - Uri uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) { + Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) { this.dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, customCacheKey, 0); this.cache = constructorHelper.getCache(); this.dataSource = constructorHelper.createCacheDataSource(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java deleted file mode 100644 index 403b4e797b..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** {@link DownloadAction} for {@link SegmentDownloader}s. */ -public abstract class SegmentDownloadAction extends DownloadAction { - - /** Base class for {@link SegmentDownloadAction} {@link Deserializer}s. */ - protected abstract static class SegmentDownloadActionDeserializer extends Deserializer { - - public SegmentDownloadActionDeserializer(String type, int version) { - super(type, version); - } - - @Override - public final DownloadAction readFromStream(int version, DataInputStream input) - throws IOException { - Uri uri = Uri.parse(input.readUTF()); - boolean isRemoveAction = input.readBoolean(); - int dataLength = input.readInt(); - byte[] data = new byte[dataLength]; - input.readFully(data); - int keyCount = input.readInt(); - List 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 keys); - } - - public final List 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 keys) { - super(type, version, uri, isRemoveAction, data); - if (isRemoveAction) { - Assertions.checkArgument(keys.isEmpty()); - this.keys = Collections.emptyList(); - } else { - ArrayList mutableKeys = new ArrayList<>(keys); - Collections.sort(mutableKeys); - this.keys = Collections.unmodifiableList(mutableKeys); - } - } - - @Override - public List 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); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java index 634d541d39..d5621ff12c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -18,13 +18,13 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; -import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Collections; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -38,10 +38,22 @@ import org.robolectric.RuntimeEnvironment; public class ActionFileTest { private File tempFile; + private DownloadAction action1; + private DownloadAction action2; @Before public void setUp() throws Exception { tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + action1 = + buildAction( + DownloadAction.TYPE_PROGRESSIVE, + Uri.parse("http://test1.uri"), + TestUtil.buildTestData(16)); + action2 = + buildAction( + DownloadAction.TYPE_PROGRESSIVE, + Uri.parse("http://test2.uri"), + TestUtil.buildTestData(32)); } @After @@ -78,44 +90,32 @@ public class ActionFileTest { @Test public void testLoadAction() throws Exception { - byte[] data = Util.getUtf8Bytes("321"); DownloadAction[] actions = loadActions( new Object[] { ActionFile.VERSION, 1, // Action count - "type2", // Action 1 - FakeDownloadAction.VERSION, - data, - }, - new FakeDeserializer("type2")); + action1 + }); assertThat(actions).isNotNull(); assertThat(actions).hasLength(1); - assertAction(actions[0], "type2", FakeDownloadAction.VERSION, data); + assertThat(actions[0]).isEqualTo(action1); } @Test public void testLoadActions() throws Exception { - byte[] data1 = Util.getUtf8Bytes("123"); - byte[] data2 = Util.getUtf8Bytes("321"); DownloadAction[] actions = loadActions( new Object[] { ActionFile.VERSION, 2, // Action count - "type1", // Action 1 - FakeDownloadAction.VERSION, - data1, - "type2", // Action 2 - FakeDownloadAction.VERSION, - data2, - }, - new FakeDeserializer("type1"), - new FakeDeserializer("type2")); + action1, + action2, + }); assertThat(actions).isNotNull(); assertThat(actions).hasLength(2); - assertAction(actions[0], "type1", FakeDownloadAction.VERSION, data1); - assertAction(actions[1], "type2", FakeDownloadAction.VERSION, data2); + assertThat(actions[0]).isEqualTo(action1); + assertThat(actions[1]).isEqualTo(action2); } @Test @@ -125,90 +125,39 @@ public class ActionFileTest { new Object[] { ActionFile.VERSION + 1, 1, // Action count - "type2", // Action 1 - FakeDownloadAction.VERSION, - Util.getUtf8Bytes("321"), - }, - new FakeDeserializer("type2")); + action1, + }); Assert.fail(); } catch (IOException e) { // Expected exception. } } - @Test - public void testLoadNotSupportedActionVersion() throws Exception { - try { - loadActions( - new Object[] { - ActionFile.VERSION, - 1, // Action count - "type2", // Action 1 - FakeDownloadAction.VERSION + 1, - Util.getUtf8Bytes("321"), - }, - new FakeDeserializer("type2")); - Assert.fail(); - } catch (IOException e) { - // Expected exception. - } - } - - @Test - public void testLoadNotSupportedType() throws Exception { - try { - loadActions( - new Object[] { - ActionFile.VERSION, - 1, // Action count - "type2", // Action 1 - FakeDownloadAction.VERSION, - Util.getUtf8Bytes("321"), - }, - new FakeDeserializer("type1")); - Assert.fail(); - } catch (DownloadException e) { - // Expected exception. - } - } - @Test public void testStoreAndLoadNoActions() throws Exception { - doTestSerializationRoundTrip(new DownloadAction[0]); + doTestSerializationRoundTrip(); } @Test public void testStoreAndLoadActions() throws Exception { - doTestSerializationRoundTrip( - new DownloadAction[] { - new FakeDownloadAction("type1", Util.getUtf8Bytes("123")), - new FakeDownloadAction("type2", Util.getUtf8Bytes("321")), - }, - new FakeDeserializer("type1"), - new FakeDeserializer("type2")); + doTestSerializationRoundTrip(action1, action2); } - private void doTestSerializationRoundTrip(DownloadAction[] actions, - Deserializer... deserializers) throws IOException { + private void doTestSerializationRoundTrip(DownloadAction... actions) throws IOException { ActionFile actionFile = new ActionFile(tempFile); actionFile.store(actions); - assertThat(actionFile.load(deserializers)).isEqualTo(actions); + assertThat(actionFile.load()).isEqualTo(actions); } - private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) - throws IOException { + private DownloadAction[] loadActions(Object[] values) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream(tempFile); DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); try { for (Object value : values) { if (value instanceof Integer) { dataOutputStream.writeInt((Integer) value); - } else if (value instanceof String) { - dataOutputStream.writeUTF((String) value); - } else if (value instanceof byte[]) { - byte[] data = (byte[]) value; - dataOutputStream.writeInt(data.length); - dataOutputStream.write(data); + } else if (value instanceof DownloadAction) { + ((DownloadAction) value).serializeToStream(dataOutputStream); } else { throw new IllegalArgumentException(); } @@ -216,50 +165,11 @@ public class ActionFileTest { } finally { dataOutputStream.close(); } - return new ActionFile(tempFile).load(deserializers); + return new ActionFile(tempFile).load(); } - private static void assertAction(DownloadAction action, String type, int version, byte[] data) { - assertThat(action).isInstanceOf(FakeDownloadAction.class); - assertThat(action.type).isEqualTo(type); - assertThat(((FakeDownloadAction) action).version).isEqualTo(version); - assertThat(((FakeDownloadAction) action).data).isEqualTo(data); + private static DownloadAction buildAction(String type, Uri uri, byte[] data) { + return DownloadAction.createDownloadAction( + type, uri, /* keys= */ Collections.emptyList(), /* customCacheKey= */ null, data); } - - private static class FakeDeserializer extends Deserializer { - - FakeDeserializer(String type) { - super(type, FakeDownloadAction.VERSION); - } - - @Override - public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { - int dataLength = input.readInt(); - byte[] data = new byte[dataLength]; - input.readFully(data); - return new FakeDownloadAction(type, data); - } - } - - private static class FakeDownloadAction extends DownloadAction { - - public static final int VERSION = 0; - - private FakeDownloadAction(String type, byte[] data) { - super(type, VERSION, Uri.parse("http://test.com"), /* isRemoveAction= */ false, data); - } - - @Override - protected void writeToStream(DataOutputStream output) throws IOException { - output.writeInt(data.length); - output.write(data); - } - - @Override - public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { - return null; - } - - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java new file mode 100644 index 0000000000..346d033e1a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link DefaultDownloaderFactory}. */ +@RunWith(RobolectricTestRunner.class) +public final class DefaultDownloaderFactoryTest { + + @Test + public void createProgressiveDownloader() throws Exception { + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + + Downloader downloader = + factory.createDownloader( + DownloadAction.createDownloadAction( + DownloadAction.TYPE_PROGRESSIVE, + Uri.parse("https://www.test.com/download"), + /* keys= */ Collections.emptyList(), + /* customCacheKey= */ null, + /* data= */ null)); + assertThat(downloader).isInstanceOf(ProgressiveDownloader.class); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 234377895f..3778641378 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -25,19 +25,17 @@ import com.google.android.exoplayer2.offline.DownloadManager.TaskState.State; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Util; -import java.io.DataOutputStream; import java.io.File; import java.io.IOException; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @@ -63,6 +61,7 @@ public class DownloadManagerTest { private DummyMainThread dummyMainThread; private File actionFile; private TestDownloadManagerListener downloadManagerListener; + private FakeDownloaderFactory downloaderFactory; private DownloadManager downloadManager; @Before @@ -73,6 +72,7 @@ public class DownloadManagerTest { uri3 = Uri.parse("http://abc.com/media3"); dummyMainThread = new DummyMainThread(); actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + downloaderFactory = new FakeDownloaderFactory(); setUpDownloadManager(100); } @@ -85,24 +85,24 @@ public class DownloadManagerTest { @Test public void testDownloadActionRuns() throws Throwable { - doTestActionRuns(createDownloadAction(uri1)); + doTestDownloaderRuns(createDownloadRunner(uri1)); } @Test public void testRemoveActionRuns() throws Throwable { - doTestActionRuns(createRemoveAction(uri1)); + doTestDownloaderRuns(createRemoveRunner(uri1)); } @Test public void testDownloadRetriesThenFails() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction(uri1); - downloadAction.post(); - FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + DownloadRunner downloadRunner = createDownloadRunner(uri1); + downloadRunner.postAction(); + FakeDownloader fakeDownloader = downloadRunner.downloader; fakeDownloader.enableDownloadIOException = true; for (int i = 0; i <= MIN_RETRY_COUNT; i++) { fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock(); } - downloadAction.assertFailed(); + downloadRunner.assertFailed(); downloadManagerListener.clearDownloadError(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); @@ -110,23 +110,23 @@ public class DownloadManagerTest { @Test public void testDownloadNoRetryWhenCanceled() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); - downloadAction.getFakeDownloader().enableDownloadIOException = true; - downloadAction.post().assertStarted(); + DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts(); + downloadRunner.downloader.enableDownloadIOException = true; + downloadRunner.postAction().assertStarted(); - FakeDownloadAction removeAction = createRemoveAction(uri1).post(); + DownloadRunner removeRunner = createRemoveRunner(uri1).postAction(); - downloadAction.unblock().assertCanceled(); - removeAction.unblock(); + downloadRunner.unblock().assertCanceled(); + removeRunner.unblock(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDownloadRetriesThenContinues() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction(uri1); - downloadAction.post(); - FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + DownloadRunner downloadRunner = createDownloadRunner(uri1); + downloadRunner.postAction(); + FakeDownloader fakeDownloader = downloadRunner.downloader; fakeDownloader.enableDownloadIOException = true; for (int i = 0; i <= MIN_RETRY_COUNT; i++) { fakeDownloader.assertStarted(MAX_RETRY_DELAY); @@ -135,7 +135,7 @@ public class DownloadManagerTest { } fakeDownloader.unblock(); } - downloadAction.assertCompleted(); + downloadRunner.assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @@ -143,9 +143,9 @@ public class DownloadManagerTest { @Test @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public void testDownloadRetryCountResetsOnProgress() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction(uri1); - downloadAction.post(); - FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + DownloadRunner downloadRunner = createDownloadRunner(uri1); + downloadRunner.postAction(); + FakeDownloader fakeDownloader = downloadRunner.downloader; fakeDownloader.enableDownloadIOException = true; fakeDownloader.downloadedBytes = 0; for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) { @@ -156,61 +156,61 @@ public class DownloadManagerTest { } fakeDownloader.unblock(); } - downloadAction.assertCompleted(); + downloadRunner.assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri2)); + doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri2)); } @Test public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction(uri1), createRemoveAction(uri2)); + doTestDownloadersRunInParallel(createDownloadRunner(uri1), createRemoveRunner(uri2)); } @Test public void testSameMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction(uri1), createDownloadAction(uri1)); + doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri1)); } @Test public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { - doTestActionsRunSequentially(createDownloadAction(uri1), createRemoveAction(uri1)); + doTestDownloadersRunSequentially(createDownloadRunner(uri1), createRemoveRunner(uri1)); } @Test public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction(uri1), createDownloadAction(uri1)); + doTestDownloadersRunSequentially(createRemoveRunner(uri1), createDownloadRunner(uri1)); } @Test public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction(uri1), createRemoveAction(uri1)); + doTestDownloadersRunSequentially(createRemoveRunner(uri1), createRemoveRunner(uri1)); } @Test public void testSameMediaMultipleActions() throws Throwable { - FakeDownloadAction downloadAction1 = createDownloadAction(uri1).ignoreInterrupts(); - FakeDownloadAction downloadAction2 = createDownloadAction(uri1).ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction(uri1); - FakeDownloadAction downloadAction3 = createDownloadAction(uri1); - FakeDownloadAction removeAction2 = createRemoveAction(uri1); + DownloadRunner downloadAction1 = createDownloadRunner(uri1).ignoreInterrupts(); + DownloadRunner downloadAction2 = createDownloadRunner(uri1).ignoreInterrupts(); + DownloadRunner removeAction1 = createRemoveRunner(uri1); + DownloadRunner downloadAction3 = createDownloadRunner(uri1); + DownloadRunner removeAction2 = createRemoveRunner(uri1); // Two download actions run in parallel. - downloadAction1.post().assertStarted(); - downloadAction2.post().assertStarted(); + downloadAction1.postAction().assertStarted(); + downloadAction2.postAction().assertStarted(); // removeAction1 is added. It interrupts the two download actions' threads but they are // configured to ignore it so removeAction1 doesn't start. - removeAction1.post().assertDoesNotStart(); + removeAction1.postAction().assertDoesNotStart(); // downloadAction2 finishes but it isn't enough to start removeAction1. downloadAction2.unblock().assertCanceled(); removeAction1.assertDoesNotStart(); - // downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. - downloadAction3.post().assertDoesNotStart(); + // downloadAction3 is postAction to DownloadManager but it waits for removeAction1 to finish. + downloadAction3.postAction().assertDoesNotStart(); // When downloadAction1 finishes, removeAction1 starts. downloadAction1.unblock().assertCanceled(); @@ -220,7 +220,7 @@ public class DownloadManagerTest { // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 // starts immediately. - removeAction2.post(); + removeAction2.postAction(); removeAction1.assertCanceled(); downloadAction3.assertCanceled(); removeAction2.assertStarted().unblock().assertCompleted(); @@ -229,13 +229,13 @@ public class DownloadManagerTest { @Test public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { - FakeDownloadAction removeAction1 = createRemoveAction(uri1).ignoreInterrupts(); - FakeDownloadAction removeAction2 = createRemoveAction(uri1); - FakeDownloadAction removeAction3 = createRemoveAction(uri1); + DownloadRunner removeAction1 = createRemoveRunner(uri1).ignoreInterrupts(); + DownloadRunner removeAction2 = createRemoveRunner(uri1); + DownloadRunner removeAction3 = createRemoveRunner(uri1); - removeAction1.post().assertStarted(); - removeAction2.post().assertDoesNotStart(); - removeAction3.post().assertDoesNotStart(); + removeAction1.postAction().assertStarted(); + removeAction2.postAction().assertDoesNotStart(); + removeAction3.postAction().assertDoesNotStart(); removeAction2.assertCanceled(); @@ -247,30 +247,30 @@ public class DownloadManagerTest { @Test public void testGetTasks() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction(uri1); - FakeDownloadAction downloadAction1 = createDownloadAction(uri1); - FakeDownloadAction downloadAction2 = createDownloadAction(uri1); + DownloadRunner removeAction = createRemoveRunner(uri1); + DownloadRunner downloadAction1 = createDownloadRunner(uri1); + DownloadRunner downloadAction2 = createDownloadRunner(uri1); - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); + removeAction.postAction().assertStarted(); + downloadAction1.postAction().assertDoesNotStart(); + downloadAction2.postAction().assertDoesNotStart(); TaskState[] states = downloadManager.getAllTaskStates(); assertThat(states).hasLength(3); - assertThat(states[0].action).isEqualTo(removeAction); - assertThat(states[1].action).isEqualTo(downloadAction1); - assertThat(states[2].action).isEqualTo(downloadAction2); + assertThat(states[0].action).isEqualTo(removeAction.action); + assertThat(states[1].action).isEqualTo(downloadAction1.action); + assertThat(states[2].action).isEqualTo(downloadAction2.action); } @Test public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction(uri1); - FakeDownloadAction downloadAction1 = createDownloadAction(uri1); - FakeDownloadAction downloadAction2 = createDownloadAction(uri1); + DownloadRunner removeAction = createRemoveRunner(uri1); + DownloadRunner downloadAction1 = createDownloadRunner(uri1); + DownloadRunner downloadAction2 = createDownloadRunner(uri1); - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); + removeAction.postAction().assertStarted(); + downloadAction1.postAction().assertDoesNotStart(); + downloadAction2.postAction().assertDoesNotStart(); removeAction.unblock().assertCompleted(); downloadAction1.assertStarted(); @@ -283,75 +283,75 @@ public class DownloadManagerTest { @Test public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction(uri1).ignoreInterrupts(); - FakeDownloadAction downloadAction1 = createDownloadAction(uri1); - FakeDownloadAction downloadAction2 = createDownloadAction(uri2); + DownloadRunner removeRunner = createRemoveRunner(uri1).ignoreInterrupts(); + DownloadRunner downloadRunner1 = createDownloadRunner(uri1); + DownloadRunner downloadRunner2 = createDownloadRunner(uri2); - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); + removeRunner.postAction().assertStarted(); + downloadRunner1.postAction().assertDoesNotStart(); + downloadRunner2.postAction().assertDoesNotStart(); - removeAction.unblock().assertCompleted(); - downloadAction1.assertStarted(); - downloadAction2.assertStarted(); - downloadAction1.unblock().assertCompleted(); - downloadAction2.unblock().assertCompleted(); + removeRunner.unblock().assertCompleted(); + downloadRunner1.assertStarted(); + downloadRunner2.assertStarted(); + downloadRunner1.unblock().assertCompleted(); + downloadRunner2.unblock().assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction(uri1).ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction(uri1); - FakeDownloadAction removeAction2 = createRemoveAction(uri2); + DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts(); + DownloadRunner removeRunner1 = createRemoveRunner(uri1); + DownloadRunner removeRunner2 = createRemoveRunner(uri2); - downloadAction.post().assertStarted(); - removeAction1.post().assertDoesNotStart(); - removeAction2.post().assertStarted(); + downloadRunner.postAction().assertStarted(); + removeRunner1.postAction().assertDoesNotStart(); + removeRunner2.postAction().assertStarted(); - downloadAction.unblock().assertCanceled(); - removeAction2.unblock().assertCompleted(); + downloadRunner.unblock().assertCanceled(); + removeRunner2.unblock().assertCompleted(); - removeAction1.assertStarted(); - removeAction1.unblock().assertCompleted(); + removeRunner1.assertStarted(); + removeRunner1.unblock().assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @Test public void testStopAndResume() throws Throwable { - FakeDownloadAction download1Action = createDownloadAction(uri1); - FakeDownloadAction remove2Action = createRemoveAction(uri2); - FakeDownloadAction download2Action = createDownloadAction(uri2); - FakeDownloadAction remove1Action = createRemoveAction(uri1); - FakeDownloadAction download3Action = createDownloadAction(uri3); + DownloadRunner download1Runner = createDownloadRunner(uri1); + DownloadRunner remove2Runner = createRemoveRunner(uri2); + DownloadRunner download2Runner = createDownloadRunner(uri2); + DownloadRunner remove1Runner = createRemoveRunner(uri1); + DownloadRunner download3Runner = createDownloadRunner(uri3); - download1Action.post().assertStarted(); - remove2Action.post().assertStarted(); - download2Action.post().assertDoesNotStart(); + download1Runner.postAction().assertStarted(); + remove2Runner.postAction().assertStarted(); + download2Runner.postAction().assertDoesNotStart(); runOnMainThread(() -> downloadManager.stopDownloads()); - download1Action.assertStopped(); + download1Runner.assertStopped(); // remove actions aren't stopped. - remove2Action.unblock().assertCompleted(); - // Although remove2Action is finished, download2Action doesn't start. - download2Action.assertDoesNotStart(); + remove2Runner.unblock().assertCompleted(); + // Although remove2 is finished, download2 doesn't start. + download2Runner.assertDoesNotStart(); // When a new remove action is added, it cancels stopped download actions with the same media. - remove1Action.post(); - download1Action.assertCanceled(); - remove1Action.assertStarted().unblock().assertCompleted(); + remove1Runner.postAction(); + download1Runner.assertCanceled(); + remove1Runner.assertStarted().unblock().assertCompleted(); // New download actions can be added but they don't start. - download3Action.post().assertDoesNotStart(); + download3Runner.postAction().assertDoesNotStart(); runOnMainThread(() -> downloadManager.startDownloads()); - download2Action.assertStarted().unblock().assertCompleted(); - download3Action.assertStarted().unblock().assertCompleted(); + download2Runner.assertStarted().unblock().assertCompleted(); + download3Runner.assertStarted().unblock().assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @@ -359,34 +359,34 @@ public class DownloadManagerTest { @Test public void testResumeBeforeTotallyStopped() throws Throwable { setUpDownloadManager(2); - FakeDownloadAction download1Action = createDownloadAction(uri1).ignoreInterrupts(); - FakeDownloadAction download2Action = createDownloadAction(uri2); - FakeDownloadAction download3Action = createDownloadAction(uri3); + DownloadRunner download1Runner = createDownloadRunner(uri1).ignoreInterrupts(); + DownloadRunner download2Runner = createDownloadRunner(uri2); + DownloadRunner download3Runner = createDownloadRunner(uri3); - download1Action.post().assertStarted(); - download2Action.post().assertStarted(); - // download3Action doesn't start as DM was configured to run two downloads in parallel. - download3Action.post().assertDoesNotStart(); + download1Runner.postAction().assertStarted(); + download2Runner.postAction().assertStarted(); + // download3 doesn't start as DM was configured to run two downloads in parallel. + download3Runner.postAction().assertDoesNotStart(); runOnMainThread(() -> downloadManager.stopDownloads()); - // download1Action doesn't stop yet as it ignores interrupts. - download2Action.assertStopped(); + // download1 doesn't stop yet as it ignores interrupts. + download2Runner.assertStopped(); runOnMainThread(() -> downloadManager.startDownloads()); - // download2Action starts immediately. - download2Action.assertStarted(); + // download2 starts immediately. + download2Runner.assertStarted(); - // download3Action doesn't start as download1Action still holds its slot. - download3Action.assertDoesNotStart(); + // download3 doesn't start as download1 still holds its slot. + download3Runner.assertDoesNotStart(); - // when unblocked download1Action stops and starts immediately. - download1Action.unblock().assertStopped().assertStarted(); + // when unblocked download1 stops and starts immediately. + download1Runner.unblock().assertStopped().assertStarted(); - download1Action.unblock(); - download2Action.unblock(); - download3Action.unblock(); + download1Runner.unblock(); + download2Runner.unblock(); + download3Runner.unblock(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } @@ -400,12 +400,7 @@ public class DownloadManagerTest { () -> { downloadManager = new DownloadManager( - new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY), - maxActiveDownloadTasks, - MIN_RETRY_COUNT, - actionFile, - ProgressiveDownloadAction.DESERIALIZER); + actionFile, downloaderFactory, maxActiveDownloadTasks, MIN_RETRY_COUNT); downloadManagerListener = new TestDownloadManagerListener(downloadManager, dummyMainThread); downloadManager.addListener(downloadManagerListener); @@ -424,104 +419,104 @@ public class DownloadManagerTest { } } - private void doTestActionRuns(FakeDownloadAction action) throws Throwable { - action.post().assertStarted().unblock().assertCompleted(); + private void doTestDownloaderRuns(DownloadRunner runner) throws Throwable { + runner.postAction().assertStarted().unblock().assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2) + private void doTestDownloadersRunSequentially(DownloadRunner runner1, DownloadRunner runner2) throws Throwable { - action1.ignoreInterrupts().post().assertStarted(); - action2.post().assertDoesNotStart(); + runner1.ignoreInterrupts().postAction().assertStarted(); + runner2.postAction().assertDoesNotStart(); - action1.unblock(); - action2.assertStarted(); + runner1.unblock(); + runner2.assertStarted(); - action2.unblock().assertCompleted(); + runner2.unblock().assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2) + private void doTestDownloadersRunInParallel(DownloadRunner runner1, DownloadRunner runner2) throws Throwable { - action1.post().assertStarted(); - action2.post().assertStarted(); - action1.unblock().assertCompleted(); - action2.unblock().assertCompleted(); + runner1.postAction().assertStarted(); + runner2.postAction().assertStarted(); + runner1.unblock().assertCompleted(); + runner2.unblock().assertCompleted(); downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private FakeDownloadAction createDownloadAction(Uri uri) { - return new FakeDownloadAction(uri, /* isRemoveAction= */ false); + private DownloadRunner createDownloadRunner(Uri uri) { + return new DownloadRunner(uri, /* isRemoveAction= */ false); } - private FakeDownloadAction createRemoveAction(Uri uri) { - return new FakeDownloadAction(uri, /* isRemoveAction= */ true); + private DownloadRunner createRemoveRunner(Uri uri) { + return new DownloadRunner(uri, /* isRemoveAction= */ true); } private void runOnMainThread(final Runnable r) { dummyMainThread.runOnMainThread(r); } - private class FakeDownloadAction extends DownloadAction { + private class DownloadRunner { - private final FakeDownloader downloader; + public final DownloadAction action; + public final FakeDownloader downloader; - private FakeDownloadAction(Uri uri, boolean isRemoveAction) { - super("Fake", /* version= */ 0, uri, isRemoveAction, /* data= */ null); - this.downloader = new FakeDownloader(isRemoveAction); + private DownloadRunner(Uri uri, boolean isRemoveAction) { + action = + isRemoveAction + ? DownloadAction.createRemoveAction( + DownloadAction.TYPE_PROGRESSIVE, + uri, + /* customCacheKey= */ null, + /* data= */ null) + : DownloadAction.createDownloadAction( + DownloadAction.TYPE_PROGRESSIVE, + uri, + /* keys= */ Collections.emptyList(), + /* customCacheKey= */ null, + /* data= */ null); + downloader = new FakeDownloader(isRemoveAction); + downloaderFactory.putFakeDownloader(action, downloader); } - @Override - protected void writeToStream(DataOutputStream output) { - // do nothing. - } - - @Override - public Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { - return downloader; - } - - private FakeDownloader getFakeDownloader() { - return downloader; - } - - private FakeDownloadAction post() { - runOnMainThread(() -> downloadManager.handleAction(FakeDownloadAction.this)); + private DownloadRunner postAction() { + runOnMainThread(() -> downloadManager.handleAction(action)); return this; } - private FakeDownloadAction assertDoesNotStart() throws InterruptedException { + private DownloadRunner assertDoesNotStart() throws InterruptedException { Thread.sleep(ASSERT_FALSE_TIME); assertThat(downloader.started.getCount()).isEqualTo(1); return this; } - private FakeDownloadAction assertStarted() throws InterruptedException { + private DownloadRunner assertStarted() throws InterruptedException { downloader.assertStarted(ASSERT_TRUE_TIMEOUT); return assertState(TaskState.STATE_STARTED); } - private FakeDownloadAction assertCompleted() { + private DownloadRunner assertCompleted() { return assertState(TaskState.STATE_COMPLETED); } - private FakeDownloadAction assertFailed() { + private DownloadRunner assertFailed() { return assertState(TaskState.STATE_FAILED); } - private FakeDownloadAction assertCanceled() { + private DownloadRunner assertCanceled() { return assertState(TaskState.STATE_CANCELED); } - private FakeDownloadAction assertStopped() { + private DownloadRunner assertStopped() { return assertState(TaskState.STATE_QUEUED); } - private FakeDownloadAction assertState(@State int expectedState) { + private DownloadRunner assertState(@State int expectedState) { while (true) { Integer state = null; try { - state = downloadManagerListener.pollStateChange(this, ASSERT_TRUE_TIMEOUT); + state = downloadManagerListener.pollStateChange(action, ASSERT_TRUE_TIMEOUT); } catch (InterruptedException e) { fail(e.getMessage()); } @@ -531,36 +526,54 @@ public class DownloadManagerTest { } } - private FakeDownloadAction unblock() { + private DownloadRunner unblock() { downloader.unblock(); return this; } - private FakeDownloadAction ignoreInterrupts() { + private DownloadRunner ignoreInterrupts() { downloader.ignoreInterrupts = true; return this; } } + private static class FakeDownloaderFactory implements DownloaderFactory { + + public IdentityHashMap downloaders; + + public FakeDownloaderFactory() { + downloaders = new IdentityHashMap<>(); + } + + public void putFakeDownloader(DownloadAction action, FakeDownloader downloader) { + downloaders.put(action, downloader); + } + + @Override + public Downloader createDownloader(DownloadAction action) { + return downloaders.get(action); + } + } + private static class FakeDownloader implements Downloader { private final com.google.android.exoplayer2.util.ConditionVariable blocker; - private final boolean isRemoveAction; + private final boolean isRemove; private CountDownLatch started; private boolean ignoreInterrupts; private volatile boolean enableDownloadIOException; private volatile int downloadedBytes = C.LENGTH_UNSET; - private FakeDownloader(boolean isRemoveAction) { - this.isRemoveAction = isRemoveAction; + private FakeDownloader(boolean isRemove) { + this.isRemove = isRemove; this.started = new CountDownLatch(1); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); } @Override public void download() throws InterruptedException, IOException { - assertThat(isRemoveAction).isFalse(); + assertThat(isRemove).isFalse(); started.countDown(); block(); if (enableDownloadIOException) { @@ -575,7 +588,7 @@ public class DownloadManagerTest { @Override public void remove() throws InterruptedException { - assertThat(isRemoveAction).isTrue(); + assertThat(isRemove).isTrue(); started.countDown(); block(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index df5e7dd044..8f7bb51fd8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -18,21 +18,19 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** Unit tests for {@link ProgressiveDownloadAction}. */ +// TODO: Merge into DownloadActionTest +/** Unit tests for progressive {@link DownloadAction}s. */ @RunWith(RobolectricTestRunner.class) public class ProgressiveDownloadActionTest { @@ -57,15 +55,6 @@ public class ProgressiveDownloadActionTest { assertThat(action2.isRemoveAction).isTrue(); } - @Test - public void testCreateDownloader() throws Exception { - MockitoAnnotations.initMocks(this); - DownloadAction action = createDownloadAction(uri1, null); - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertThat(action.createDownloader(constructorHelper)).isNotNull(); - } - @Test public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { DownloadAction action1 = createRemoveAction(uri1, null); @@ -139,6 +128,13 @@ public class ProgressiveDownloadActionTest { doTestSerializationRoundTrip(createRemoveAction(uri2, "key")); } + @Test + public void testSerializerVersion0() throws Exception { + doTestLegacySerializationRoundTrip(createDownloadAction(uri1, "key")); + doTestLegacySerializationRoundTrip(createRemoveAction(uri1, "key")); + doTestLegacySerializationRoundTrip(createDownloadAction(uri2, "key")); + } + private void assertSameMedia(DownloadAction action1, DownloadAction action2) { assertThat(action1.isSameMedia(action2)).isTrue(); assertThat(action2.isSameMedia(action1)).isTrue(); @@ -149,25 +145,61 @@ public class ProgressiveDownloadActionTest { assertThat(action2.isSameMedia(action1)).isFalse(); } + private static void assertEqual(DownloadAction action1, DownloadAction action2) { + assertThat(action1).isEqualTo(action2); + assertThat(action2).isEqualTo(action1); + } + private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - DownloadAction.serializeToStream(action, output); + action.serializeToStream(output); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); - DownloadAction action2 = - DownloadAction.deserializeFromStream( - new DownloadAction.Deserializer[] {ProgressiveDownloadAction.DESERIALIZER}, input); + DownloadAction action2 = DownloadAction.deserializeFromStream(input); assertThat(action2).isEqualTo(action); } + private static void doTestLegacySerializationRoundTrip(DownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(action.type); + dataOutputStream.writeInt(/* version= */ 0); + dataOutputStream.writeUTF(action.uri.toString()); + dataOutputStream.writeBoolean(action.isRemoveAction); + dataOutputStream.writeInt(action.data.length); + dataOutputStream.write(action.data); + boolean customCacheKeySet = action.customCacheKey != null; + output.writeBoolean(customCacheKeySet); + if (customCacheKeySet) { + output.writeUTF(action.customCacheKey); + } + dataOutputStream.flush(); + + assertEqual(action, deserializeActionFromStream(out)); + } + + private static DownloadAction deserializeActionFromStream(ByteArrayOutputStream out) + throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + return DownloadAction.deserializeFromStream(input); + } + private static DownloadAction createDownloadAction(Uri uri1, String customCacheKey) { - return ProgressiveDownloadAction.createDownloadAction(uri1, null, customCacheKey); + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_PROGRESSIVE, + uri1, + /* keys= */ Collections.emptyList(), + customCacheKey, + /* data= */ null); } private static DownloadAction createRemoveAction(Uri uri1, String customCacheKey) { - return ProgressiveDownloadAction.createRemoveAction(uri1, null, customCacheKey); + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_PROGRESSIVE, uri1, customCacheKey, /* data= */ null); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java deleted file mode 100644 index f36a018e5b..0000000000 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.dash.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.offline.StreamKey; -import java.util.Collections; -import java.util.List; - -/** An action to download or remove downloaded DASH streams. */ -public final class DashDownloadAction extends SegmentDownloadAction { - - private static final String TYPE = "dash"; - private static final int VERSION = 0; - - public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { - @Override - protected DownloadAction createDownloadAction( - Uri uri, boolean isRemoveAction, byte[] data, List 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 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 keys) { - super(TYPE, VERSION, uri, isRemoveAction, data, keys); - } - - @Override - public DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - return new DashDownloader(uri, keys, constructorHelper); - } - -} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index 91e41b9ded..5b3ca3664b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; @@ -86,13 +87,15 @@ public final class DashDownloadHelper extends DownloadHelper { } @Override - public DashDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return DashDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys)); + public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data); } @Override - public DashDownloadAction getRemoveAction(@Nullable byte[] data) { - return DashDownloadAction.createRemoveAction(uri, data); + public DownloadAction getRemoveAction(@Nullable byte[] data) { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data); } private static List toStreamKeys(List trackKeys) { diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java index 0ebf6bb628..f3f6c8aa5c 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java @@ -19,10 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -33,11 +30,10 @@ import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** Unit tests for {@link DashDownloadAction}. */ +// TODO: Merge into DownloadActionTest +/** Unit tests for DASH {@link DownloadAction}s. */ @RunWith(RobolectricTestRunner.class) public class DashDownloadActionTest { @@ -62,15 +58,6 @@ public class DashDownloadActionTest { assertThat(action2.isRemoveAction).isTrue(); } - @Test - public void testCreateDownloader() { - MockitoAnnotations.initMocks(this); - DownloadAction action = createDownloadAction(uri1); - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertThat(action.createDownloader(constructorHelper)).isNotNull(); - } - @Test public void testSameUriDifferentAction_IsSameMedia() { DownloadAction action1 = createRemoveAction(uri1); @@ -141,6 +128,14 @@ public class DashDownloadActionTest { createDownloadAction(uri2, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1))); } + @Test + public void testSerializerVersion0() throws Exception { + doTestLegacySerializationRoundTrip(createDownloadAction(uri1)); + doTestLegacySerializationRoundTrip(createRemoveAction(uri1)); + doTestLegacySerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0, 0), new StreamKey(1, 1, 1))); + } + private static void assertNotEqual(DownloadAction action1, DownloadAction action2) { assertThat(action1).isNotEqualTo(action2); assertThat(action2).isNotEqualTo(action1); @@ -154,24 +149,53 @@ public class DashDownloadActionTest { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - DownloadAction.serializeToStream(action, output); + action.serializeToStream(output); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); - DownloadAction action2 = - DownloadAction.deserializeFromStream( - new DownloadAction.Deserializer[] {DashDownloadAction.DESERIALIZER}, input); + DownloadAction action2 = DownloadAction.deserializeFromStream(input); assertThat(action).isEqualTo(action2); } + private static void doTestLegacySerializationRoundTrip(DownloadAction action) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(action.type); + dataOutputStream.writeInt(/* version= */ 0); + dataOutputStream.writeUTF(action.uri.toString()); + dataOutputStream.writeBoolean(action.isRemoveAction); + dataOutputStream.writeInt(action.data.length); + dataOutputStream.write(action.data); + dataOutputStream.writeInt(action.keys.size()); + for (int i = 0; i < action.keys.size(); i++) { + StreamKey key = action.keys.get(i); + dataOutputStream.writeInt(key.periodIndex); + dataOutputStream.writeInt(key.groupIndex); + dataOutputStream.writeInt(key.trackIndex); + } + dataOutputStream.flush(); + + assertEqual(action, deserializeActionFromStream(out)); + } + + private static DownloadAction deserializeActionFromStream(ByteArrayOutputStream out) + throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + return DownloadAction.deserializeFromStream(input); + } + private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) { ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - return DashDownloadAction.createDownloadAction(uri, null, keysList); + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, /* data= */ null); } private static DownloadAction createRemoveAction(Uri uri) { - return DashDownloadAction.createRemoveAction(uri, null); + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, /* data= */ null); } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index a597d780e0..76699217e3 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -25,14 +25,21 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.net.Uri; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadException; +import com.google.android.exoplayer2.offline.Downloader; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; @@ -44,6 +51,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @@ -67,6 +75,23 @@ public class DashDownloaderTest { Util.recursiveDelete(tempFolder); } + @Test + public void testCreateWithDefaultDownloaderFactory() throws Exception { + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + + Downloader downloader = + factory.createDownloader( + DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, + Uri.parse("https://www.test.com/download"), + Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)), + /* customCacheKey= */ null, + /* data= */ null)); + assertThat(downloader).isInstanceOf(DashDownloader.class); + } + @Test public void testDownloadRepresentation() throws Exception { FakeDataSet fakeDataSet = diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 88d4ed6a9d..f0cab7532f 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -25,6 +25,7 @@ import android.content.Context; import android.net.Uri; import android.os.ConditionVariable; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; @@ -238,11 +239,11 @@ public class DownloadManagerDashTest { Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); downloadManager = new DownloadManager( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory), - /* maxSimultaneousDownloads= */ 1, - /* minRetryCount= */ 3, actionFile, - DashDownloadAction.DESERIALIZER); + new DefaultDownloaderFactory( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), + /* maxSimultaneousDownloads= */ 1, + /* minRetryCount= */ 3); downloadManagerListener = new TestDownloadManagerListener(downloadManager, dummyMainThread); @@ -257,9 +258,13 @@ public class DownloadManagerDashTest { Collections.addAll(keysList, keys); DownloadAction result; if (isRemoveAction) { - result = DashDownloadAction.createRemoveAction(uri, data); + result = + DownloadAction.createRemoveAction( + DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data); } else { - result = DashDownloadAction.createDownloadAction(uri, data, keysList); + result = + DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, data); } return result; } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 70a64f5524..2466a42049 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; @@ -116,11 +117,11 @@ public class DownloadServiceDashTest { actionFile.delete(); final DownloadManager dashDownloadManager = new DownloadManager( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory), - 1, - 3, actionFile, - DashDownloadAction.DESERIALIZER); + new DefaultDownloaderFactory( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), + /* maxSimultaneousDownloads= */ 1, + /* minRetryCount= */ 3); downloadManagerListener = new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); dashDownloadManager.addListener(downloadManagerListener); @@ -211,9 +212,13 @@ public class DownloadServiceDashTest { Collections.addAll(keysList, keys); DownloadAction result; if (isRemoveAction) { - result = DashDownloadAction.createRemoveAction(uri, data); + result = + DownloadAction.createRemoveAction( + DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null, data); } else { - result = DashDownloadAction.createDownloadAction(uri, data, keysList); + result = + DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, uri, keysList, /* customCacheKey= */ null, data); } return result; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java deleted file mode 100644 index c54a9a7dd3..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.hls.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.offline.StreamKey; -import java.io.DataInputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -/** An action to download or remove downloaded HLS streams. */ -public final class HlsDownloadAction extends SegmentDownloadAction { - - private static final String TYPE = "hls"; - private static final int VERSION = 1; - - public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { - - @Override - protected StreamKey readKey(int version, DataInputStream input) throws IOException { - if (version > 0) { - return super.readKey(version, input); - } - int renditionGroup = input.readInt(); - int trackIndex = input.readInt(); - return new StreamKey(renditionGroup, trackIndex); - } - - @Override - protected DownloadAction createDownloadAction( - Uri uri, boolean isRemoveAction, byte[] data, List 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 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 keys) { - super(TYPE, VERSION, uri, isRemoveAction, data, keys); - } - - @Override - public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - return new HlsDownloader(uri, keys, constructorHelper); - } - -} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index fcbe06993e..1aae136231 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; @@ -97,15 +98,20 @@ public final class HlsDownloadHelper extends DownloadHelper { } @Override - public HlsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { Assertions.checkNotNull(renditionGroups); - return HlsDownloadAction.createDownloadAction( - uri, data, toStreamKeys(trackKeys, renditionGroups)); + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_HLS, + uri, + toStreamKeys(trackKeys, renditionGroups), + /* customCacheKey= */ null, + data); } @Override - public HlsDownloadAction getRemoveAction(@Nullable byte[] data) { - return HlsDownloadAction.createRemoveAction(uri, data); + public DownloadAction getRemoveAction(@Nullable byte[] data) { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null, data); } private static Format[] toFormats(List hlsUrls) { diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java index 778ecadddd..5e2870bc3a 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadActionTest.java @@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -33,11 +31,10 @@ import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** Unit tests for {@link HlsDownloadAction}. */ +// TODO: Merge into DownloadActionTest +/** Unit tests for HLS {@link DownloadAction}s. */ @RunWith(RobolectricTestRunner.class) public class HlsDownloadActionTest { @@ -62,15 +59,6 @@ public class HlsDownloadActionTest { assertThat(action2.isRemoveAction).isTrue(); } - @Test - public void testCreateDownloader() { - MockitoAnnotations.initMocks(this); - DownloadAction action = createDownloadAction(uri1); - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertThat(action.createDownloader(constructorHelper)).isNotNull(); - } - @Test public void testSameUriDifferentAction_IsSameMedia() { DownloadAction action1 = createRemoveAction(uri1); @@ -140,10 +128,18 @@ public class HlsDownloadActionTest { @Test public void testSerializerVersion0() throws Exception { - doTestSerializationV0RoundTrip(createDownloadAction(uri1)); - doTestSerializationV0RoundTrip(createRemoveAction(uri1)); - doTestSerializationV0RoundTrip( - createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); + doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 0); + doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 0); + doTestLegacySerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 0); + } + + @Test + public void testSerializerVersion1() throws Exception { + doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 1); + doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 1); + doTestLegacySerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 1); } private static void assertNotEqual(DownloadAction action1, DownloadAction action2) { @@ -159,17 +155,19 @@ public class HlsDownloadActionTest { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - DownloadAction.serializeToStream(action, output); + action.serializeToStream(output); assertEqual(action, deserializeActionFromStream(out)); } - private static void doTestSerializationV0RoundTrip(HlsDownloadAction action) throws IOException { + private static void doTestLegacySerializationRoundTrip(DownloadAction action, int version) + throws IOException { + Assertions.checkState(version == 0 || version == 1); ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); DataOutputStream dataOutputStream = new DataOutputStream(output); dataOutputStream.writeUTF(action.type); - dataOutputStream.writeInt(/* version */ 0); + dataOutputStream.writeInt(version); dataOutputStream.writeUTF(action.uri.toString()); dataOutputStream.writeBoolean(action.isRemoveAction); dataOutputStream.writeInt(action.data.length); @@ -177,6 +175,9 @@ public class HlsDownloadActionTest { dataOutputStream.writeInt(action.keys.size()); for (int i = 0; i < action.keys.size(); i++) { StreamKey key = action.keys.get(i); + if (version == 1) { + dataOutputStream.writeInt(key.periodIndex); + } dataOutputStream.writeInt(key.groupIndex); dataOutputStream.writeInt(key.trackIndex); } @@ -189,17 +190,18 @@ public class HlsDownloadActionTest { throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); - return DownloadAction.deserializeFromStream( - new DownloadAction.Deserializer[] {HlsDownloadAction.DESERIALIZER}, input); + return DownloadAction.deserializeFromStream(input); } - private static HlsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) { + private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) { ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - return HlsDownloadAction.createDownloadAction(uri, null, keysList); + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_HLS, uri, keysList, /* customCacheKey= */ null, /* data= */ null); } - private static HlsDownloadAction createRemoveAction(Uri uri) { - return HlsDownloadAction.createRemoveAction(uri, null); + private static DownloadAction createRemoveAction(Uri uri) { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null, /* data= */ null); } } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 825988994e..90ccbba2de 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -35,21 +35,29 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.Downloader; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @@ -84,6 +92,23 @@ public class HlsDownloaderTest { Util.recursiveDelete(tempFolder); } + @Test + public void testCreateWithDefaultDownloaderFactory() throws Exception { + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + + Downloader downloader = + factory.createDownloader( + DownloadAction.createDownloadAction( + DownloadAction.TYPE_HLS, + Uri.parse("https://www.test.com/download"), + Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)), + /* customCacheKey= */ null, + /* data= */ null)); + assertThat(downloader).isInstanceOf(HlsDownloader.class); + } + @Test public void testCounterMethods() throws Exception { HlsDownloader downloader = diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java deleted file mode 100644 index ad2196fd74..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.smoothstreaming.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.offline.StreamKey; -import java.io.DataInputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -/** An action to download or remove downloaded SmoothStreaming streams. */ -public final class SsDownloadAction extends SegmentDownloadAction { - - private static final String TYPE = "ss"; - private static final int VERSION = 1; - - public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { - - @Override - protected StreamKey readKey(int version, DataInputStream input) throws IOException { - if (version > 0) { - return super.readKey(version, input); - } - int groupIndex = input.readInt(); - int trackIndex = input.readInt(); - return new StreamKey(groupIndex, trackIndex); - } - - @Override - protected DownloadAction createDownloadAction( - Uri uri, boolean isRemoveAction, byte[] data, List 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 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 keys) { - super(TYPE, VERSION, uri, isRemoveAction, data, keys); - } - - @Override - public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { - return new SsDownloader(uri, keys, constructorHelper); - } - -} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index 5125beff1c..815e4337bd 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.TrackKey; @@ -76,13 +77,15 @@ public final class SsDownloadHelper extends DownloadHelper { } @Override - public SsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return SsDownloadAction.createDownloadAction(uri, data, toStreamKeys(trackKeys)); + public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_SS, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data); } @Override - public SsDownloadAction getRemoveAction(@Nullable byte[] data) { - return SsDownloadAction.createRemoveAction(uri, data); + public DownloadAction getRemoveAction(@Nullable byte[] data) { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null, data); } private static List toStreamKeys(List trackKeys) { diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java index fea03902ec..a9d6e9f9c8 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadActionTest.java @@ -19,10 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -33,11 +31,10 @@ import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** Unit tests for {@link SsDownloadAction}. */ +// TODO: Merge into DownloadActionTest +/** Unit tests for SmoothStreaming {@link DownloadAction}s. */ @RunWith(RobolectricTestRunner.class) public class SsDownloadActionTest { @@ -62,15 +59,6 @@ public class SsDownloadActionTest { assertThat(action2.isRemoveAction).isTrue(); } - @Test - public void testCreateDownloader() { - MockitoAnnotations.initMocks(this); - DownloadAction action = createDownloadAction(uri1); - DownloaderConstructorHelper constructorHelper = - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertThat(action.createDownloader(constructorHelper)).isNotNull(); - } - @Test public void testSameUriDifferentAction_IsSameMedia() { DownloadAction action1 = createRemoveAction(uri1); @@ -140,10 +128,18 @@ public class SsDownloadActionTest { @Test public void testSerializerVersion0() throws Exception { - doTestSerializationV0RoundTrip(createDownloadAction(uri1)); - doTestSerializationV0RoundTrip(createRemoveAction(uri1)); - doTestSerializationV0RoundTrip( - createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1))); + doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 0); + doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 0); + doTestLegacySerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 0); + } + + @Test + public void testSerializerVersion1() throws Exception { + doTestLegacySerializationRoundTrip(createDownloadAction(uri1), /* version= */ 1); + doTestLegacySerializationRoundTrip(createRemoveAction(uri1), /* version= */ 1); + doTestLegacySerializationRoundTrip( + createDownloadAction(uri2, new StreamKey(0, 0), new StreamKey(1, 1)), /* version= */ 1); } private static void assertNotEqual(DownloadAction action1, DownloadAction action2) { @@ -159,17 +155,19 @@ public class SsDownloadActionTest { private static void doTestSerializationRoundTrip(DownloadAction action) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - DownloadAction.serializeToStream(action, output); + action.serializeToStream(output); assertEqual(action, deserializeActionFromStream(out)); } - private static void doTestSerializationV0RoundTrip(SsDownloadAction action) throws IOException { + private static void doTestLegacySerializationRoundTrip(DownloadAction action, int version) + throws IOException { + Assertions.checkState(version == 0 || version == 1); ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); DataOutputStream dataOutputStream = new DataOutputStream(output); dataOutputStream.writeUTF(action.type); - dataOutputStream.writeInt(/* version */ 0); + dataOutputStream.writeInt(version); dataOutputStream.writeUTF(action.uri.toString()); dataOutputStream.writeBoolean(action.isRemoveAction); dataOutputStream.writeInt(action.data.length); @@ -177,6 +175,9 @@ public class SsDownloadActionTest { dataOutputStream.writeInt(action.keys.size()); for (int i = 0; i < action.keys.size(); i++) { StreamKey key = action.keys.get(i); + if (version == 1) { + dataOutputStream.writeInt(key.periodIndex); + } dataOutputStream.writeInt(key.groupIndex); dataOutputStream.writeInt(key.trackIndex); } @@ -189,17 +190,18 @@ public class SsDownloadActionTest { throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); - return DownloadAction.deserializeFromStream( - new DownloadAction.Deserializer[] {SsDownloadAction.DESERIALIZER}, input); + return DownloadAction.deserializeFromStream(input); } - private static SsDownloadAction createDownloadAction(Uri uri, StreamKey... keys) { + private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) { ArrayList keysList = new ArrayList<>(); Collections.addAll(keysList, keys); - return SsDownloadAction.createDownloadAction(uri, null, keysList); + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_SS, uri, keysList, /* customCacheKey= */ null, /* data= */ null); } - private static SsDownloadAction createRemoveAction(Uri uri) { - return SsDownloadAction.createRemoveAction(uri, null); + private static DownloadAction createRemoveAction(Uri uri) { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null, /* data= */ null); } } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java new file mode 100644 index 0000000000..e47ffec64f --- /dev/null +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.smoothstreaming.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.Downloader; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.DownloaderFactory; +import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link SsDownloader}. */ +@RunWith(RobolectricTestRunner.class) +public final class SsDownloaderTest { + + @Test + public void createWithDefaultDownloaderFactory() throws Exception { + DownloaderConstructorHelper constructorHelper = + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); + DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); + + Downloader downloader = + factory.createDownloader( + DownloadAction.createDownloadAction( + DownloadAction.TYPE_SS, + Uri.parse("https://www.test.com/download"), + Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)), + /* customCacheKey= */ null, + /* data= */ null)); + assertThat(downloader).isInstanceOf(SsDownloader.class); + } +}