Enable use of database storage in CachedContentIndex

It's not yet enabled in the SimpleCache layer, however, so
this is a no-op change.

PiperOrigin-RevId: 233064490
This commit is contained in:
olly 2019-02-08 16:27:47 +00:00 committed by Andrew Lewis
parent 9c3ac92ae8
commit 434b5a3029
7 changed files with 128 additions and 53 deletions

View File

@ -102,41 +102,60 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
/** /**
* Creates a CachedContentIndex which works on the index file in the given cacheDir. * Creates an instance supporting database storage only.
* *
* @param cacheDir Directory where the index file is kept. * @param databaseProvider Provides the database in which the index is stored.
*/ */
public CachedContentIndex(File cacheDir) { public CachedContentIndex(DatabaseProvider databaseProvider) {
this(cacheDir, null); this(
databaseProvider,
/* legacyStorageDir= */ null,
/* legacyStorageSecretKey= */ null,
/* legacyStorageEncrypt= */ false,
/* preferLegacyStorage= */ false);
} }
/** /**
* Creates a CachedContentIndex which works on the index file in the given cacheDir. * Creates an instance supporting either or both of database and legacy storage.
* *
* @param cacheDir Directory where the index file is kept. * @param databaseProvider Provides the database in which the index is stored, or {@code null} to
* @param secretKey 16 byte AES key for reading and writing the cache index. * use only legacy storage.
* @param legacyStorageDir The directory in which any legacy storage is stored, or {@code null} to
* use only database storage.
* @param legacyStorageSecretKey A 16 byte AES key for reading, and optionally writing, legacy
* storage.
* @param legacyStorageEncrypt Whether to encrypt when writing to legacy storage. Must be false if
* {@code secretKey} is null.
* @param preferLegacyStorage Whether to use prefer legacy storage if both storage types are
* enabled. This option is only useful for downgrading from database storage back to legacy
* storage.
*/ */
public CachedContentIndex(File cacheDir, @Nullable byte[] secretKey) { public CachedContentIndex(
this(cacheDir, secretKey, secretKey != null); @Nullable DatabaseProvider databaseProvider,
} @Nullable File legacyStorageDir,
@Nullable byte[] legacyStorageSecretKey,
/** boolean legacyStorageEncrypt,
* Creates a CachedContentIndex which works on the index file in the given cacheDir. boolean preferLegacyStorage) {
* Assertions.checkState(databaseProvider != null || legacyStorageDir != null);
* @param cacheDir Directory where the index file is kept.
* @param secretKey 16 byte AES key for reading, and optionally writing, the cache index.
* @param encrypt Whether the index will be encrypted when written. Must be false if {@code
* secretKey} is null.
*/
public CachedContentIndex(File cacheDir, @Nullable byte[] secretKey, boolean encrypt) {
keyToContent = new HashMap<>(); keyToContent = new HashMap<>();
idToKey = new SparseArray<>(); idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray(); removedIds = new SparseBooleanArray();
Storage atomicFileStorage = Storage databaseStorage =
new AtomicFileStorage(new File(cacheDir, FILE_NAME_ATOMIC), secretKey, encrypt); databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;
// Storage sqliteStorage = new SQLiteStorage(databaseProvider); Storage legacyStorage =
storage = atomicFileStorage; legacyStorageDir != null
previousStorage = null; ? new LegacyStorage(
new File(legacyStorageDir, FILE_NAME_ATOMIC),
legacyStorageSecretKey,
legacyStorageEncrypt)
: null;
if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) {
storage = legacyStorage;
previousStorage = databaseStorage;
} else {
storage = databaseStorage;
previousStorage = legacyStorage;
}
} }
/** Loads the index file. */ /** Loads the index file. */
@ -413,7 +432,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
/** {@link Storage} implementation that uses an {@link AtomicFile}. */ /** {@link Storage} implementation that uses an {@link AtomicFile}. */
private static class AtomicFileStorage implements Storage { private static class LegacyStorage implements Storage {
private final boolean encrypt; private final boolean encrypt;
@Nullable private final Cipher cipher; @Nullable private final Cipher cipher;
@ -424,7 +443,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private boolean changed; private boolean changed;
@Nullable private ReusableBufferedOutputStream bufferedOutputStream; @Nullable private ReusableBufferedOutputStream bufferedOutputStream;
public AtomicFileStorage(File file, @Nullable byte[] secretKey, boolean encrypt) { public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) {
Cipher cipher = null; Cipher cipher = null;
SecretKeySpec secretKeySpec = null; SecretKeySpec secretKeySpec = null;
if (secretKey != null) { if (secretKey != null) {
@ -436,7 +455,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
throw new IllegalStateException(e); // Should never happen. throw new IllegalStateException(e); // Should never happen.
} }
} else { } else {
Assertions.checkArgument(!encrypt); Assertions.checkState(!encrypt);
} }
this.encrypt = encrypt; this.encrypt = encrypt;
this.cipher = cipher; this.cipher = cipher;
@ -642,7 +661,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
/** {@link Storage} implementation that uses an SQL database. */ /** {@link Storage} implementation that uses an SQL database. */
private static final class SQLiteStorage implements Storage { private static final class DatabaseStorage implements Storage {
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata"; private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata";
private static final int TABLE_VERSION = 1; private static final int TABLE_VERSION = 1;
@ -674,7 +693,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final DatabaseProvider databaseProvider; private final DatabaseProvider databaseProvider;
private final SparseArray<CachedContent> pendingUpdates; private final SparseArray<CachedContent> pendingUpdates;
public SQLiteStorage(DatabaseProvider databaseProvider) { public DatabaseStorage(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
pendingUpdates = new SparseArray<>(); pendingUpdates = new SparseArray<>();
} }

View File

@ -125,7 +125,15 @@ public final class SimpleCache implements Cache {
* secretKey} is null. * secretKey} is null.
*/ */
public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) {
this(cacheDir, evictor, new CachedContentIndex(cacheDir, secretKey, encrypt)); this(
cacheDir,
evictor,
new CachedContentIndex(
/* databaseProvider= */ null,
cacheDir,
secretKey,
encrypt,
/* preferLegacyStorage= */ true));
} }
/** /**

View File

@ -19,8 +19,10 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -73,14 +75,12 @@ public class CachedContentIndexTest {
0, 0, 0, 0, 0, 0, 10, 0, // original_content_length 0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
0x12, 0x15, 0x66, (byte) 0x8A // hashcode_of_CachedContent_array 0x12, 0x15, 0x66, (byte) 0x8A // hashcode_of_CachedContent_array
}; };
private CachedContentIndex index;
private File cacheDir; private File cacheDir;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
cacheDir = cacheDir =
Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
} }
@After @After
@ -94,6 +94,8 @@ public class CachedContentIndexTest {
final String key2 = "key2"; final String key2 = "key2";
final String key3 = "key3"; final String key3 = "key3";
CachedContentIndex index = newInstance();
// Add two CachedContents with add methods // Add two CachedContents with add methods
CachedContent cachedContent1 = index.getOrAdd(key1); CachedContent cachedContent1 = index.getOrAdd(key1);
CachedContent cachedContent2 = index.getOrAdd(key2); CachedContent cachedContent2 = index.getOrAdd(key2);
@ -145,12 +147,14 @@ public class CachedContentIndexTest {
} }
@Test @Test
public void testStoreAndLoad() throws Exception { public void testLegacyStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance());
} }
@Test @Test
public void testLoadV1() throws Exception { public void testLegacyLoadV1() throws Exception {
CachedContentIndex index = newLegacyInstance();
FileOutputStream fos = FileOutputStream fos =
new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC)); new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));
fos.write(testIndexV1File); fos.write(testIndexV1File);
@ -169,7 +173,9 @@ public class CachedContentIndexTest {
} }
@Test @Test
public void testLoadV2() throws Exception { public void testLegacyLoadV2() throws Exception {
CachedContentIndex index = newLegacyInstance();
FileOutputStream fos = FileOutputStream fos =
new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC)); new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));
fos.write(testIndexV2File); fos.write(testIndexV2File);
@ -190,6 +196,7 @@ public class CachedContentIndexTest {
@Test @Test
public void testAssignIdForKeyAndGetKeyForId() { public void testAssignIdForKeyAndGetKeyForId() {
CachedContentIndex index = newInstance();
final String key1 = "key1"; final String key1 = "key1";
final String key2 = "key2"; final String key2 = "key2";
int id1 = index.assignIdForKey(key1); int id1 = index.assignIdForKey(key1);
@ -214,12 +221,11 @@ public class CachedContentIndexTest {
} }
@Test @Test
public void testEncryption() throws Exception { public void testLegacyEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
assertStoredAndLoadedEqual( assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));
new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key));
// Rename the index file from the test above // Rename the index file from the test above
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC); File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC);
@ -227,8 +233,7 @@ public class CachedContentIndexTest {
assertThat(file1.renameTo(file2)).isTrue(); assertThat(file1.renameTo(file2)).isTrue();
// Write a new index file // Write a new index file
assertStoredAndLoadedEqual( assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));
new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key));
assertThat(file1.length()).isEqualTo(file2.length()); assertThat(file1.length()).isEqualTo(file2.length());
// Assert file content is different // Assert file content is different
@ -240,8 +245,7 @@ public class CachedContentIndexTest {
boolean threw = false; boolean threw = false;
try { try {
assertStoredAndLoadedEqual( assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key2));
new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key2));
} catch (AssertionError e) { } catch (AssertionError e) {
threw = true; threw = true;
} }
@ -250,8 +254,7 @@ public class CachedContentIndexTest {
.isTrue(); .isTrue();
try { try {
assertStoredAndLoadedEqual( assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance());
new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir));
} catch (AssertionError e) { } catch (AssertionError e) {
threw = true; threw = true;
} }
@ -260,18 +263,18 @@ public class CachedContentIndexTest {
.isTrue(); .isTrue();
// Non encrypted index file can be read even when encryption key provided. // Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual( assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance(key));
new CachedContentIndex(cacheDir), new CachedContentIndex(cacheDir, key));
// Test multiple store() calls // Test multiple store() calls
CachedContentIndex index = new CachedContentIndex(cacheDir, key); CachedContentIndex index = newLegacyInstance(key);
index.getOrAdd("key3"); index.getOrAdd("key3");
index.store(); index.store();
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); assertStoredAndLoadedEqual(index, newLegacyInstance(key));
} }
@Test @Test
public void testRemoveEmptyNotLockedCachedContent() { public void testRemoveEmptyNotLockedCachedContent() {
CachedContentIndex index = newInstance();
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
index.maybeRemove(cachedContent.key); index.maybeRemove(cachedContent.key);
@ -281,6 +284,8 @@ public class CachedContentIndexTest {
@Test @Test
public void testCantRemoveNotEmptyCachedContent() throws Exception { public void testCantRemoveNotEmptyCachedContent() throws Exception {
CachedContentIndex index = newInstance();
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
long cacheFileLength = 20; long cacheFileLength = 20;
File cacheFile = File cacheFile =
@ -300,6 +305,7 @@ public class CachedContentIndexTest {
@Test @Test
public void testCantRemoveLockedCachedContent() { public void testCantRemoveLockedCachedContent() {
CachedContentIndex index = newInstance();
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true); cachedContent.setLocked(true);
@ -327,4 +333,21 @@ public class CachedContentIndexTest {
assertThat(index2.get(key)).isEqualTo(index.get(key)); assertThat(index2.get(key)).isEqualTo(index.get(key));
} }
} }
private CachedContentIndex newInstance() {
return new CachedContentIndex(TestUtil.getTestDatabaseProvider());
}
private CachedContentIndex newLegacyInstance() {
return newLegacyInstance(null);
}
private CachedContentIndex newLegacyInstance(@Nullable byte[] key) {
return new CachedContentIndex(
/* databaseProvider= */ null,
cacheDir,
/* legacyStorageSecretKey= */ key,
/* legacyStorageEncrypt= */ key != null,
/* preferLegacyStorage= */ true);
}
} }

