diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index d46458db2b..c0d9570d7a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; - import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - // Create a temporary folder - cacheDir = File.createTempFile("CacheDataSourceTest", null); - assertTrue(cacheDir.delete()); - assertTrue(cacheDir.mkdir()); - + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); } @@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { public void testMaxCacheFileSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, false, false); - assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE), - cacheDir.listFiles().length); + File[] files = cacheDir.listFiles(); + for (File file : files) { + if (file.getName().endsWith(SimpleCacheSpan.SUFFIX)) { + assertTrue(file.length() <= MAX_CACHE_FILE_SIZE); + } + } } public void testCacheAndRead() throws Exception { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java deleted file mode 100644 index 38008c814e..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheSpanTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2016 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.upstream.cache; - -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.File; -import java.util.Random; -import junit.framework.TestCase; - -/** - * Unit tests for {@link CacheSpan}. - */ -public class CacheSpanTest extends TestCase { - - public void testCacheFile() throws Exception { - assertCacheSpan(new File("parent"), "key", 0, 0); - assertCacheSpan(new File("parent/"), "key", 1, 2); - assertCacheSpan(new File("parent"), "<>:\"/\\|?*%", 1, 2); - assertCacheSpan(new File("/"), "key", 1, 2); - - assertNullCacheSpan(new File("parent"), "", 1, 2); - assertNullCacheSpan(new File("parent"), "key", -1, 2); - assertNullCacheSpan(new File("parent"), "key", 1, -2); - - assertNotNull(CacheSpan.createCacheEntry(new File("/asd%aa.1.2.v2.exo"))); - assertNull(CacheSpan.createCacheEntry(new File("/asd%za.1.2.v2.exo"))); - - assertCacheSpan(new File("parent"), - "A newline (line feed) character \n" - + "A carriage-return character followed immediately by a newline character \r\n" - + "A standalone carriage-return character \r" - + "A next-line character \u0085" - + "A line-separator character \u2028" - + "A paragraph-separator character \u2029", 1, 2); - } - - public void testCacheFileNameRandomData() throws Exception { - Random random = new Random(0); - File parent = new File("parent"); - for (int i = 0; i < 1000; i++) { - String key = TestUtil.buildTestString(1000, random); - long offset = Math.abs(random.nextLong()); - long lastAccessTimestamp = Math.abs(random.nextLong()); - assertCacheSpan(parent, key, offset, lastAccessTimestamp); - } - } - - private void assertCacheSpan(File parent, String key, long offset, long lastAccessTimestamp) { - File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp); - CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile); - String message = cacheFile.toString(); - assertNotNull(message, cacheSpan); - assertEquals(message, parent, cacheFile.getParentFile()); - assertEquals(message, key, cacheSpan.key); - assertEquals(message, offset, cacheSpan.position); - assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp); - } - - private void assertNullCacheSpan(File parent, String key, long offset, - long lastAccessTimestamp) { - File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp); - CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile); - assertNull(cacheFile.toString(), cacheSpan); - } - -} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java new file mode 100644 index 0000000000..4666c81dfb --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -0,0 +1,171 @@ +package com.google.android.exoplayer2.upstream.cache; + +import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import android.util.SparseArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +/** + * Tests {@link CachedContentIndex}. + */ +public class CachedContentIndexTest extends InstrumentationTestCase { + + private final byte[] testIndexV1File = { + 0, 0, 0, 1, // version + 0, 0, 0, 0, // flags + 0, 0, 0, 2, // number_of_CachedContent + 0, 0, 0, 5, // cache_id + 0, 5, 65, 66, 67, 68, 69, // cache_key + 0, 0, 0, 0, 0, 0, 0, 10, // original_content_length + 0, 0, 0, 2, // cache_id + 0, 5, 75, 76, 77, 78, 79, // cache_key + 0, 0, 0, 0, 0, 0, 10, 00, // original_content_length + (byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array + }; + private CachedContentIndex index; + private File cacheDir; + + @Override + public void setUp() throws Exception { + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + index = new CachedContentIndex(cacheDir); + } + + public void testAddGetRemove() throws Exception { + final String key1 = "key1"; + final String key2 = "key2"; + final String key3 = "key3"; + + // Add two CachedContents with add methods + CachedContent cachedContent1 = new CachedContent(5, key1, 10); + index.addNew(cachedContent1); + CachedContent cachedContent2 = index.add(key2); + assertTrue(cachedContent1.id != cachedContent2.id); + + // add a span + File cacheSpanFile = SimpleCacheSpanTest + .createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30); + SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index); + assertNotNull(span); + cachedContent1.addSpan(span); + + // Check if they are added and get method returns null if the key isn't found + assertEquals(cachedContent1, index.get(key1)); + assertEquals(cachedContent2, index.get(key2)); + assertNull(index.get(key3)); + + // test getAll() + Collection cachedContents = index.getAll(); + assertEquals(2, cachedContents.size()); + assertTrue(Arrays.asList(cachedContent1, cachedContent2).containsAll(cachedContents)); + + // test getKeys() + Set keys = index.getKeys(); + assertEquals(2, keys.size()); + assertTrue(Arrays.asList(key1, key2).containsAll(keys)); + + // test getKeyForId() + assertEquals(key1, index.getKeyForId(cachedContent1.id)); + assertEquals(key2, index.getKeyForId(cachedContent2.id)); + + // test remove() + index.removeEmpty(key2); + index.removeEmpty(key3); + assertEquals(cachedContent1, index.get(key1)); + assertNull(index.get(key2)); + assertTrue(cacheSpanFile.exists()); + + // test removeEmpty() + index.addNew(cachedContent2); + index.removeEmpty(); + assertEquals(cachedContent1, index.get(key1)); + assertNull(index.get(key2)); + assertTrue(cacheSpanFile.exists()); + } + + public void testStoreAndLoad() throws Exception { + index.addNew(new CachedContent(5, "key1", 10)); + index.add("key2"); + + index.store(); + + CachedContentIndex index2 = new CachedContentIndex(cacheDir); + index2.load(); + + Set keys = index.getKeys(); + Set keys2 = index2.getKeys(); + assertEquals(keys, keys2); + for (String key : keys) { + assertEquals(index.getContentLength(key), index2.getContentLength(key)); + assertEquals(index.get(key).getSpans(), index2.get(key).getSpans()); + } + } + + public void testLoadV1() throws Exception { + FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + fos.write(testIndexV1File); + fos.close(); + + index.load(); + assertEquals(2, index.getAll().size()); + assertEquals(5, index.assignIdForKey("ABCDE")); + assertEquals(10, index.getContentLength("ABCDE")); + assertEquals(2, index.assignIdForKey("KLMNO")); + assertEquals(2560, index.getContentLength("KLMNO")); + } + + public void testStoreV1() throws Exception { + index.addNew(new CachedContent(2, "KLMNO", 2560)); + index.addNew(new CachedContent(5, "ABCDE", 10)); + + index.store(); + + byte[] buffer = new byte[testIndexV1File.length]; + FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + assertEquals(testIndexV1File.length, fos.read(buffer)); + assertEquals(-1, fos.read()); + fos.close(); + + // TODO: The order of the CachedContent stored in index file isn't defined so this test may fail + // on a different implementation of the underlying set + MoreAsserts.assertEquals(testIndexV1File, buffer); + } + + public void testAssignIdForKeyAndGetKeyForId() throws Exception { + final String key1 = "key1"; + final String key2 = "key2"; + int id1 = index.assignIdForKey(key1); + int id2 = index.assignIdForKey(key2); + assertEquals(key1, index.getKeyForId(id1)); + assertEquals(key2, index.getKeyForId(id2)); + assertTrue(id1 != id2); + assertEquals(id1, index.assignIdForKey(key1)); + assertEquals(id2, index.assignIdForKey(key2)); + } + + public void testSetGetContentLength() throws Exception { + final String key1 = "key1"; + assertEquals(C.LENGTH_UNSET, index.getContentLength(key1)); + index.setContentLength(key1, 10); + assertEquals(10, index.getContentLength(key1)); + } + + public void testGetNewId() throws Exception { + SparseArray idToKey = new SparseArray<>(); + assertEquals(0, CachedContentIndex.getNewId(idToKey)); + idToKey.put(10, ""); + assertEquals(11, CachedContentIndex.getNewId(idToKey)); + idToKey.put(Integer.MAX_VALUE, ""); + assertEquals(0, CachedContentIndex.getNewId(idToKey)); + idToKey.put(0, ""); + assertEquals(1, CachedContentIndex.getNewId(idToKey)); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java new file mode 100644 index 0000000000..6ccfc9dee9 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeSet; + +/** + * Unit tests for {@link SimpleCacheSpan}. + */ +public class SimpleCacheSpanTest extends InstrumentationTestCase { + + private CachedContentIndex index; + private File cacheDir; + + public static File createCacheSpanFile(File cacheDir, int id, long offset, int length, + long lastAccessTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + createTestFile(cacheFile, length); + return cacheFile; + } + + public static CacheSpan createCacheSpan(CachedContentIndex index, File cacheDir, String key, + long offset, int length, long lastAccessTimestamp) throws IOException { + int id = index.assignIdForKey(key); + File cacheFile = createCacheSpanFile(cacheDir, id, offset, length, lastAccessTimestamp); + return SimpleCacheSpan.createCacheEntry(cacheFile, index); + } + + @Override + protected void setUp() throws Exception { + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + index = new CachedContentIndex(cacheDir); + } + + @Override + protected void tearDown() throws Exception { + TestUtil.recursiveDelete(cacheDir); + } + + public void testCacheFile() throws Exception { + assertCacheSpan("key1", 0, 0); + assertCacheSpan("key2", 1, 2); + assertCacheSpan("<>:\"/\\|?*%", 1, 2); + assertCacheSpan("key3", 1, 2); + + assertNullCacheSpan(new File("parent"), "key4", -1, 2); + assertNullCacheSpan(new File("parent"), "key5", 1, -2); + + assertCacheSpan( + "A newline (line feed) character \n" + + "A carriage-return character followed immediately by a newline character \r\n" + + "A standalone carriage-return character \r" + + "A next-line character \u0085" + + "A line-separator character \u2028" + + "A paragraph-separator character \u2029", 1, 2); + } + + public void testUpgradeFileName() throws Exception { + String key = "asd\u00aa"; + int id = index.assignIdForKey(key); + File v3file = createTestFile(id + ".0.1.v3.exo"); + File v2file = createTestFile("asd%aa.1.2.v2.exo"); + File wrongEscapedV2file = createTestFile("asd%za.3.4.v2.exo"); + File v1File = createTestFile("asd\u00aa.5.6.v1.exo"); + + SimpleCacheSpan.upgradeOldFiles(cacheDir, index); + + assertTrue(v3file.exists()); + assertFalse(v2file.exists()); + assertTrue(wrongEscapedV2file.exists()); + assertFalse(v1File.exists()); + + File[] files = cacheDir.listFiles(); + assertEquals(4, files.length); + + Set keys = index.getKeys(); + assertEquals("There should be only one key for all files.", 1, keys.size()); + assertTrue(keys.contains(key)); + + TreeSet spans = index.get(key).getSpans(); + assertTrue("upgradeOldFiles() shouldn't add any spans.", spans.isEmpty()); + + HashMap cachedPositions = new HashMap<>(); + for (File file : files) { + SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, index); + if (cacheSpan != null) { + assertEquals(key, cacheSpan.key); + cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp); + } + } + + assertEquals(1, (long) cachedPositions.get((long) 0)); + assertEquals(2, (long) cachedPositions.get((long) 1)); + assertEquals(6, (long) cachedPositions.get((long) 5)); + } + + private static void createTestFile(File file, int length) throws IOException { + FileOutputStream output = new FileOutputStream(file); + for (int i = 0; i < length; i++) { + output.write(i); + } + output.close(); + } + + private File createTestFile(String name) throws IOException { + File file = new File(cacheDir, name); + createTestFile(file, 1); + return file; + } + + private void assertCacheSpan(String key, long offset, long lastAccessTimestamp) + throws IOException { + int id = index.assignIdForKey(key); + File cacheFile = createCacheSpanFile(cacheDir, id, offset, 1, lastAccessTimestamp); + SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index); + String message = cacheFile.toString(); + assertNotNull(message, cacheSpan); + assertEquals(message, cacheDir, cacheFile.getParentFile()); + assertEquals(message, key, cacheSpan.key); + assertEquals(message, offset, cacheSpan.position); + assertEquals(message, 1, cacheSpan.length); + assertTrue(message, cacheSpan.isCached); + assertEquals(message, cacheFile, cacheSpan.file); + assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp); + } + + private void assertNullCacheSpan(File parent, String key, long offset, + long lastAccessTimestamp) { + File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, + lastAccessTimestamp); + CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index); + assertNull(cacheFile.toString(), cacheSpan); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 2c8ea912fb..5f539c6213 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.File; @@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - // Create a temporary folder - cacheDir = File.createTempFile("SimpleCacheTest", null); - assertTrue(cacheDir.delete()); - assertTrue(cacheDir.mkdir()); + this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); } @Override @@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { } public void testCommittingOneFile() throws Exception { - SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); assertFalse(cacheSpan.isCached); @@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase { } public void testSetGetLength() throws Exception { - SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + SimpleCache simpleCache = getSimpleCache(); assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); - assertTrue(simpleCache.setContentLength(KEY_1, 15)); + simpleCache.setContentLength(KEY_1, 15); assertEquals(15, simpleCache.getContentLength(KEY_1)); simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, 0, 15); - assertTrue(simpleCache.setContentLength(KEY_1, 150)); + simpleCache.setContentLength(KEY_1, 150); assertEquals(150, simpleCache.getContentLength(KEY_1)); addCache(simpleCache, 140, 10); - // Try to set length shorter then the content - assertFalse(simpleCache.setContentLength(KEY_1, 15)); - assertEquals("Content length should be unchanged.", - 150, simpleCache.getContentLength(KEY_1)); - - /* TODO Enable when the length persistance is fixed // Check if values are kept after cache is reloaded. - simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); - assertEquals(150, simpleCache.getContentLength(KEY_1)); - CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145); + SimpleCache simpleCache2 = getSimpleCache(); + Set keys = simpleCache.getKeys(); + Set keys2 = simpleCache2.getKeys(); + assertEquals(keys, keys2); + for (String key : keys) { + assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key)); + assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key)); + } // Removing the last span shouldn't cause the length be change next time cache loaded - simpleCache.removeSpan(lastSpan); - simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); - assertEquals(150, simpleCache.getContentLength(KEY_1)); - */ + SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); + simpleCache2.removeSpan(lastSpan); + simpleCache2 = getSimpleCache(); + assertEquals(150, simpleCache2.getContentLength(KEY_1)); + } + + private SimpleCache getSimpleCache() { + return new SimpleCache(cacheDir, new NoOpCacheEvictor()); } private void addCache(SimpleCache simpleCache, int position, int length) throws IOException { diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 8d74379093..e3d681d6dd 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.TestUtil; import java.text.ParseException; import java.util.ArrayList; @@ -146,20 +145,6 @@ public class UtilTest extends TestCase { assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); } - public void testGetHexStringByteArray() throws Exception { - assertHexStringByteArray("", new byte[] {}); - assertHexStringByteArray("01", new byte[] {1}); - assertHexStringByteArray("FF", new byte[] {(byte) 255}); - assertHexStringByteArray("01020304", new byte[] {1, 2, 3, 4}); - assertHexStringByteArray("0123456789ABCDEF", - new byte[] {1, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF}); - } - - private void assertHexStringByteArray(String hex, byte[] array) { - assertEquals(hex, Util.getHexString(array)); - MoreAsserts.assertEquals(array, Util.getBytesFromHexString(hex)); - } - public void testUnescapeInvalidFileName() { assertNull(Util.unescapeFileName("%a")); assertNull(Util.unescapeFileName("%xyz")); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index a8a8de4361..27b989c36f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -187,10 +187,8 @@ public interface Cache { * * @param key The cache key for the data. * @param length The length of the data. - * @return Whether the length was set successfully. Returns false if the length conflicts with the - * existing contents of the cache. */ - boolean setContentLength(String key, long length); + void setContentLength(String key, long length); /** * Returns the content length for the given key if one set, or {@link diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 1f56d4ef83..d53a5d8fe8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -26,7 +25,6 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Retention; @@ -81,8 +79,6 @@ public final class CacheDataSource implements DataSource { } - private static final String TAG = "CacheDataSource"; - private final Cache cache; private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; @@ -164,7 +160,7 @@ public final class CacheDataSource implements DataSource { try { uri = dataSpec.uri; flags = dataSpec.flags; - key = dataSpec.key != null ? dataSpec.key : Util.sha1(uri.toString()); + key = dataSpec.key != null ? dataSpec.key : uri.toString(); readPosition = dataSpec.position; currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError; if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { @@ -333,10 +329,7 @@ public final class CacheDataSource implements DataSource { } private void setContentLength(long length) { - if (!cache.setContentLength(key, length)) { - Log.e(TAG, "cache.setContentLength(" + length + ") failed. cache.getContentLength() = " - + cache.getContentLength(key)); - } + cache.setContentLength(key, length); } private void closeCurrentSource() throws IOException { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index d706f4f006..fb96c0fb0e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -16,21 +16,12 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Util; import java.io.File; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}). */ -public final class CacheSpan implements Comparable { - - private static final String SUFFIX = ".v2.exo"; - private static final Pattern CACHE_FILE_PATTERN_V1 = - Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL); - private static final Pattern CACHE_FILE_PATTERN_V2 = - Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL); +public class CacheSpan implements Comparable { /** * The cache key that uniquely identifies the original stream. @@ -57,64 +48,34 @@ public final class CacheSpan implements Comparable { */ public final long lastAccessTimestamp; - public static File getCacheFileName(File cacheDir, String key, long offset, - long lastAccessTimestamp) { - return new File(cacheDir, - Util.escapeFileName(key) + "." + offset + "." + lastAccessTimestamp + SUFFIX); - } - - public static CacheSpan createLookup(String key, long position) { - return new CacheSpan(key, position, C.LENGTH_UNSET, false, C.TIME_UNSET, null); - } - - public static CacheSpan createOpenHole(String key, long position) { - return new CacheSpan(key, position, C.LENGTH_UNSET, false, C.TIME_UNSET, null); - } - - public static CacheSpan createClosedHole(String key, long position, long length) { - return new CacheSpan(key, position, length, false, C.TIME_UNSET, null); + /** + * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated. + * + * @param key The cache key that uniquely identifies the original stream. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + */ + public CacheSpan(String key, long position, long length) { + this(key, position, length, C.TIME_UNSET, null); } /** - * Creates a cache span from an underlying cache file. + * Creates a CacheSpan. * - * @param file The cache file. - * @return The span, or null if the file name is not correctly formatted. + * @param key The cache key that uniquely identifies the original stream. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if + * {@link #isCached} is false. + * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ - public static CacheSpan createCacheEntry(File file) { - Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(file.getName()); - if (!matcher.matches()) { - return null; - } - String key = Util.unescapeFileName(matcher.group(1)); - return key == null ? null : createCacheEntry( - key, Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)), file); - } - - static File upgradeIfNeeded(File file) { - Matcher matcher = CACHE_FILE_PATTERN_V1.matcher(file.getName()); - if (!matcher.matches()) { - return file; - } - String key = matcher.group(1); // Keys were not escaped in version 1. - File newCacheFile = getCacheFileName(file.getParentFile(), key, - Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); - file.renameTo(newCacheFile); - return newCacheFile; - } - - private static CacheSpan createCacheEntry(String key, long position, long lastAccessTimestamp, - File file) { - return new CacheSpan(key, position, file.length(), true, lastAccessTimestamp, file); - } - - // Visible for testing. - CacheSpan(String key, long position, long length, boolean isCached, - long lastAccessTimestamp, File file) { + public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) { this.key = key; this.position = position; this.length = length; - this.isCached = isCached; + this.isCached = file != null; this.file = file; this.lastAccessTimestamp = lastAccessTimestamp; } @@ -127,15 +88,10 @@ public final class CacheSpan implements Comparable { } /** - * Renames the file underlying this cache span to update its last access time. - * - * @return A {@link CacheSpan} representing the updated cache file. + * Returns whether this is a hole {@link CacheSpan}. */ - public CacheSpan touch() { - long now = System.currentTimeMillis(); - File newCacheFile = getCacheFileName(file.getParentFile(), key, position, now); - file.renameTo(newCacheFile); - return createCacheEntry(key, position, now, newCacheFile); + public boolean isHoleSpan() { + return !isCached; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java new file mode 100644 index 0000000000..a25688f9db --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.TreeSet; + +/** + * Defines the cached content for a single stream. + */ +/*package*/ final class CachedContent { + + /** + * The cache file id that uniquely identifies the original stream. + */ + public final int id; + /** + * The cache key that uniquely identifies the original stream. + */ + public final String key; + /** + * The cached spans of this content. + */ + private final TreeSet cachedSpans; + /** + * The length of the original stream, or {@link C#LENGTH_UNSET} if the length is unknown. + */ + private long length; + + /** + * Reads an instance from a {@link DataInputStream}. + * + * @param input Input stream containing values needed to initialize CachedContent instance. + * @throws IOException If an error occurs during reading values. + */ + public CachedContent(DataInputStream input) throws IOException { + this(input.readInt(), input.readUTF(), input.readLong()); + } + + /** + * Creates a CachedContent. + * + * @param id The cache file id. + * @param key The cache stream key. + * @param length The length of the original stream. + */ + public CachedContent(int id, String key, long length) { + this.id = id; + this.key = key; + this.length = length; + this.cachedSpans = new TreeSet<>(); + } + + /** + * Writes the instance to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + public void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(id); + output.writeUTF(key); + output.writeLong(length); + } + + /** Returns the length of the content. */ + public long getLength() { + return length; + } + + /** Sets the length of the content. */ + public void setLength(long length) { + this.length = length; + } + + /** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */ + public void addSpan(SimpleCacheSpan span) { + cachedSpans.add(span); + } + + /** Returns a set of all {@link SimpleCacheSpan}s. */ + public TreeSet getSpans() { + return cachedSpans; + } + + /** + * Returns the span containing the position. If there isn't one, it returns a hole span + * which defines the maximum extents of the hole in the cache. + */ + public SimpleCacheSpan getSpan(long position) { + SimpleCacheSpan span = getSpanInternal(position); + if (!span.isCached) { + SimpleCacheSpan ceilEntry = cachedSpans.ceiling(span); + return ceilEntry == null ? SimpleCacheSpan.createOpenHole(key, position) + : SimpleCacheSpan.createClosedHole(key, position, ceilEntry.position - position); + } + return span; + } + + /** Queries if a range is entirely available in the cache. */ + public boolean isCached(long position, long length) { + SimpleCacheSpan floorSpan = getSpanInternal(position); + if (!floorSpan.isCached) { + // We don't have a span covering the start of the queried region. + return false; + } + long queryEndPosition = position + length; + long currentEndPosition = floorSpan.position + floorSpan.length; + if (currentEndPosition >= queryEndPosition) { + // floorSpan covers the queried region. + return true; + } + for (SimpleCacheSpan next : cachedSpans.tailSet(floorSpan, false)) { + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + return false; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + return true; + } + } + // We ran out of spans before covering the queried region. + return false; + } + + /** + * Copies the given span with an updated last access time. Passed span becomes invalid after this + * call. + * + * @param cacheSpan Span to be copied and updated. + * @return a span with the updated last access time. + */ + public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) { + // Remove the old span from the in-memory representation. + Assertions.checkState(cachedSpans.remove(cacheSpan)); + // Obtain a new span with updated last access timestamp. + SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id); + // Rename the cache file + cacheSpan.file.renameTo(newCacheSpan.file); + // Add the updated span back into the in-memory representation. + cachedSpans.add(newCacheSpan); + return newCacheSpan; + } + + /** Returns whether there are any spans cached. */ + public boolean isEmpty() { + return cachedSpans.isEmpty(); + } + + /** Removes the given span from cache. */ + public boolean removeSpan(CacheSpan span) { + if (cachedSpans.remove(span)) { + span.file.delete(); + return true; + } + return false; + } + + /** Calculates a hash code for the header of this {@code CachedContent}. */ + public int headerHashCode() { + int result = id; + result = 31 * result + key.hashCode(); + result = 31 * result + (int) (length ^ (length >>> 32)); + return result; + } + + /** + * Returns the span containing the position. If there isn't one, it returns the lookup span it + * used for searching. + */ + private SimpleCacheSpan getSpanInternal(long position) { + SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position); + SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan); + return floorSpan == null || floorSpan.position + floorSpan.length <= position ? lookupSpan + : floorSpan; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java new file mode 100644 index 0000000000..4f884606ee --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import android.util.SparseArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.AtomicFile; +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.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Set; + +/** + * This class maintains the index of cached content. + */ +/*package*/ final class CachedContentIndex { + + public static final String FILE_NAME = "cached_content_index.exi"; + private static final int VERSION = 1; + + private final HashMap keyToContent; + private final SparseArray idToKey; + private final AtomicFile atomicFile; + private boolean changed; + + /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */ + public CachedContentIndex(File cacheDir) { + keyToContent = new HashMap<>(); + idToKey = new SparseArray<>(); + atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); + } + + /** Loads the index file. */ + public void load() { + Assertions.checkState(!changed); + File cacheIndex = atomicFile.getBaseFile(); + if (cacheIndex.exists()) { + if (!readFile()) { + cacheIndex.delete(); + keyToContent.clear(); + idToKey.clear(); + } + } + } + + /** Stores the index data to index file if there is a change. */ + public void store() { + if (!changed) { + return; + } + writeFile(); + changed = false; + } + + /** + * Adds the given key to the index if it isn't there already. + * + * @param key The cache key that uniquely identifies the original stream. + * @return A new or existing CachedContent instance with the given key. + */ + public CachedContent add(String key) { + CachedContent cachedContent = keyToContent.get(key); + if (cachedContent == null) { + cachedContent = addNew(key, C.LENGTH_UNSET); + } + return cachedContent; + } + + /** Returns a CachedContent instance with the given key or null if there isn't one. */ + public CachedContent get(String key) { + return keyToContent.get(key); + } + + /** + * Returns a Collection of all CachedContent instances in the index. The collection is backed by + * the {@code keyToContent} map, so changes to the map are reflected in the collection, and + * vice-versa. If the map is modified while an iteration over the collection is in progress + * (except through the iterator's own remove operation), the results of the iteration are + * undefined. + */ + public Collection getAll() { + return keyToContent.values(); + } + + /** Returns an existing or new id assigned to the given key. */ + public int assignIdForKey(String key) { + return add(key).id; + } + + /** Returns the key which has the given id assigned. */ + public String getKeyForId(int id) { + return idToKey.get(id); + } + + /** + * Removes {@link CachedContent} with the given key from index. It shouldn't contain any spans. + * + * @throws IllegalStateException If {@link CachedContent} isn't empty. + */ + public void removeEmpty(String key) { + CachedContent cachedContent = keyToContent.remove(key); + if (cachedContent != null) { + Assertions.checkState(cachedContent.isEmpty()); + idToKey.remove(cachedContent.id); + changed = true; + } + } + + /** Removes empty {@link CachedContent} instances from index. */ + public void removeEmpty() { + LinkedList cachedContentToBeRemoved = new LinkedList<>(); + for (CachedContent cachedContent : keyToContent.values()) { + if (cachedContent.isEmpty()) { + cachedContentToBeRemoved.add(cachedContent.key); + } + } + for (String key : cachedContentToBeRemoved) { + removeEmpty(key); + } + } + + /** + * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so + * changes to the map are reflected in the set, and vice-versa. If the map is modified while an + * iteration over the set is in progress (except through the iterator's own remove operation), the + * results of the iteration are undefined. + */ + public Set getKeys() { + return keyToContent.keySet(); + } + + /** + * Sets the content length for the given key. A new {@link CachedContent} is added if there isn't + * one already with the given key. + */ + public void setContentLength(String key, long length) { + CachedContent cachedContent = get(key); + if (cachedContent != null) { + if (cachedContent.getLength() != length) { + cachedContent.setLength(length); + changed = true; + } + } else { + addNew(key, length); + } + } + + /** + * Returns the content length for the given key if one set, or {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. + */ + public long getContentLength(String key) { + CachedContent cachedContent = get(key); + return cachedContent == null ? C.LENGTH_UNSET : cachedContent.getLength(); + } + + private boolean readFile() { + DataInputStream input = null; + try { + input = new DataInputStream(atomicFile.openRead()); + int version = input.readInt(); + if (version != VERSION) { + // Currently there is no other version + return false; + } + input.readInt(); // ignore flags placeholder + int count = input.readInt(); + int hashCode = 0; + for (int i = 0; i < count; i++) { + CachedContent cachedContent = new CachedContent(input); + addNew(cachedContent); + hashCode += cachedContent.headerHashCode(); + } + if (input.readInt() != hashCode) { + return false; + } + } catch (IOException e) { + return false; + } finally { + if (input != null) { + Util.closeQuietly(input); + } + } + return true; + } + + private void writeFile() { + FileOutputStream outputStream = null; + try { + outputStream = atomicFile.startWrite(); + DataOutputStream output = new DataOutputStream(outputStream); + + output.writeInt(VERSION); + output.writeInt(0); // flags placeholder + output.writeInt(keyToContent.size()); + int hashCode = 0; + for (CachedContent cachedContent : keyToContent.values()) { + cachedContent.writeToStream(output); + hashCode += cachedContent.headerHashCode(); + } + output.writeInt(hashCode); + + output.flush(); + atomicFile.finishWrite(outputStream); + } catch (IOException e) { + atomicFile.failWrite(outputStream); + throw new RuntimeException("Writing the new cache index file failed.", e); + } + } + + /** Adds the given CachedContent to the index. */ + /*package*/ void addNew(CachedContent cachedContent) { + keyToContent.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + changed = true; + } + + private CachedContent addNew(String key, long length) { + int id = getNewId(idToKey); + CachedContent cachedContent = new CachedContent(id, key, length); + addNew(cachedContent); + return cachedContent; + } + + /** + * Returns an id which isn't used in the given array. If the maximum id in the array is smaller + * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it + * returns the smallest unused non-negative integer. + */ + //@VisibleForTesting + public static int getNewId(SparseArray idToKey) { + int size = idToKey.size(); + int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); + if (id < 0) { // In case if we pass max int value. + // TODO optimization: defragmentation or binary search? + for (id = 0; id < size; id++) { + if (id != idToKey.keyAt(id)) { + break; + } + } + } + return id; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index a2f2596ad5..f21929a748 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -16,16 +16,13 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; - -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; +import java.util.LinkedList; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; @@ -38,7 +35,7 @@ public final class SimpleCache implements Cache { private final File cacheDir; private final CacheEvictor evictor; private final HashMap lockedSpans; - private final HashMap>> cachedSpans; + private final CachedContentIndex index; private final HashMap> listeners; private long totalSpace = 0; @@ -52,7 +49,7 @@ public final class SimpleCache implements Cache { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.cachedSpans = new HashMap<>(); + this.index = new CachedContentIndex(cacheDir); this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -62,6 +59,7 @@ public final class SimpleCache implements Cache { synchronized (SimpleCache.this) { conditionVariable.open(); initialize(); + SimpleCache.this.evictor.onCacheInitialized(); } } }.start(); @@ -92,13 +90,13 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { - TreeSet spansForKey = getSpansForKey(key); - return spansForKey == null ? null : new TreeSet<>(spansForKey); + CachedContent cachedContent = index.get(key); + return cachedContent == null ? null : new TreeSet(cachedContent.getSpans()); } @Override public synchronized Set getKeys() { - return new HashSet<>(cachedSpans.keySet()); + return new HashSet<>(index.getKeys()); } @Override @@ -107,11 +105,10 @@ public final class SimpleCache implements Cache { } @Override - public synchronized CacheSpan startReadWrite(String key, long position) + public synchronized SimpleCacheSpan startReadWrite(String key, long position) throws InterruptedException { - CacheSpan lookupSpan = CacheSpan.createLookup(key, position); while (true) { - CacheSpan span = startReadWriteNonBlocking(lookupSpan); + SimpleCacheSpan span = startReadWriteNonBlocking(key, position); if (span != null) { return span; } else { @@ -125,25 +122,20 @@ public final class SimpleCache implements Cache { } @Override - public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) { - return startReadWriteNonBlocking(CacheSpan.createLookup(key, position)); - } - - private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) { - CacheSpan cacheSpan = getSpan(lookupSpan); + public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) { + SimpleCacheSpan cacheSpan = getSpan(key, position); // Read case. if (cacheSpan.isCached) { // Obtain a new span with updated last access timestamp. - CacheSpan newCacheSpan = cacheSpan.touch(); - replaceSpan(cacheSpan, newCacheSpan); + SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan); notifySpanTouched(cacheSpan, newCacheSpan); return newCacheSpan; } // Write case, lock available. - if (!lockedSpans.containsKey(lookupSpan.key)) { - lockedSpans.put(lookupSpan.key, cacheSpan); + if (!lockedSpans.containsKey(key)) { + lockedSpans.put(key, cacheSpan); return cacheSpan; } @@ -156,16 +148,17 @@ public final class SimpleCache implements Cache { Assertions.checkState(lockedSpans.containsKey(key)); if (!cacheDir.exists()) { // For some reason the cache directory doesn't exist. Make a best effort to create it. - removeStaleSpans(); + removeStaleSpansAndCachedContents(); cacheDir.mkdirs(); } evictor.onStartFile(this, key, position, maxLength); - return CacheSpan.getCacheFileName(cacheDir, key, position, System.currentTimeMillis()); + return SimpleCacheSpan.getCacheFile(cacheDir, index.assignIdForKey(key), position, + System.currentTimeMillis()); } @Override public synchronized void commitFile(File file) { - CacheSpan span = CacheSpan.createCacheEntry(file); + SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index); Assertions.checkState(span != null); Assertions.checkState(lockedSpans.containsKey(span.key)); // If the file doesn't exist, don't add it to the in-memory representation. @@ -183,6 +176,7 @@ public final class SimpleCache implements Cache { Assertions.checkState((span.position + span.length) <= length); } addSpan(span); + index.store(); notifyAll(); } @@ -193,40 +187,33 @@ public final class SimpleCache implements Cache { } /** - * Returns the cache {@link CacheSpan} corresponding to the provided lookup {@link CacheSpan}. - *

