From 6ebb6124bbcc8645f8e7bae4905a6439a1b3ad33 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 20 Nov 2018 09:33:47 -0800 Subject: [PATCH] Offline refactor step 1a - Make DownloadAction concrete 1. Pull up all subclasses of DownloadAction into DownloadAction 2. Add DownloaderFactory for Downloader instantiation, and DefaultDownloaderFactory to replace the instantiation logic being removed from the DownloadAction subclasses. This change will upgrade existing action files gracefully (i.e. it does not lose compatibility with the existing offline implementation, other than some minor breaking changes to the API). TODOs: 1. Move test methods from the XDownloadActionTest classes into DownloadActionTest. This will be done in a subsequent CL. There's a lot of consolidation that can be done here, including de-duplicating some of the test code added in this CL. 2. Look at merging DownloaderConstructorHelper into DefaultDownloaderFactory. 3. Use customCacheKey in DASH/HLS/SS Downloaders, for completeness. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=222258983 --- .../exoplayer2/demo/DemoApplication.java | 7 +- .../exoplayer2/demo/DownloadTracker.java | 13 +- library/core/proguard-rules.txt | 17 +- .../exoplayer2/offline/ActionFile.java | 8 +- .../offline/DefaultDownloaderFactory.java | 120 ++++++ .../exoplayer2/offline/DownloadAction.java | 279 +++++++------ .../exoplayer2/offline/DownloadManager.java | 79 ++-- .../exoplayer2/offline/DownloaderFactory.java | 28 ++ .../offline/ProgressiveDownloadAction.java | 138 ------- .../offline/ProgressiveDownloadHelper.java | 16 +- .../offline/ProgressiveDownloader.java | 3 +- .../offline/SegmentDownloadAction.java | 138 ------- .../exoplayer2/offline/ActionFileTest.java | 160 ++------ .../offline/DefaultDownloaderFactoryTest.java | 49 +++ .../offline/DownloadManagerTest.java | 369 +++++++++--------- .../ProgressiveDownloadActionTest.java | 72 +++- .../dash/offline/DashDownloadAction.java | 84 ---- .../dash/offline/DashDownloadHelper.java | 11 +- .../dash/offline/DashDownloadActionTest.java | 66 +++- .../dash/offline/DashDownloaderTest.java | 25 ++ .../dash/offline/DownloadManagerDashTest.java | 17 +- .../dash/offline/DownloadServiceDashTest.java | 17 +- .../source/hls/offline/HlsDownloadAction.java | 97 ----- .../source/hls/offline/HlsDownloadHelper.java | 16 +- .../hls/offline/HlsDownloadActionTest.java | 58 +-- .../source/hls/offline/HlsDownloaderTest.java | 25 ++ .../offline/SsDownloadAction.java | 97 ----- .../offline/SsDownloadHelper.java | 11 +- .../offline/SsDownloadActionTest.java | 58 +-- .../offline/SsDownloaderTest.java | 55 +++ 30 files changed, 955 insertions(+), 1178 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderFactory.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java delete mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java create mode 100644 library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java 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); + } +}