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 new file mode 100644 index 0000000000..38f599cc6e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -0,0 +1,95 @@ +/* + * 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 com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.AtomicFile; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Stores and loads {@link DownloadAction}s to/from a file. + */ +@ClosedSource(reason = "Not ready yet") +public final class ActionFile { + + private final AtomicFile atomicFile; + + /** + * @param actionFile File to be used to store and load {@link DownloadAction}s. + */ + public ActionFile(File actionFile) { + atomicFile = new AtomicFile(actionFile); + } + + /** + * Loads {@link DownloadAction}s from file. + * + * @param deserializers {@link Deserializer}s to deserialize DownloadActions. + * @return Loaded DownloadActions. + * @throws IOException If there is an error during loading. + */ + public DownloadAction[] load(Deserializer... deserializers) throws IOException { + InputStream inputStream = null; + try { + inputStream = atomicFile.openRead(); + DataInputStream dataInputStream = new DataInputStream(inputStream); + int version = dataInputStream.readInt(); + if (version > DownloadAction.MASTER_VERSION) { + throw new IOException("Not supported action file version: " + version); + } + int actionCount = dataInputStream.readInt(); + DownloadAction[] actions = new DownloadAction[actionCount]; + for (int i = 0; i < actionCount; i++) { + actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version); + } + return actions; + } finally { + Util.closeQuietly(inputStream); + } + } + + /** + * Stores {@link DownloadAction}s to file. + * + * @param downloadActions DownloadActions to store to file. + * @throws IOException If there is an error during storing. + */ + public void store(DownloadAction... downloadActions) throws IOException { + OutputStream outputStream = null; + try { + outputStream = atomicFile.startWrite(); + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + dataOutputStream.writeInt(DownloadAction.MASTER_VERSION); + dataOutputStream.writeInt(downloadActions.length); + for (DownloadAction action : downloadActions) { + DownloadAction.serializeToStream(action, dataOutputStream); + } + atomicFile.endWrite(outputStream); + // Avoid calling close twice. + outputStream = null; + } finally { + Util.closeQuietly(outputStream); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 5d93f413d5..24132e400c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1076,12 +1076,17 @@ public final class Util { /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */ public static File createTempDirectory(Context context, String prefix) throws IOException { - File tempFile = File.createTempFile(prefix, null, context.getCacheDir()); + File tempFile = createTempFile(context, prefix); tempFile.delete(); // Delete the temp file. tempFile.mkdir(); // Create a directory with the same name. return tempFile; } + /** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */ + public static File createTempFile(Context context, String prefix) throws IOException { + return File.createTempFile(prefix, null, context.getCacheDir()); + } + /** * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" * order. 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 new file mode 100644 index 0000000000..cc6a3e84ad --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -0,0 +1,253 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.ClosedSource; +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 org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@ClosedSource(reason = "Not ready yet") +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class ActionFileTest { + + private File tempFile; + + @Before + public void setUp() throws Exception { + tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + } + + @After + public void tearDown() throws Exception { + tempFile.delete(); + } + + @Test + public void testLoadNoDataThrowsIOException() throws Exception { + try { + loadActions(new Object[] {}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadIncompleteHeaderThrowsIOException() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadCompleteHeaderZeroAction() throws Exception { + DownloadAction[] actions = + loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/0}); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(0); + } + + @Test + public void testLoadAction() throws Exception { + DownloadAction[] actions = loadActions( + new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, /*action 1*/"type2", 321}, + new FakeDeserializer("type2")); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(1); + assertAction(actions[0], "type2", DownloadAction.MASTER_VERSION, 321); + } + + @Test + public void testLoadActions() throws Exception { + DownloadAction[] actions = loadActions( + new Object[] {DownloadAction.MASTER_VERSION, /*action count*/2, /*action 1*/"type1", 123, + /*action 2*/"type2", 321}, // Action 2 + new FakeDeserializer("type1"), new FakeDeserializer("type2")); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(2); + assertAction(actions[0], "type1", DownloadAction.MASTER_VERSION, 123); + assertAction(actions[1], "type2", DownloadAction.MASTER_VERSION, 321); + } + + @Test + public void testLoadNotSupportedVersion() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION + 1, /*action count*/1, + /*action 1*/"type2", 321}, new FakeDeserializer("type2")); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadNotSupportedType() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, + /*action 1*/"type2", 321}, new FakeDeserializer("type1")); + Assert.fail(); + } catch (DownloadException e) { + // Expected exception. + } + } + + @Test + public void testStoreAndLoadNoActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[0]); + } + + @Test + public void testStoreAndLoadActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[] { + new FakeDownloadAction("type1", DownloadAction.MASTER_VERSION, 123), + new FakeDownloadAction("type2", DownloadAction.MASTER_VERSION, 321), + }, new FakeDeserializer("type1"), new FakeDeserializer("type2")); + } + + private void doTestSerializationRoundTrip(DownloadAction[] actions, + Deserializer... deserializers) throws IOException { + ActionFile actionFile = new ActionFile(tempFile); + actionFile.store(actions); + assertThat(actionFile.load(deserializers)).isEqualTo(actions); + } + + private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) + 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); // Action count + } else if (value instanceof String) { + dataOutputStream.writeUTF((String) value); // Action count + } else { + throw new IllegalArgumentException(); + } + } + } finally { + dataOutputStream.close(); + } + return new ActionFile(tempFile).load(deserializers); + } + + private static void assertAction(DownloadAction action, String type, int version, int data) { + assertThat(action).isInstanceOf(FakeDownloadAction.class); + assertThat(action.getType()).isEqualTo(type); + assertThat(((FakeDownloadAction) action).version).isEqualTo(version); + assertThat(((FakeDownloadAction) action).data).isEqualTo(data); + } + + private static class FakeDeserializer implements Deserializer { + final String type; + + FakeDeserializer(String type) { + this.type = type; + } + + @Override + public String getType() { + return type; + } + + @Override + public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { + return new FakeDownloadAction(type, version, input.readInt()); + } + } + + private static class FakeDownloadAction extends DownloadAction { + final String type; + final int version; + final int data; + + private FakeDownloadAction(String type, int version, int data) { + this.type = type; + this.version = version; + this.data = data; + } + + @Override + protected String getType() { + return type; + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(data); + } + + @Override + protected boolean isRemoveAction() { + return false; + } + + @Override + protected boolean isSameMedia(DownloadAction other) { + return false; + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return null; + } + + // auto generated code + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FakeDownloadAction that = (FakeDownloadAction) o; + return version == that.version && data == that.data && type.equals(that.type); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + version; + result = 31 * result + data; + return result; + } + } + +} 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 44fe6c069b..85a2b02799 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 @@ -136,7 +136,7 @@ public class ProgressiveDownloadActionTest { ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); DownloadAction action2 = - ProgressiveDownloadAction.DESERIALIZER.readFromStream(action1.getVersion(), input); + ProgressiveDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); assertThat(action2).isEqualTo(action1); }