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) {
this(cacheDir, null);
public CachedContentIndex(DatabaseProvider databaseProvider) {
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 secretKey 16 byte AES key for reading and writing the cache index.
* @param databaseProvider Provides the database in which the index is stored, or {@code null} to
* 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) {
this(cacheDir, secretKey, secretKey != null);
}
/**
* Creates a CachedContentIndex which works on the index file in the given cacheDir.
*
* @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) {
public CachedContentIndex(
@Nullable DatabaseProvider databaseProvider,
@Nullable File legacyStorageDir,
@Nullable byte[] legacyStorageSecretKey,
boolean legacyStorageEncrypt,
boolean preferLegacyStorage) {
Assertions.checkState(databaseProvider != null || legacyStorageDir != null);
keyToContent = new HashMap<>();
idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
Storage atomicFileStorage =
new AtomicFileStorage(new File(cacheDir, FILE_NAME_ATOMIC), secretKey, encrypt);
// Storage sqliteStorage = new SQLiteStorage(databaseProvider);
storage = atomicFileStorage;
previousStorage = null;
Storage databaseStorage =
databaseProvider != null ? new DatabaseStorage(databaseProvider) : null;
Storage legacyStorage =
legacyStorageDir != 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. */
@ -413,7 +432,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
/** {@link Storage} implementation that uses an {@link AtomicFile}. */
private static class AtomicFileStorage implements Storage {
private static class LegacyStorage implements Storage {
private final boolean encrypt;
@Nullable private final Cipher cipher;
@ -424,7 +443,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private boolean changed;
@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;
SecretKeySpec secretKeySpec = null;
if (secretKey != null) {
@ -436,7 +455,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
throw new IllegalStateException(e); // Should never happen.
}
} else {
Assertions.checkArgument(!encrypt);
Assertions.checkState(!encrypt);
}
this.encrypt = encrypt;
this.cipher = cipher;
@ -642,7 +661,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
/** {@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 int TABLE_VERSION = 1;
@ -674,7 +693,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final DatabaseProvider databaseProvider;
private final SparseArray<CachedContent> pendingUpdates;
public SQLiteStorage(DatabaseProvider databaseProvider) {
public DatabaseStorage(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
pendingUpdates = new SparseArray<>();
}

View File

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

View File

@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.doAnswer;
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.util.Util;
import java.io.File;
@ -293,7 +294,8 @@ public class SimpleCacheTest {
/* Tests https://github.com/google/ExoPlayer/issues/3260 case. */
@Test
public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception {
CachedContentIndex index = Mockito.spy(new CachedContentIndex(cacheDir));
CachedContentIndex index =
Mockito.spy(new CachedContentIndex(TestUtil.getTestDatabaseProvider()));
SimpleCache simpleCache =
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 android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
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.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
@ -155,6 +159,23 @@ public class TestUtil {
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}.
*