Add index file to hold header information for cached content.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138373878
This commit is contained in:
eguven 2016-11-07 03:39:15 -08:00 committed by Oliver Woodman
parent 992cfdecc2
commit ff77d1e72c
16 changed files with 1295 additions and 411 deletions

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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<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));
}
}

View file

@ -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<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);
}
}

View file

@ -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<String> keys = simpleCache.getKeys();
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
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 {

View file

@ -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"));

View file

@ -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

View file

@ -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 {

View file

@ -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<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);
public class CacheSpan implements Comparable<CacheSpan> {
/**
* The cache key that uniquely identifies the original stream.
@ -57,64 +48,34 @@ public final class CacheSpan implements Comparable<CacheSpan> {
*/
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<CacheSpan> {
}
/**
* 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

View file

@ -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<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;
}
}

View file

@ -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<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;
}
}

View file

@ -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<String, CacheSpan> lockedSpans;
private final HashMap<String, Pair<Long, TreeSet<CacheSpan>>> cachedSpans;
private final CachedContentIndex index;
private final HashMap<String, ArrayList<Listener>> 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<CacheSpan> getCachedSpans(String key) {
TreeSet<CacheSpan> spansForKey = getSpansForKey(key);
return spansForKey == null ? null : new TreeSet<>(spansForKey);
CachedContent cachedContent = index.get(key);
return cachedContent == null ? null : new TreeSet<CacheSpan>(cachedContent.getSpans());
}
@Override
public synchronized Set<String> 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}.
* <p>
* 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}.
* <p>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<CacheSpan> 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<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(span.key);
TreeSet<CacheSpan> 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<CacheSpan> 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<Entry<String, Pair<Long, TreeSet<CacheSpan>>>> iterator =
cachedSpans.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, Pair<Long, TreeSet<CacheSpan>>> next = iterator.next();
Iterator<CacheSpan> spanIterator = next.getValue().second.iterator();
boolean isEmpty = true;
while (spanIterator.hasNext()) {
CacheSpan span = spanIterator.next();
private void removeStaleSpansAndCachedContents() {
LinkedList<CacheSpan> 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<Listener> 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<Listener> 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<CacheSpan> 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<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(key);
TreeSet<CacheSpan> 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<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(key);
return entryForKey == null ? C.LENGTH_UNSET : entryForKey.first;
}
private TreeSet<CacheSpan> getSpansForKey(String key) {
Pair<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(key);
return entryForKey != null ? entryForKey.second : null;
}
private void setKeyValue(String key, long length, TreeSet<CacheSpan> 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<CacheSpan> 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);
}
}

View file

@ -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);
}
}

View file

@ -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.
* <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;
}
}

View file

@ -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.
*

View file

@ -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;
}
}