From a6c1dbe1563fc77cdc3b389ce3f1cca1cefa0aba Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Feb 2019 15:17:00 +0000 Subject: [PATCH] Implement CacheFileMetadataIndex using SQLite PiperOrigin-RevId: 232670039 --- .../exoplayer2/database/VersionTable.java | 8 +- .../cache/CacheFileMetadataIndex.java | 126 ++++++++++++++++-- .../upstream/cache/CachedContentIndex.java | 11 +- .../upstream/cache/SimpleCache.java | 9 +- .../exoplayer2/database/VersionTableTest.java | 7 +- 5 files changed, 137 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java index 5193d1836f..cdcca7a350 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java @@ -35,8 +35,10 @@ public final class VersionTable { public static final int VERSION_UNSET = -1; /** Version of tables used for offline functionality. */ public static final int FEATURE_OFFLINE = 0; - /** Version of tables used for cache functionality. */ - public static final int FEATURE_CACHE = 1; + /** Version of tables used for cache content metadata. */ + public static final int FEATURE_CACHE_CONTENT_METADATA = 1; + /** Version of tables used for cache file metadata. */ + public static final int FEATURE_CACHE_FILE_METADATA = 2; private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; @@ -54,7 +56,7 @@ public final class VersionTable { @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({FEATURE_OFFLINE, FEATURE_CACHE}) + @IntDef({FEATURE_OFFLINE, FEATURE_CACHE_CONTENT_METADATA, FEATURE_CACHE_FILE_METADATA}) private @interface Feature {} private VersionTable() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index b25eb91810..96213b4bbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -15,19 +15,72 @@ */ package com.google.android.exoplayer2.upstream.cache; -import java.util.Collections; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.VersionTable; +import java.util.HashMap; import java.util.Map; import java.util.Set; /** Maintains an index of cache file metadata. */ -/* package */ class CacheFileMetadataIndex { +/* package */ final class CacheFileMetadataIndex { + + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata"; + private static final int TABLE_VERSION = 1; + + private static final String COLUMN_NAME = "name"; + private static final String COLUMN_LENGTH = "length"; + private static final String COLUMN_LAST_ACCESS_TIMESTAMP = "last_access_timestamp"; + + private static final int COLUMN_INDEX_NAME = 0; + private static final int COLUMN_INDEX_LENGTH = 1; + private static final int COLUMN_INDEX_LAST_ACCESS_TIMESTAMP = 2; + + private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; + + private static final String[] COLUMNS = + new String[] { + COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_ACCESS_TIMESTAMP, + }; + + private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; + private static final String SQL_CREATE_TABLE = + "CREATE TABLE " + + TABLE_NAME + + " (" + + COLUMN_NAME + + " TEXT PRIMARY KEY NOT NULL," + + COLUMN_LENGTH + + " INTEGER NOT NULL," + + COLUMN_LAST_ACCESS_TIMESTAMP + + " INTEGER NOT NULL)"; + + private final DatabaseProvider databaseProvider; + + private boolean initialized; + + public CacheFileMetadataIndex(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + } /** * Returns all file metadata keyed by file name. The returned map is mutable and may be modified * by the caller. */ public Map getAll() { - return Collections.emptyMap(); + ensureInitialized(); + try (Cursor cursor = getCursor()) { + Map fileMetadata = new HashMap<>(cursor.getCount()); + while (cursor.moveToNext()) { + String name = cursor.getString(COLUMN_INDEX_NAME); + long length = cursor.getLong(COLUMN_INDEX_LENGTH); + long lastAccessTimestamp = cursor.getLong(COLUMN_INDEX_LAST_ACCESS_TIMESTAMP); + fileMetadata.put(name, new CacheFileMetadata(length, lastAccessTimestamp)); + } + return fileMetadata; + } } /** @@ -36,11 +89,15 @@ import java.util.Set; * @param name The name of the file. * @param length The file length. * @param lastAccessTimestamp The file last access timestamp. - * @return Whether the index was updated successfully. */ - public boolean set(String name, long length, long lastAccessTimestamp) { - // TODO. - return false; + public void set(String name, long length, long lastAccessTimestamp) { + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COLUMN_NAME, name); + values.put(COLUMN_LENGTH, length); + values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); } /** @@ -49,7 +106,9 @@ import java.util.Set; * @param name The name of the file whose metadata is to be removed. */ public void remove(String name) { - // TODO. + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.delete(TABLE_NAME, WHERE_NAME_EQUALS, new String[] {name}); } /** @@ -58,6 +117,55 @@ import java.util.Set; * @param names The names of the files whose metadata is to be removed. */ public void removeAll(Set names) { - // TODO. + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + for (String name : names) { + writableDatabase.delete(TABLE_NAME, WHERE_NAME_EQUALS, new String[] {name}); + } + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } + + private void ensureInitialized() { + if (initialized) { + return; + } + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + int version = + VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA); + if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, TABLE_VERSION); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.execSQL(SQL_CREATE_TABLE); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } else if (version < TABLE_VERSION) { + // There is no previous version currently. + throw new IllegalStateException(); + } + initialized = true; + } + + private Cursor getCursor() { + return databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + COLUMNS, + /* selection */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null); } } 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 0888290e1b..b22645c9d2 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 @@ -678,7 +678,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // the entire table. Currently this implementation only encrypts new and updated entries. private static final class SQLiteStorage implements Storage { - private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Cache"; + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata"; private static final int TABLE_VERSION = 1; private static final String COLUMN_ID = "id"; @@ -736,7 +736,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public boolean exists() { return file.exists() && VersionTable.getVersion( - databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE) + databaseProvider.getReadableDatabase(), + VersionTable.FEATURE_CACHE_CONTENT_METADATA) != VersionTable.VERSION_UNSET; } @@ -755,7 +756,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; try { int version = VersionTable.getVersion( - databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE); + databaseProvider.getReadableDatabase(), + VersionTable.FEATURE_CACHE_CONTENT_METADATA); if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); @@ -870,7 +872,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void initializeTable(SQLiteDatabase writableDatabase) { - VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, TABLE_VERSION); writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); writableDatabase.execSQL(SQL_CREATE_TABLE); } 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 dcdedfc32d..f66471ba1f 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 @@ -253,13 +253,12 @@ public final class SimpleCache implements Cache { String fileName = span.file.getName(); long length = span.length; long lastAccessTimestamp = System.currentTimeMillis(); - // Updating the file itself to incorporate the new last access timestamp is much slower than - // updating the file index. Hence we only update the file if we don't have a file index, or if - // updating the file index failed. - boolean updateFile; + boolean updateFile = false; if (fileIndex != null) { - updateFile = !fileIndex.set(fileName, length, lastAccessTimestamp); + fileIndex.set(fileName, length, lastAccessTimestamp); } else { + // Updating the file itself to incorporate the new last access timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index. updateFile = true; } SimpleCacheSpan newSpan = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java index a607cc01db..44961e8681 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.database; -import static com.google.android.exoplayer2.database.VersionTable.FEATURE_CACHE; +import static com.google.android.exoplayer2.database.VersionTable.FEATURE_CACHE_CONTENT_METADATA; import static com.google.android.exoplayer2.database.VersionTable.FEATURE_OFFLINE; import static com.google.common.truth.Truth.assertThat; @@ -61,8 +61,9 @@ public class VersionTableTest { VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 10); assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); - VersionTable.setVersion(writableDatabase, FEATURE_CACHE, 5); - assertThat(VersionTable.getVersion(readableDatabase, FEATURE_CACHE)).isEqualTo(5); + VersionTable.setVersion(writableDatabase, FEATURE_CACHE_CONTENT_METADATA, 5); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_CACHE_CONTENT_METADATA)) + .isEqualTo(5); assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); }