diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index d0712ac685..85e753322a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -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 pendingUpdates; - public SQLiteStorage(DatabaseProvider databaseProvider) { + public DatabaseStorage(DatabaseProvider databaseProvider) { this.databaseProvider = databaseProvider; pendingUpdates = new SparseArray<>(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 8943a594cb..89007b00dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -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)); } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index d86e76d147..d25aa6784e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -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); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index cb980206fe..231ab3fb2b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -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 diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 41aeb56364..10c96917ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -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 diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 6140d0ac82..cf76feca49 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -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); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 3351e2db8d..facfa0d7e4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -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}. *