- * If the lookup position is contained by an existing entry in the cache, then the returned - * {@link CacheSpan} defines the file in which the data is stored. If the lookup position is not - * contained by an existing entry, then the returned {@link CacheSpan} defines the maximum extents - * of the hole in the cache. + * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link + * SimpleCacheSpan}. * - * @param lookupSpan A lookup {@link CacheSpan} specifying a key and position. - * @return The corresponding cache {@link CacheSpan}. + *

If the lookup position is contained by an existing entry in the cache, then the returned + * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is + * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the + * maximum extents of the hole in the cache. + * + * @param key The key of the span being requested. + * @param position The position of the span being requested. + * @return The corresponding cache {@link SimpleCacheSpan}. */ - private CacheSpan getSpan(CacheSpan lookupSpan) { - String key = lookupSpan.key; - long offset = lookupSpan.position; - TreeSet entries = getSpansForKey(key); - if (entries == null) { - return CacheSpan.createOpenHole(key, lookupSpan.position); + private SimpleCacheSpan getSpan(String key, long position) { + CachedContent cachedContent = index.get(key); + if (cachedContent == null) { + return SimpleCacheSpan.createOpenHole(key, position); } - CacheSpan floorSpan = entries.floor(lookupSpan); - if (floorSpan != null && - floorSpan.position <= offset && offset < floorSpan.position + floorSpan.length) { - // The lookup position is contained within floorSpan. - if (floorSpan.file.exists()) { - return floorSpan; - } else { + while (true) { + SimpleCacheSpan span = cachedContent.getSpan(position); + if (span.isCached && !span.file.exists()) { // The file has been deleted from under us. It's likely that other files will have been // deleted too, so scan the whole in-memory representation. - removeStaleSpans(); - return getSpan(lookupSpan); + removeStaleSpansAndCachedContents(); + continue; } + return span; } - CacheSpan ceilEntry = entries.ceiling(lookupSpan); - return ceilEntry == null ? CacheSpan.createOpenHole(key, lookupSpan.position) : - CacheSpan.createClosedHole(key, lookupSpan.position, - ceilEntry.position - lookupSpan.position); } /** @@ -235,25 +222,37 @@ public final class SimpleCache implements Cache { private void initialize() { if (!cacheDir.exists()) { cacheDir.mkdirs(); + return; } + + index.load(); + + SimpleCacheSpan.upgradeOldFiles(cacheDir, index); + File[] files = cacheDir.listFiles(); if (files == null) { return; } for (File file : files) { - if (file.length() == 0) { - file.delete(); - } else { - file = CacheSpan.upgradeIfNeeded(file); - CacheSpan span = CacheSpan.createCacheEntry(file); - if (span == null) { - file.delete(); - } else { - addSpan(span); + String name = file.getName(); + if (!name.endsWith(SimpleCacheSpan.SUFFIX)) { + if (!name.equals(CachedContentIndex.FILE_NAME)) { + file.delete(); // Delete unknown files } + continue; + } + + SimpleCacheSpan span = file.length() > 0 + ? SimpleCacheSpan.createCacheEntry(file, index) : null; + if (span != null) { + addSpan(span); + } else { + file.delete(); } } - evictor.onCacheInitialized(); + + index.removeEmpty(); + index.store(); } /** @@ -261,59 +260,47 @@ public final class SimpleCache implements Cache { * * @param span The span to be added. */ - private void addSpan(CacheSpan span) { - Pair> entryForKey = cachedSpans.get(span.key); - TreeSet spansForKey; - if (entryForKey == null) { - spansForKey = new TreeSet<>(); - setKeyValue(span.key, C.LENGTH_UNSET, spansForKey); - } else { - spansForKey = entryForKey.second; - } - spansForKey.add(span); + private void addSpan(SimpleCacheSpan span) { + index.add(span.key).addSpan(span); totalSpace += span.length; notifySpanAdded(span); } - @Override - public synchronized void removeSpan(CacheSpan span) { - TreeSet spansForKey = getSpansForKey(span.key); + private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) { + CachedContent cachedContent = index.get(span.key); + Assertions.checkState(cachedContent.removeSpan(span)); totalSpace -= span.length; - Assertions.checkState(spansForKey.remove(span)); - span.file.delete(); - if (spansForKey.isEmpty()) { - cachedSpans.remove(span.key); + if (removeEmptyCachedContent && cachedContent.isEmpty()) { + index.removeEmpty(cachedContent.key); + index.store(); } notifySpanRemoved(span); } + @Override + public synchronized void removeSpan(CacheSpan span) { + removeSpan(span, true); + } + /** * Scans all of the cached spans in the in-memory representation, removing any for which files * no longer exist. */ - private void removeStaleSpans() { - Iterator>>> iterator = - cachedSpans.entrySet().iterator(); - while (iterator.hasNext()) { - Entry>> next = iterator.next(); - Iterator spanIterator = next.getValue().second.iterator(); - boolean isEmpty = true; - while (spanIterator.hasNext()) { - CacheSpan span = spanIterator.next(); + private void removeStaleSpansAndCachedContents() { + LinkedList spansToBeRemoved = new LinkedList<>(); + for (CachedContent cachedContent : index.getAll()) { + for (CacheSpan span : cachedContent.getSpans()) { if (!span.file.exists()) { - spanIterator.remove(); - if (span.isCached) { - totalSpace -= span.length; - } - notifySpanRemoved(span); - } else { - isEmpty = false; + spansToBeRemoved.add(span); } } - if (isEmpty) { - iterator.remove(); - } } + for (CacheSpan span : spansToBeRemoved) { + // Remove span but not CachedContent to prevent multiple index.store() calls. + removeSpan(span, false); + } + index.removeEmpty(); + index.store(); } private void notifySpanRemoved(CacheSpan span) { @@ -326,7 +313,7 @@ public final class SimpleCache implements Cache { evictor.onSpanRemoved(this, span); } - private void notifySpanAdded(CacheSpan span) { + private void notifySpanAdded(SimpleCacheSpan span) { ArrayList keyListeners = listeners.get(span.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { @@ -336,7 +323,7 @@ public final class SimpleCache implements Cache { evictor.onSpanAdded(this, span); } - private void notifySpanTouched(CacheSpan oldSpan, CacheSpan newSpan) { + private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) { ArrayList keyListeners = listeners.get(oldSpan.key); if (keyListeners != null) { for (int i = keyListeners.size() - 1; i >= 0; i--) { @@ -348,82 +335,22 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { - TreeSet entries = getSpansForKey(key); - if (entries == null) { + CachedContent cachedContent = index.get(key); + if (cachedContent == null) { return false; } - CacheSpan lookupSpan = CacheSpan.createLookup(key, position); - CacheSpan floorSpan = entries.floor(lookupSpan); - if (floorSpan == null || floorSpan.position + floorSpan.length <= position) { - // We don't have a span covering the start of the queried region. - return false; - } - long queryEndPosition = position + length; - long currentEndPosition = floorSpan.position + floorSpan.length; - if (currentEndPosition >= queryEndPosition) { - // floorSpan covers the queried region. - return true; - } - for (CacheSpan next : entries.tailSet(floorSpan, false)) { - if (next.position > currentEndPosition) { - // There's a hole in the cache within the queried region. - return false; - } - // We expect currentEndPosition to always equal (next.position + next.length), but - // perform a max check anyway to guard against the existence of overlapping spans. - currentEndPosition = Math.max(currentEndPosition, next.position + next.length); - if (currentEndPosition >= queryEndPosition) { - // We've found spans covering the queried region. - return true; - } - } - // We ran out of spans before covering the queried region. - return false; + return cachedContent.isCached(position, length); } @Override - public synchronized boolean setContentLength(String key, long length) { - Pair> entryForKey = cachedSpans.get(key); - TreeSet entries; - if (entryForKey != null) { - entries = entryForKey.second; - if (entries != null && !entries.isEmpty()) { - CacheSpan last = entries.last(); - long end = last.position + last.length; - if (end > length) { - return false; - } - } - } else { - entries = new TreeSet<>(); - } - // TODO persist the length value - setKeyValue(key, length, entries); - return true; + public synchronized void setContentLength(String key, long length) { + index.setContentLength(key, length); + index.store(); } @Override public synchronized long getContentLength(String key) { - Pair> entryForKey = cachedSpans.get(key); - return entryForKey == null ? C.LENGTH_UNSET : entryForKey.first; - } - - - private TreeSet getSpansForKey(String key) { - Pair> entryForKey = cachedSpans.get(key); - return entryForKey != null ? entryForKey.second : null; - } - - private void setKeyValue(String key, long length, TreeSet entries) { - cachedSpans.put(key, Pair.create(length, entries)); - } - - private void replaceSpan(CacheSpan oldSpan, CacheSpan newSpan) { - // Remove the old span from the in-memory representation. - TreeSet spansForKey = getSpansForKey(oldSpan.key); - Assertions.checkState(spansForKey.remove(oldSpan)); - // Add the updated span back into the in-memory representation. - spansForKey.add(newSpan); + return index.getContentLength(key); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java new file mode 100644 index 0000000000..deac524e2a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016 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.upstream.cache; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class stores span metadata in filename. + */ +/*package*/ final class SimpleCacheSpan extends CacheSpan { + + private static final String FILE_EXTENSION = "exo"; + public static final String SUFFIX = ".v3." + FILE_EXTENSION; + private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile( + "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile( + "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile( + "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\." + FILE_EXTENSION + "$", Pattern.DOTALL); + + public static File getCacheFile(File cacheDir, int id, long position, + long lastAccessTimestamp) { + return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX); + } + + public static SimpleCacheSpan createLookup(String key, long position) { + return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); + } + + public static SimpleCacheSpan createOpenHole(String key, long position) { + return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); + } + + public static SimpleCacheSpan createClosedHole(String key, long position, long length) { + return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null); + } + + /** + * Creates a cache span from an underlying cache file. + * + * @param file The cache file. + * @param index Cached content index. + * @return The span, or null if the file name is not correctly formatted, or if the id is not + * present in the content index. + */ + public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) { + Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(file.getName()); + if (!matcher.matches()) { + return null; + } + long length = file.length(); + int id = Integer.parseInt(matcher.group(1)); + String key = index.getKeyForId(id); + return key == null ? null : new SimpleCacheSpan(key, Long.parseLong(matcher.group(2)), length, + Long.parseLong(matcher.group(3)), file); + } + + /** Upgrades span files with old versions. */ + public static void upgradeOldFiles(File cacheDir, CachedContentIndex index) { + for (File file : cacheDir.listFiles()) { + String name = file.getName(); + if (!name.endsWith(SUFFIX) && name.endsWith(FILE_EXTENSION)) { + upgradeFile(file, index); + } + } + } + + private static void upgradeFile(File file, CachedContentIndex index) { + String key; + String filename = file.getName(); + Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename); + if (matcher.matches()) { + key = Util.unescapeFileName(matcher.group(1)); + if (key == null) { + return; + } + } else { + matcher = CACHE_FILE_PATTERN_V1.matcher(filename); + if (!matcher.matches()) { + return; + } + key = matcher.group(1); // Keys were not escaped in version 1. + } + + File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), + Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + file.renameTo(newCacheFile); + } + + private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp, + File file) { + super(key, position, length, lastAccessTimestamp, file); + } + + /** + * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This + * doesn't copy or change the underlying cache file. + * + * @param id The cache file id. + * @return A {@link SimpleCacheSpan} with updated last access time stamp. + * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false). + */ + public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) { + Assertions.checkState(isCached); + long now = System.currentTimeMillis(); + File newCacheFile = getCacheFile(file.getParentFile(), id, position, now); + return new SimpleCacheSpan(key, position, length, now, newCacheFile); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java new file mode 100644 index 0000000000..3746a741e0 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2009 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.util; + +import android.util.Log; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Exoplayer internal version of the framework's {@link android.util.AtomicFile}, + * a helper class for performing atomic operations on a file by creating a + * backup file until a write has successfully completed. + *

+ * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + *

+ * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + *

+ */ +public class AtomicFile { + private final File mBaseName; + private final File mBackupName; + + /** + * Create a new AtomicFile for a file located at the given File path. + * The secondary backup file will be the same file path with ".bak" appended. + */ + public AtomicFile(File baseName) { + mBaseName = baseName; + mBackupName = new File(baseName.getPath() + ".bak"); + } + + /** + * Return the path to the base file. You should not generally use this, + * as the data at that path may not be valid. + */ + public File getBaseFile() { + return mBaseName; + } + + /** + * Delete the atomic file. This deletes both the base and backup files. + */ + public void delete() { + mBaseName.delete(); + mBackupName.delete(); + } + + /** + * Start a new write operation on the file. This returns a FileOutputStream + * to which you can write the new file data. The existing file is replaced + * with the new data. You must not directly close the given + * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} + * or {@link #failWrite(FileOutputStream)}. + * + *

Note that if another thread is currently performing + * a write, this will simply replace whatever that thread is writing + * with the new file being written by this thread, and when the other + * thread finishes the write the new write operation will no longer be + * safe (or will be lost). You must do your own threading protection for + * access to AtomicFile. + */ + public FileOutputStream startWrite() throws IOException { + // Rename the current file so it may be used as a backup during the next read + if (mBaseName.exists()) { + if (!mBackupName.exists()) { + if (!mBaseName.renameTo(mBackupName)) { + Log.w("AtomicFile", "Couldn't rename file " + mBaseName + + " to backup file " + mBackupName); + } + } else { + mBaseName.delete(); + } + } + FileOutputStream str = null; + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e) { + File parent = mBaseName.getParentFile(); + if (!parent.mkdirs()) { + throw new IOException("Couldn't create directory " + mBaseName); + } + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e2) { + throw new IOException("Couldn't create " + mBaseName); + } + } + return str; + } + + /** + * Call when you have successfully finished writing to the stream + * returned by {@link #startWrite()}. This will close, sync, and + * commit the new data. The next attempt to read the atomic file + * will return the new file stream. + */ + public void finishWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBackupName.delete(); + } catch (IOException e) { + Log.w("AtomicFile", "finishWrite: Got exception:", e); + } + } + } + + /** + * Call when you have failed for some reason at writing to the stream + * returned by {@link #startWrite()}. This will close the current + * write stream, and roll back to the previous state of the file. + */ + public void failWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } catch (IOException e) { + Log.w("AtomicFile", "failWrite: Got exception:", e); + } + } + } + + /** + * Open the atomic file for reading. If there previously was an + * incomplete write, this will roll back to the last good data before + * opening for read. You should call close() on the FileInputStream when + * you are done reading from it. + * + *

Note that if another thread is currently performing + * a write, this will incorrectly consider it to be in the state of a bad + * write and roll back, causing the new data currently being written to + * be dropped. You must do your own threading protection for access to + * AtomicFile. + */ + public FileInputStream openRead() throws FileNotFoundException { + if (mBackupName.exists()) { + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } + return new FileInputStream(mBaseName); + } + + /** + * A convenience for {@link #openRead()} that also reads all of the + * file contents into a byte array which is returned. + */ + public byte[] readFully() throws IOException { + FileInputStream stream = openRead(); + try { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length - pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length - pos) { + byte[] newData = new byte[pos + avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + stream.close(); + } + } + + private static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + // do nothing + } + return false; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index c4505fd8b9..0184018bc9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -34,15 +34,12 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; @@ -97,7 +94,6 @@ public final class Util { Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); - private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); private Util() {} @@ -215,13 +211,14 @@ public final class Util { } /** - * Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur. + * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link + * java.io.OutputStream} and {@link InputStream} are {@code Closeable}. * - * @param outputStream The {@link OutputStream} to close. + * @param closeable The {@link Closeable} to close. */ - public static void closeQuietly(OutputStream outputStream) { + public static void closeQuietly(Closeable closeable) { try { - outputStream.close(); + closeable.close(); } catch (IOException e) { // Ignore. } @@ -630,21 +627,6 @@ public final class Util { return data; } - /** - * Returns a hex string representation of the given byte array. - * - * @param bytes The byte array. - */ - public static String getHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - int i = 0; - for (byte v : bytes) { - hexChars[i++] = HEX_DIGITS[(v >> 4) & 0xf]; - hexChars[i++] = HEX_DIGITS[v & 0xf]; - } - return new String(hexChars); - } - /** * Returns a string with comma delimited simple names of each object's class. * @@ -869,22 +851,6 @@ public final class Util { return initialValue; } - /** - * Returns the SHA-1 digest of {@code input} as a hex string. - * - * @param input The string whose SHA-1 digest is required. - */ - public static String sha1(String input) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = input.getBytes("UTF-8"); - digest.update(bytes, 0, bytes.length); - return getHexString(digest.digest()); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - /** * Gets the physical size of the default display, in pixels. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 6f4578b694..5962bf9911 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; +import android.content.Context; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; @@ -313,4 +314,12 @@ public class TestUtil { fileOrDirectory.delete(); } + /** Creates an empty folder in the application specific cache directory. */ + public static File createTempFolder(Context context) throws IOException { + File tempFolder = File.createTempFile("ExoPlayerTest", null, context.getCacheDir()); + Assert.assertTrue(tempFolder.delete()); + Assert.assertTrue(tempFolder.mkdir()); + return tempFolder; + } + }