View File

@ -21,6 +21,7 @@ import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -64,7 +65,7 @@ public final class CachedRegionTrackerTest {
when(cache.addListener(anyString(), any(Cache.Listener.class))).thenReturn(new TreeSet<>()); when(cache.addListener(anyString(), any(Cache.Listener.class))).thenReturn(new TreeSet<>());
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(TestUtil.getTestDatabaseProvider());
} }
@After @After

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -51,7 +52,7 @@ public class SimpleCacheSpanTest {
public void setUp() throws Exception { public void setUp() throws Exception {
cacheDir = cacheDir =
Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(TestUtil.getTestDatabaseProvider());
} }
@After @After

View File

@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
@ -293,7 +294,8 @@ public class SimpleCacheTest {
/* Tests https://github.com/google/ExoPlayer/issues/3260 case. */ /* Tests https://github.com/google/ExoPlayer/issues/3260 case. */
@Test @Test
public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception { public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception {
CachedContentIndex index = Mockito.spy(new CachedContentIndex(cacheDir)); CachedContentIndex index =
Mockito.spy(new CachedContentIndex(TestUtil.getTestDatabaseProvider()));
SimpleCache simpleCache = SimpleCache simpleCache =
new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(20), index); new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(20), index);

View File

@ -19,11 +19,15 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.DefaultDatabaseProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
@ -155,6 +159,23 @@ public class TestUtil {
return BitmapFactory.decodeStream(getInputStream(context, fileName)); return BitmapFactory.decodeStream(getInputStream(context, fileName));
} }
public static DatabaseProvider getTestDatabaseProvider() {
// Provides an in-memory database.
return new DefaultDatabaseProvider(
new SQLiteOpenHelper(
/* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) {
@Override
public void onCreate(SQLiteDatabase db) {
// Do nothing.
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Do nothing.
}
});
}
/** /**
* Asserts that data read from a {@link DataSource} matches {@code expected}. * Asserts that data read from a {@link DataSource} matches {@code expected}.
* *