mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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.FakeDataSource.Builder;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
// Create a temporary folder
|
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||||
cacheDir = File.createTempFile("CacheDataSourceTest", null);
|
|
||||||
assertTrue(cacheDir.delete());
|
|
||||||
assertTrue(cacheDir.mkdir());
|
|
||||||
|
|
||||||
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||||
public void testMaxCacheFileSize() throws Exception {
|
public void testMaxCacheFileSize() throws Exception {
|
||||||
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
|
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
|
||||||
assertReadDataContentLength(cacheDataSource, false, false);
|
assertReadDataContentLength(cacheDataSource, false, false);
|
||||||
assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE),
|
File[] files = cacheDir.listFiles();
|
||||||
cacheDir.listFiles().length);
|
for (File file : files) {
|
||||||
|
if (file.getName().endsWith(SimpleCacheSpan.SUFFIX)) {
|
||||||
|
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testCacheAndRead() throws Exception {
|
public void testCacheAndRead() throws Exception {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package com.google.android.exoplayer2.upstream.cache;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
// Create a temporary folder
|
this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||||
cacheDir = File.createTempFile("SimpleCacheTest", null);
|
|
||||||
assertTrue(cacheDir.delete());
|
|
||||||
assertTrue(cacheDir.mkdir());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testCommittingOneFile() throws Exception {
|
public void testCommittingOneFile() throws Exception {
|
||||||
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
SimpleCache simpleCache = getSimpleCache();
|
||||||
|
|
||||||
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
||||||
assertFalse(cacheSpan.isCached);
|
assertFalse(cacheSpan.isCached);
|
||||||
|
|
@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSetGetLength() throws Exception {
|
public void testSetGetLength() throws Exception {
|
||||||
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
SimpleCache simpleCache = getSimpleCache();
|
||||||
|
|
||||||
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
|
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
|
||||||
assertTrue(simpleCache.setContentLength(KEY_1, 15));
|
simpleCache.setContentLength(KEY_1, 15);
|
||||||
assertEquals(15, simpleCache.getContentLength(KEY_1));
|
assertEquals(15, simpleCache.getContentLength(KEY_1));
|
||||||
|
|
||||||
simpleCache.startReadWrite(KEY_1, 0);
|
simpleCache.startReadWrite(KEY_1, 0);
|
||||||
|
|
||||||
addCache(simpleCache, 0, 15);
|
addCache(simpleCache, 0, 15);
|
||||||
|
|
||||||
assertTrue(simpleCache.setContentLength(KEY_1, 150));
|
simpleCache.setContentLength(KEY_1, 150);
|
||||||
assertEquals(150, simpleCache.getContentLength(KEY_1));
|
assertEquals(150, simpleCache.getContentLength(KEY_1));
|
||||||
|
|
||||||
addCache(simpleCache, 140, 10);
|
addCache(simpleCache, 140, 10);
|
||||||
|
|
||||||
// Try to set length shorter then the content
|
|
||||||
assertFalse(simpleCache.setContentLength(KEY_1, 15));
|
|
||||||
assertEquals("Content length should be unchanged.",
|
|
||||||
150, simpleCache.getContentLength(KEY_1));
|
|
||||||
|
|
||||||
/* TODO Enable when the length persistance is fixed
|
|
||||||
// Check if values are kept after cache is reloaded.
|
// Check if values are kept after cache is reloaded.
|
||||||
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
SimpleCache simpleCache2 = getSimpleCache();
|
||||||
assertEquals(150, simpleCache.getContentLength(KEY_1));
|
Set<String> keys = simpleCache.getKeys();
|
||||||
CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145);
|
Set<String> keys2 = simpleCache2.getKeys();
|
||||||
|
assertEquals(keys, keys2);
|
||||||
|
for (String key : keys) {
|
||||||
|
assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key));
|
||||||
|
assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key));
|
||||||
|
}
|
||||||
|
|
||||||
// Removing the last span shouldn't cause the length be change next time cache loaded
|
// Removing the last span shouldn't cause the length be change next time cache loaded
|
||||||
simpleCache.removeSpan(lastSpan);
|
SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
|
||||||
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
simpleCache2.removeSpan(lastSpan);
|
||||||
assertEquals(150, simpleCache.getContentLength(KEY_1));
|
simpleCache2 = getSimpleCache();
|
||||||
*/
|
assertEquals(150, simpleCache2.getContentLength(KEY_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimpleCache getSimpleCache() {
|
||||||
|
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
|
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.util;
|
package com.google.android.exoplayer2.util;
|
||||||
|
|
||||||
import android.test.MoreAsserts;
|
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -146,20 +145,6 @@ public class UtilTest extends TestCase {
|
||||||
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
|
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testGetHexStringByteArray() throws Exception {
|
|
||||||
assertHexStringByteArray("", new byte[] {});
|
|
||||||
assertHexStringByteArray("01", new byte[] {1});
|
|
||||||
assertHexStringByteArray("FF", new byte[] {(byte) 255});
|
|
||||||
assertHexStringByteArray("01020304", new byte[] {1, 2, 3, 4});
|
|
||||||
assertHexStringByteArray("0123456789ABCDEF",
|
|
||||||
new byte[] {1, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertHexStringByteArray(String hex, byte[] array) {
|
|
||||||
assertEquals(hex, Util.getHexString(array));
|
|
||||||
MoreAsserts.assertEquals(array, Util.getBytesFromHexString(hex));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testUnescapeInvalidFileName() {
|
public void testUnescapeInvalidFileName() {
|
||||||
assertNull(Util.unescapeFileName("%a"));
|
assertNull(Util.unescapeFileName("%a"));
|
||||||
assertNull(Util.unescapeFileName("%xyz"));
|
assertNull(Util.unescapeFileName("%xyz"));
|
||||||
|
|
|
||||||
|
|
@ -187,10 +187,8 @@ public interface Cache {
|
||||||
*
|
*
|
||||||
* @param key The cache key for the data.
|
* @param key The cache key for the data.
|
||||||
* @param length The length of the data.
|
* @param length The length of the data.
|
||||||
* @return Whether the length was set successfully. Returns false if the length conflicts with the
|
|
||||||
* existing contents of the cache.
|
|
||||||
*/
|
*/
|
||||||
boolean setContentLength(String key, long length);
|
void setContentLength(String key, long length);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content length for the given key if one set, or {@link
|
* Returns the content length for the given key if one set, or {@link
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.upstream.cache;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.util.Log;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSink;
|
import com.google.android.exoplayer2.upstream.DataSink;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
|
@ -26,7 +25,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.TeeDataSource;
|
import com.google.android.exoplayer2.upstream.TeeDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
|
|
@ -81,8 +79,6 @@ public final class CacheDataSource implements DataSource {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String TAG = "CacheDataSource";
|
|
||||||
|
|
||||||
private final Cache cache;
|
private final Cache cache;
|
||||||
private final DataSource cacheReadDataSource;
|
private final DataSource cacheReadDataSource;
|
||||||
private final DataSource cacheWriteDataSource;
|
private final DataSource cacheWriteDataSource;
|
||||||
|
|
@ -164,7 +160,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
try {
|
try {
|
||||||
uri = dataSpec.uri;
|
uri = dataSpec.uri;
|
||||||
flags = dataSpec.flags;
|
flags = dataSpec.flags;
|
||||||
key = dataSpec.key != null ? dataSpec.key : Util.sha1(uri.toString());
|
key = dataSpec.key != null ? dataSpec.key : uri.toString();
|
||||||
readPosition = dataSpec.position;
|
readPosition = dataSpec.position;
|
||||||
currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError;
|
currentRequestIgnoresCache = ignoreCacheOnError && seenCacheError;
|
||||||
if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
|
if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
|
||||||
|
|
@ -333,10 +329,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setContentLength(long length) {
|
private void setContentLength(long length) {
|
||||||
if (!cache.setContentLength(key, length)) {
|
cache.setContentLength(key, length);
|
||||||
Log.e(TAG, "cache.setContentLength(" + length + ") failed. cache.getContentLength() = "
|
|
||||||
+ cache.getContentLength(key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeCurrentSource() throws IOException {
|
private void closeCurrentSource() throws IOException {
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,12 @@
|
||||||
package com.google.android.exoplayer2.upstream.cache;
|
package com.google.android.exoplayer2.upstream.cache;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
|
* Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
|
||||||
*/
|
*/
|
||||||
public final class CacheSpan implements Comparable<CacheSpan> {
|
public class CacheSpan implements Comparable<CacheSpan> {
|
||||||
|
|
||||||
private static final String SUFFIX = ".v2.exo";
|
|
||||||
private static final Pattern CACHE_FILE_PATTERN_V1 =
|
|
||||||
Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL);
|
|
||||||
private static final Pattern CACHE_FILE_PATTERN_V2 =
|
|
||||||
Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The cache key that uniquely identifies the original stream.
|
* The cache key that uniquely identifies the original stream.
|
||||||
|
|
@ -57,64 +48,34 @@ public final class CacheSpan implements Comparable<CacheSpan> {
|
||||||
*/
|
*/
|
||||||
public final long lastAccessTimestamp;
|
public final long lastAccessTimestamp;
|
||||||
|
|
||||||
public static File getCacheFileName(File cacheDir, String key, long offset,
|
/**
|
||||||
long lastAccessTimestamp) {
|
* Creates a hole CacheSpan which isn't cached, has no last access time and no file associated.
|
||||||
return new File(cacheDir,
|
*
|
||||||
Util.escapeFileName(key) + "." + offset + "." + lastAccessTimestamp + SUFFIX);
|
* @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
|
||||||
public static CacheSpan createLookup(String key, long position) {
|
* open-ended hole.
|
||||||
return new CacheSpan(key, position, C.LENGTH_UNSET, false, C.TIME_UNSET, null);
|
*/
|
||||||
}
|
public CacheSpan(String key, long position, long length) {
|
||||||
|
this(key, position, length, C.TIME_UNSET, null);
|
||||||
public static CacheSpan createOpenHole(String key, long position) {
|
|
||||||
return new CacheSpan(key, position, C.LENGTH_UNSET, false, C.TIME_UNSET, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CacheSpan createClosedHole(String key, long position, long length) {
|
|
||||||
return new CacheSpan(key, position, length, false, C.TIME_UNSET, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a cache span from an underlying cache file.
|
* Creates a CacheSpan.
|
||||||
*
|
*
|
||||||
* @param file The cache file.
|
* @param key The cache key that uniquely identifies the original stream.
|
||||||
* @return The span, or null if the file name is not correctly formatted.
|
* @param position The position of the {@link CacheSpan} in the original stream.
|
||||||
|
* @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
|
||||||
|
* open-ended hole.
|
||||||
|
* @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) {
|
public CacheSpan(String key, long position, long length, long lastAccessTimestamp, 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) {
|
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
this.isCached = isCached;
|
this.isCached = file != null;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.lastAccessTimestamp = lastAccessTimestamp;
|
this.lastAccessTimestamp = lastAccessTimestamp;
|
||||||
}
|
}
|
||||||
|
|
@ -127,15 +88,10 @@ public final class CacheSpan implements Comparable<CacheSpan> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames the file underlying this cache span to update its last access time.
|
* Returns whether this is a hole {@link CacheSpan}.
|
||||||
*
|
|
||||||
* @return A {@link CacheSpan} representing the updated cache file.
|
|
||||||
*/
|
*/
|
||||||
public CacheSpan touch() {
|
public boolean isHoleSpan() {
|
||||||
long now = System.currentTimeMillis();
|
return !isCached;
|
||||||
File newCacheFile = getCacheFileName(file.getParentFile(), key, position, now);
|
|
||||||
file.renameTo(newCacheFile);
|
|
||||||
return createCacheEntry(key, position, now, newCacheFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
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;
|
package com.google.android.exoplayer2.upstream.cache;
|
||||||
|
|
||||||
import android.os.ConditionVariable;
|
import android.os.ConditionVariable;
|
||||||
|
|
||||||
import android.util.Pair;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.LinkedList;
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.NavigableSet;
|
import java.util.NavigableSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
@ -38,7 +35,7 @@ public final class SimpleCache implements Cache {
|
||||||
private final File cacheDir;
|
private final File cacheDir;
|
||||||
private final CacheEvictor evictor;
|
private final CacheEvictor evictor;
|
||||||
private final HashMap<String, CacheSpan> lockedSpans;
|
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 final HashMap<String, ArrayList<Listener>> listeners;
|
||||||
private long totalSpace = 0;
|
private long totalSpace = 0;
|
||||||
|
|
||||||
|
|
@ -52,7 +49,7 @@ public final class SimpleCache implements Cache {
|
||||||
this.cacheDir = cacheDir;
|
this.cacheDir = cacheDir;
|
||||||
this.evictor = evictor;
|
this.evictor = evictor;
|
||||||
this.lockedSpans = new HashMap<>();
|
this.lockedSpans = new HashMap<>();
|
||||||
this.cachedSpans = new HashMap<>();
|
this.index = new CachedContentIndex(cacheDir);
|
||||||
this.listeners = new HashMap<>();
|
this.listeners = new HashMap<>();
|
||||||
// Start cache initialization.
|
// Start cache initialization.
|
||||||
final ConditionVariable conditionVariable = new ConditionVariable();
|
final ConditionVariable conditionVariable = new ConditionVariable();
|
||||||
|
|
@ -62,6 +59,7 @@ public final class SimpleCache implements Cache {
|
||||||
synchronized (SimpleCache.this) {
|
synchronized (SimpleCache.this) {
|
||||||
conditionVariable.open();
|
conditionVariable.open();
|
||||||
initialize();
|
initialize();
|
||||||
|
SimpleCache.this.evictor.onCacheInitialized();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
|
@ -92,13 +90,13 @@ public final class SimpleCache implements Cache {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {
|
public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {
|
||||||
TreeSet<CacheSpan> spansForKey = getSpansForKey(key);
|
CachedContent cachedContent = index.get(key);
|
||||||
return spansForKey == null ? null : new TreeSet<>(spansForKey);
|
return cachedContent == null ? null : new TreeSet<CacheSpan>(cachedContent.getSpans());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized Set<String> getKeys() {
|
public synchronized Set<String> getKeys() {
|
||||||
return new HashSet<>(cachedSpans.keySet());
|
return new HashSet<>(index.getKeys());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -107,11 +105,10 @@ public final class SimpleCache implements Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized CacheSpan startReadWrite(String key, long position)
|
public synchronized SimpleCacheSpan startReadWrite(String key, long position)
|
||||||
throws InterruptedException {
|
throws InterruptedException {
|
||||||
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
|
|
||||||
while (true) {
|
while (true) {
|
||||||
CacheSpan span = startReadWriteNonBlocking(lookupSpan);
|
SimpleCacheSpan span = startReadWriteNonBlocking(key, position);
|
||||||
if (span != null) {
|
if (span != null) {
|
||||||
return span;
|
return span;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -125,25 +122,20 @@ public final class SimpleCache implements Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) {
|
public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) {
|
||||||
return startReadWriteNonBlocking(CacheSpan.createLookup(key, position));
|
SimpleCacheSpan cacheSpan = getSpan(key, position);
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) {
|
|
||||||
CacheSpan cacheSpan = getSpan(lookupSpan);
|
|
||||||
|
|
||||||
// Read case.
|
// Read case.
|
||||||
if (cacheSpan.isCached) {
|
if (cacheSpan.isCached) {
|
||||||
// Obtain a new span with updated last access timestamp.
|
// Obtain a new span with updated last access timestamp.
|
||||||
CacheSpan newCacheSpan = cacheSpan.touch();
|
SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan);
|
||||||
replaceSpan(cacheSpan, newCacheSpan);
|
|
||||||
notifySpanTouched(cacheSpan, newCacheSpan);
|
notifySpanTouched(cacheSpan, newCacheSpan);
|
||||||
return newCacheSpan;
|
return newCacheSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write case, lock available.
|
// Write case, lock available.
|
||||||
if (!lockedSpans.containsKey(lookupSpan.key)) {
|
if (!lockedSpans.containsKey(key)) {
|
||||||
lockedSpans.put(lookupSpan.key, cacheSpan);
|
lockedSpans.put(key, cacheSpan);
|
||||||
return cacheSpan;
|
return cacheSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,16 +148,17 @@ public final class SimpleCache implements Cache {
|
||||||
Assertions.checkState(lockedSpans.containsKey(key));
|
Assertions.checkState(lockedSpans.containsKey(key));
|
||||||
if (!cacheDir.exists()) {
|
if (!cacheDir.exists()) {
|
||||||
// For some reason the cache directory doesn't exist. Make a best effort to create it.
|
// For some reason the cache directory doesn't exist. Make a best effort to create it.
|
||||||
removeStaleSpans();
|
removeStaleSpansAndCachedContents();
|
||||||
cacheDir.mkdirs();
|
cacheDir.mkdirs();
|
||||||
}
|
}
|
||||||
evictor.onStartFile(this, key, position, maxLength);
|
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
|
@Override
|
||||||
public synchronized void commitFile(File file) {
|
public synchronized void commitFile(File file) {
|
||||||
CacheSpan span = CacheSpan.createCacheEntry(file);
|
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index);
|
||||||
Assertions.checkState(span != null);
|
Assertions.checkState(span != null);
|
||||||
Assertions.checkState(lockedSpans.containsKey(span.key));
|
Assertions.checkState(lockedSpans.containsKey(span.key));
|
||||||
// If the file doesn't exist, don't add it to the in-memory representation.
|
// 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);
|
Assertions.checkState((span.position + span.length) <= length);
|
||||||
}
|
}
|
||||||
addSpan(span);
|
addSpan(span);
|
||||||
|
index.store();
|
||||||
notifyAll();
|
notifyAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,40 +187,33 @@ public final class SimpleCache implements Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cache {@link CacheSpan} corresponding to the provided lookup {@link CacheSpan}.
|
* Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link
|
||||||
* <p>
|
* SimpleCacheSpan}.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* @param lookupSpan A lookup {@link CacheSpan} specifying a key and position.
|
* <p>If the lookup position is contained by an existing entry in the cache, then the returned
|
||||||
* @return The corresponding cache {@link CacheSpan}.
|
* {@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) {
|
private SimpleCacheSpan getSpan(String key, long position) {
|
||||||
String key = lookupSpan.key;
|
CachedContent cachedContent = index.get(key);
|
||||||
long offset = lookupSpan.position;
|
if (cachedContent == null) {
|
||||||
TreeSet<CacheSpan> entries = getSpansForKey(key);
|
return SimpleCacheSpan.createOpenHole(key, position);
|
||||||
if (entries == null) {
|
|
||||||
return CacheSpan.createOpenHole(key, lookupSpan.position);
|
|
||||||
}
|
}
|
||||||
CacheSpan floorSpan = entries.floor(lookupSpan);
|
while (true) {
|
||||||
if (floorSpan != null &&
|
SimpleCacheSpan span = cachedContent.getSpan(position);
|
||||||
floorSpan.position <= offset && offset < floorSpan.position + floorSpan.length) {
|
if (span.isCached && !span.file.exists()) {
|
||||||
// The lookup position is contained within floorSpan.
|
|
||||||
if (floorSpan.file.exists()) {
|
|
||||||
return floorSpan;
|
|
||||||
} else {
|
|
||||||
// The file has been deleted from under us. It's likely that other files will have been
|
// 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.
|
// deleted too, so scan the whole in-memory representation.
|
||||||
removeStaleSpans();
|
removeStaleSpansAndCachedContents();
|
||||||
return getSpan(lookupSpan);
|
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() {
|
private void initialize() {
|
||||||
if (!cacheDir.exists()) {
|
if (!cacheDir.exists()) {
|
||||||
cacheDir.mkdirs();
|
cacheDir.mkdirs();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
index.load();
|
||||||
|
|
||||||
|
SimpleCacheSpan.upgradeOldFiles(cacheDir, index);
|
||||||
|
|
||||||
File[] files = cacheDir.listFiles();
|
File[] files = cacheDir.listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
if (file.length() == 0) {
|
String name = file.getName();
|
||||||
file.delete();
|
if (!name.endsWith(SimpleCacheSpan.SUFFIX)) {
|
||||||
} else {
|
if (!name.equals(CachedContentIndex.FILE_NAME)) {
|
||||||
file = CacheSpan.upgradeIfNeeded(file);
|
file.delete(); // Delete unknown files
|
||||||
CacheSpan span = CacheSpan.createCacheEntry(file);
|
|
||||||
if (span == null) {
|
|
||||||
file.delete();
|
|
||||||
} else {
|
|
||||||
addSpan(span);
|
|
||||||
}
|
}
|
||||||
|
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.
|
* @param span The span to be added.
|
||||||
*/
|
*/
|
||||||
private void addSpan(CacheSpan span) {
|
private void addSpan(SimpleCacheSpan span) {
|
||||||
Pair<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(span.key);
|
index.add(span.key).addSpan(span);
|
||||||
TreeSet<CacheSpan> spansForKey;
|
|
||||||
if (entryForKey == null) {
|
|
||||||
spansForKey = new TreeSet<>();
|
|
||||||
setKeyValue(span.key, C.LENGTH_UNSET, spansForKey);
|
|
||||||
} else {
|
|
||||||
spansForKey = entryForKey.second;
|
|
||||||
}
|
|
||||||
spansForKey.add(span);
|
|
||||||
totalSpace += span.length;
|
totalSpace += span.length;
|
||||||
notifySpanAdded(span);
|
notifySpanAdded(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) {
|
||||||
public synchronized void removeSpan(CacheSpan span) {
|
CachedContent cachedContent = index.get(span.key);
|
||||||
TreeSet<CacheSpan> spansForKey = getSpansForKey(span.key);
|
Assertions.checkState(cachedContent.removeSpan(span));
|
||||||
totalSpace -= span.length;
|
totalSpace -= span.length;
|
||||||
Assertions.checkState(spansForKey.remove(span));
|
if (removeEmptyCachedContent && cachedContent.isEmpty()) {
|
||||||
span.file.delete();
|
index.removeEmpty(cachedContent.key);
|
||||||
if (spansForKey.isEmpty()) {
|
index.store();
|
||||||
cachedSpans.remove(span.key);
|
|
||||||
}
|
}
|
||||||
notifySpanRemoved(span);
|
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
|
* Scans all of the cached spans in the in-memory representation, removing any for which files
|
||||||
* no longer exist.
|
* no longer exist.
|
||||||
*/
|
*/
|
||||||
private void removeStaleSpans() {
|
private void removeStaleSpansAndCachedContents() {
|
||||||
Iterator<Entry<String, Pair<Long, TreeSet<CacheSpan>>>> iterator =
|
LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>();
|
||||||
cachedSpans.entrySet().iterator();
|
for (CachedContent cachedContent : index.getAll()) {
|
||||||
while (iterator.hasNext()) {
|
for (CacheSpan span : cachedContent.getSpans()) {
|
||||||
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();
|
|
||||||
if (!span.file.exists()) {
|
if (!span.file.exists()) {
|
||||||
spanIterator.remove();
|
spansToBeRemoved.add(span);
|
||||||
if (span.isCached) {
|
|
||||||
totalSpace -= span.length;
|
|
||||||
}
|
|
||||||
notifySpanRemoved(span);
|
|
||||||
} else {
|
|
||||||
isEmpty = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
private void notifySpanRemoved(CacheSpan span) {
|
||||||
|
|
@ -326,7 +313,7 @@ public final class SimpleCache implements Cache {
|
||||||
evictor.onSpanRemoved(this, span);
|
evictor.onSpanRemoved(this, span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifySpanAdded(CacheSpan span) {
|
private void notifySpanAdded(SimpleCacheSpan span) {
|
||||||
ArrayList<Listener> keyListeners = listeners.get(span.key);
|
ArrayList<Listener> keyListeners = listeners.get(span.key);
|
||||||
if (keyListeners != null) {
|
if (keyListeners != null) {
|
||||||
for (int i = keyListeners.size() - 1; i >= 0; i--) {
|
for (int i = keyListeners.size() - 1; i >= 0; i--) {
|
||||||
|
|
@ -336,7 +323,7 @@ public final class SimpleCache implements Cache {
|
||||||
evictor.onSpanAdded(this, span);
|
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);
|
ArrayList<Listener> keyListeners = listeners.get(oldSpan.key);
|
||||||
if (keyListeners != null) {
|
if (keyListeners != null) {
|
||||||
for (int i = keyListeners.size() - 1; i >= 0; i--) {
|
for (int i = keyListeners.size() - 1; i >= 0; i--) {
|
||||||
|
|
@ -348,82 +335,22 @@ public final class SimpleCache implements Cache {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized boolean isCached(String key, long position, long length) {
|
public synchronized boolean isCached(String key, long position, long length) {
|
||||||
TreeSet<CacheSpan> entries = getSpansForKey(key);
|
CachedContent cachedContent = index.get(key);
|
||||||
if (entries == null) {
|
if (cachedContent == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
|
return cachedContent.isCached(position, length);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized boolean setContentLength(String key, long length) {
|
public synchronized void setContentLength(String key, long length) {
|
||||||
Pair<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(key);
|
index.setContentLength(key, length);
|
||||||
TreeSet<CacheSpan> entries;
|
index.store();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized long getContentLength(String key) {
|
public synchronized long getContentLength(String key) {
|
||||||
Pair<Long, TreeSet<CacheSpan>> entryForKey = cachedSpans.get(key);
|
return index.getContentLength(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
@ -97,7 +94,6 @@ public final class Util {
|
||||||
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
|
Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
|
||||||
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
|
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
|
||||||
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
|
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
|
||||||
private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
|
|
||||||
|
|
||||||
private Util() {}
|
private Util() {}
|
||||||
|
|
||||||
|
|
@ -215,13 +211,14 @@ public final class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur.
|
* Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link
|
||||||
|
* java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
|
||||||
*
|
*
|
||||||
* @param outputStream The {@link OutputStream} to close.
|
* @param closeable The {@link Closeable} to close.
|
||||||
*/
|
*/
|
||||||
public static void closeQuietly(OutputStream outputStream) {
|
public static void closeQuietly(Closeable closeable) {
|
||||||
try {
|
try {
|
||||||
outputStream.close();
|
closeable.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
|
|
@ -630,21 +627,6 @@ public final class Util {
|
||||||
return data;
|
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.
|
* Returns a string with comma delimited simple names of each object's class.
|
||||||
*
|
*
|
||||||
|
|
@ -869,22 +851,6 @@ public final class Util {
|
||||||
return initialValue;
|
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.
|
* Gets the physical size of the default display, in pixels.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.testutil;
|
package com.google.android.exoplayer2.testutil;
|
||||||
|
|
||||||
import android.app.Instrumentation;
|
import android.app.Instrumentation;
|
||||||
|
import android.content.Context;
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
|
@ -313,4 +314,12 @@ public class TestUtil {
|
||||||
fileOrDirectory.delete();
|
fileOrDirectory.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates an empty folder in the application specific cache directory. */
|
||||||
|
public static File createTempFolder(Context context) throws IOException {
|
||||||
|
File tempFolder = File.createTempFile("ExoPlayerTest", null, context.getCacheDir());
|
||||||
|
Assert.assertTrue(tempFolder.delete());
|
||||||
|
Assert.assertTrue(tempFolder.mkdir());
|
||||||
|
return tempFolder;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue