mirror of
https://github.com/samsonjs/media.git
synced 2026-04-05 11:15:46 +00:00
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:
parent
992cfdecc2
commit
ff77d1e72c
16 changed files with 1295 additions and 411 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
155
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
vendored
Normal file
155
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
vendored
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
199
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
vendored
Normal file
199
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
vendored
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
265
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
vendored
Normal file
265
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
vendored
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
128
library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
vendored
Normal file
128
library/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
vendored
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue