Commit ff77d1e7 by eguven Committed by Oliver Woodman

Add index file to hold header information for cached content.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138373878
parent 992cfdec
...@@ -23,7 +23,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; ...@@ -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.FakeDataSource.Builder;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
// Create a temporary folder cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = File.createTempFile("CacheDataSourceTest", null);
assertTrue(cacheDir.delete());
assertTrue(cacheDir.mkdir());
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
...@@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
public void testMaxCacheFileSize() throws Exception { public void testMaxCacheFileSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false); CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, false, false); assertReadDataContentLength(cacheDataSource, false, false);
assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE), File[] files = cacheDir.listFiles();
cacheDir.listFiles().length); for (File file : files) {
if (file.getName().endsWith(SimpleCacheSpan.SUFFIX)) {
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
}
}
} }
public void testCacheAndRead() throws Exception { public void testCacheAndRead() throws Exception {
......
/*
* 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);
}
}
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<CachedContent> cachedContents = index.getAll();
assertEquals(2, cachedContents.size());
assertTrue(Arrays.asList(cachedContent1, cachedContent2).containsAll(cachedContents));
// test getKeys()
Set<String> 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<String> keys = index.getKeys();
Set<String> 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<String> 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));
}
}
/*
* 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<String> keys = index.getKeys();
assertEquals("There should be only one key for all files.", 1, keys.size());
assertTrue(keys.contains(key));
TreeSet<SimpleCacheSpan> spans = index.get(key).getSpans();
assertTrue("upgradeOldFiles() shouldn't add any spans.", spans.isEmpty());
HashMap<Long, Long> 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);
}
}
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File; import java.io.File;
...@@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
// Create a temporary folder this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = File.createTempFile("SimpleCacheTest", null);
assertTrue(cacheDir.delete());
assertTrue(cacheDir.mkdir());
} }
@Override @Override
...@@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
} }
public void testCommittingOneFile() throws Exception { public void testCommittingOneFile() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan.isCached); assertFalse(cacheSpan.isCached);
...@@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase {
} }
public void testSetGetLength() throws Exception { public void testSetGetLength() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); SimpleCache simpleCache = getSimpleCache();
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); 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)); assertEquals(15, simpleCache.getContentLength(KEY_1));
simpleCache.startReadWrite(KEY_1, 0); simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, 0, 15); addCache(simpleCache, 0, 15);
assertTrue(simpleCache.setContentLength(KEY_1, 150)); simpleCache.setContentLength(KEY_1, 150);
assertEquals(150, simpleCache.getContentLength(KEY_1)); assertEquals(150, simpleCache.getContentLength(KEY_1));
addCache(simpleCache, 140, 10); 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. // Check if values are kept after cache is reloaded.
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); SimpleCache simpleCache2 = getSimpleCache();
assertEquals(150, simpleCache.getContentLength(KEY_1)); Set<String> keys = simpleCache.getKeys();
CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145); Set<String> 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 // Removing the last span shouldn't cause the length be change next time cache loaded
simpleCache.removeSpan(lastSpan); SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache2.removeSpan(lastSpan);
assertEquals(150, simpleCache.getContentLength(KEY_1)); 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 { private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -146,20 +145,6 @@ public class UtilTest extends TestCase { ...@@ -146,20 +145,6 @@ public class UtilTest extends TestCase {
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); 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() { public void testUnescapeInvalidFileName() {
assertNull(Util.unescapeFileName("%a")); assertNull(Util.unescapeFileName("%a"));
assertNull(Util.unescapeFileName("%xyz")); assertNull(Util.unescapeFileName("%xyz"));
......
...@@ -187,10 +187,8 @@ public interface Cache { ...@@ -187,10 +187,8 @@ public interface Cache {
* *
* @param key The cache key for the data. * @param key The cache key for the data.
* @param length The length of 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 * Returns the content length for the given key if one set, or {@link
......
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
...@@ -26,7 +25,6 @@ import com.google.android.exoplayer2.upstream.DataSpec; ...@@ -26,7 +25,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.TeeDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException; import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
...@@ -81,8 +79,6 @@ public final class CacheDataSource implements DataSource { ...@@ -81,8 +79,6 @@ public final class CacheDataSource implements DataSource {
} }
private static final String TAG = "CacheDataSource";
private final Cache cache; private final Cache cache;
private final DataSource cacheReadDataSource; private final DataSource cacheReadDataSource;
private final DataSource cacheWriteDataSource; private final DataSource cacheWriteDataSource;
...@@ -164,7 +160,7 @@ public final class CacheDataSource implements DataSource { ...@@ -164,7 +160,7 @@ public final class CacheDataSource implements DataSource {
try { try {
uri = dataSpec.uri; uri = dataSpec.uri;
flags = dataSpec.flags; flags = dataSpec.flags;
key = dataSpec.key != null ? dataSpec.key : Util.sha1(uri.toString()); key = dataSpec.key != null ? dataSpec.key : uri.toString();
readPosition = dataSpec.position; readPosition = dataSpec.position;
currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError; currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError;
if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
...@@ -333,10 +329,7 @@ public final class CacheDataSource implements DataSource { ...@@ -333,10 +329,7 @@ public final class CacheDataSource implements DataSource {
} }
private void setContentLength(long length) { private void setContentLength(long length) {
if (!cache.setContentLength(key, length)) { cache.setContentLength(key, length);
Log.e(TAG, "cache.setContentLength(" + length + ") failed. cache.getContentLength() = "
+ cache.getContentLength(key));
}
} }
private void closeCurrentSource() throws IOException { private void closeCurrentSource() throws IOException {
......
...@@ -16,21 +16,12 @@ ...@@ -16,21 +16,12 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.io.File; 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}). * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
*/ */
public final class CacheSpan implements Comparable<CacheSpan> { public class CacheSpan implements Comparable<CacheSpan> {
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);
/** /**
* The cache key that uniquely identifies the original stream. * The cache key that uniquely identifies the original stream.
...@@ -57,64 +48,34 @@ public final class CacheSpan implements Comparable<CacheSpan> { ...@@ -57,64 +48,34 @@ public final class CacheSpan implements Comparable<CacheSpan> {
*/ */
public final long lastAccessTimestamp; 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 cache span from an underlying cache file. * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated.
* *
* @param file The cache file. * @param key The cache key that uniquely identifies the original stream.
* @return The span, or null if the file name is not correctly formatted. * @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 static CacheSpan createCacheEntry(File file) { public CacheSpan(String key, long position, long length) {
Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(file.getName()); this(key, position, length, C.TIME_UNSET, null);
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()); * Creates a CacheSpan.
if (!matcher.matches()) { *
return file; * @param key The cache key that uniquely identifies the original stream.
} * @param position The position of the {@link CacheSpan} in the original stream.
String key = matcher.group(1); // Keys were not escaped in version 1. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
File newCacheFile = getCacheFileName(file.getParentFile(), key, * open-ended hole.
Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if
file.renameTo(newCacheFile); * {@link #isCached} is false.
return newCacheFile; * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
} */
public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) {
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) {
this.key = key; this.key = key;
this.position = position; this.position = position;
this.length = length; this.length = length;
this.isCached = isCached; this.isCached = file != null;
this.file = file; this.file = file;
this.lastAccessTimestamp = lastAccessTimestamp; this.lastAccessTimestamp = lastAccessTimestamp;
} }
...@@ -127,15 +88,10 @@ public final class CacheSpan implements Comparable<CacheSpan> { ...@@ -127,15 +88,10 @@ public final class CacheSpan implements Comparable<CacheSpan> {
} }
/** /**
* Renames the file underlying this cache span to update its last access time. * Returns whether this is a hole {@link CacheSpan}.
*
* @return A {@link CacheSpan} representing the updated cache file.
*/ */
public CacheSpan touch() { public boolean isHoleSpan() {
long now = System.currentTimeMillis(); return !isCached;
File newCacheFile = getCacheFileName(file.getParentFile(), key, position, now);
file.renameTo(newCacheFile);
return createCacheEntry(key, position, now, newCacheFile);
} }
@Override @Override
......
/*
* 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<SimpleCacheSpan> 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<SimpleCacheSpan> 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;
}
}
/*
* 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<String, CachedContent> keyToContent;
private final SparseArray<String> 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<CachedContent> 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<String> 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<String> 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<String> 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;
}
}
/*
* 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);
}
}
/*
* 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.
* <p>
* 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).
* </p><p>
* 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.
* </p>
*/
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 <em>must not</em> directly close the given
* FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
* or {@link #failWrite(FileOutputStream)}.
*
* <p>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.
*
* <p>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;
}
}
...@@ -34,15 +34,12 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; ...@@ -34,15 +34,12 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException; import java.text.ParseException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
...@@ -97,7 +94,6 @@ public final class Util { ...@@ -97,7 +94,6 @@ public final class Util {
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); + "(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 Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
private Util() {} private Util() {}
...@@ -215,13 +211,14 @@ public final class 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 { try {
outputStream.close(); closeable.close();
} catch (IOException e) { } catch (IOException e) {
// Ignore. // Ignore.
} }
...@@ -631,21 +628,6 @@ public final class Util { ...@@ -631,21 +628,6 @@ public final class Util {
} }
/** /**
* 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. * Returns a string with comma delimited simple names of each object's class.
* *
* @param objects The objects whose simple class names should be comma delimited and returned. * @param objects The objects whose simple class names should be comma delimited and returned.
...@@ -870,22 +852,6 @@ public final class Util { ...@@ -870,22 +852,6 @@ public final class Util {
} }
/** /**
* 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. * Gets the physical size of the default display, in pixels.
* *
* @param context Any context. * @param context Any context.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.testutil; package com.google.android.exoplayer2.testutil;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.content.Context;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
...@@ -313,4 +314,12 @@ public class TestUtil { ...@@ -313,4 +314,12 @@ public class TestUtil {
fileOrDirectory.delete(); 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;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment