Support multiple instances of database features

- Use Cache UID for CacheContentIndex and CacheFileMetadataIndex. This
  enables SD card swapping for a single device.
- I'm hopeful of finding a way to get the Cache UID to DefaultDownloadIndex
  so we can do the same there.

PiperOrigin-RevId: 234662753
This commit is contained in:
olly 2019-02-19 21:28:40 +00:00 committed by Andrew Lewis
parent 5b891a4c1f
commit 2685b8bd90
8 changed files with 188 additions and 127 deletions

View File

@ -31,7 +31,7 @@ import java.lang.annotation.RetentionPolicy;
*/ */
public final class VersionTable { public final class VersionTable {
/** Returned by {@link #getVersion(SQLiteDatabase, int)} if the version is unset. */ /** Returned by {@link #getVersion(SQLiteDatabase, int, String)} if the version is unset. */
public static final int VERSION_UNSET = -1; public static final int VERSION_UNSET = -1;
/** Version of tables used for offline functionality. */ /** Version of tables used for offline functionality. */
public static final int FEATURE_OFFLINE = 0; public static final int FEATURE_OFFLINE = 0;
@ -43,18 +43,26 @@ public final class VersionTable {
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions";
private static final String COLUMN_FEATURE = "feature"; private static final String COLUMN_FEATURE = "feature";
private static final String COLUMN_INSTANCE_UID = "instance_uid";
private static final String COLUMN_VERSION = "version"; private static final String COLUMN_VERSION = "version";
private static final String WHERE_FEATURE_EQUALS = COLUMN_FEATURE + " = ?"; private static final String WHERE_FEATURE_AND_INSTANCE_UID_EQUALS =
COLUMN_FEATURE + " = ? AND " + COLUMN_INSTANCE_UID + " = ?";
private static final String PRIMARY_KEY =
"PRIMARY KEY (" + COLUMN_FEATURE + ", " + COLUMN_INSTANCE_UID + ")";
private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS = private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS =
"CREATE TABLE IF NOT EXISTS " "CREATE TABLE IF NOT EXISTS "
+ TABLE_NAME + TABLE_NAME
+ " (" + " ("
+ COLUMN_FEATURE + COLUMN_FEATURE
+ " INTEGER PRIMARY KEY NOT NULL," + " INTEGER NOT NULL,"
+ COLUMN_INSTANCE_UID
+ " TEXT NOT NULL,"
+ COLUMN_VERSION + COLUMN_VERSION
+ " INTEGER NOT NULL)"; + " INTEGER NOT NULL,"
+ PRIMARY_KEY
+ ")";
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -64,42 +72,51 @@ public final class VersionTable {
private VersionTable() {} private VersionTable() {}
/** /**
* Sets the version of the specified feature. * Sets the version of a specified instance of a specified feature.
* *
* @param writableDatabase The database to update. * @param writableDatabase The database to update.
* @param feature The feature. * @param feature The feature.
* @param instanceUid The unique identifier of the instance of the feature.
* @param version The version. * @param version The version.
*/ */
public static void setVersion( public static void setVersion(
SQLiteDatabase writableDatabase, @Feature int feature, int version) { SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version) {
writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS); writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COLUMN_FEATURE, feature); values.put(COLUMN_FEATURE, feature);
values.put(COLUMN_INSTANCE_UID, instanceUid);
values.put(COLUMN_VERSION, version); values.put(COLUMN_VERSION, version);
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
} }
/** /**
* Removes the version of the specified feature. * Removes the version of a specified instance of a feature.
* *
* @param writableDatabase The database to update. * @param writableDatabase The database to update.
* @param feature The feature. * @param feature The feature.
* @param instanceUid The unique identifier of the instance of the feature.
*/ */
public static void removeVersion(SQLiteDatabase writableDatabase, @Feature int feature) { public static void removeVersion(
SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid) {
if (!tableExists(writableDatabase, TABLE_NAME)) { if (!tableExists(writableDatabase, TABLE_NAME)) {
return; return;
} }
writableDatabase.delete(TABLE_NAME, WHERE_FEATURE_EQUALS, featureArgument(feature)); writableDatabase.delete(
TABLE_NAME,
WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,
featureAndInstanceUidArguments(feature, instanceUid));
} }
/** /**
* Returns the version of the specified feature, or {@link #VERSION_UNSET} if no version * Returns the version of a specified instance of a feature, or {@link #VERSION_UNSET} if no
* information is available. * version is set.
* *
* @param database The database to query. * @param database The database to query.
* @param feature The feature. * @param feature The feature.
* @param instanceUid The unique identifier of the instance of the feature.
* @return The version, or {@link #VERSION_UNSET} if no version is set.
*/ */
public static int getVersion(SQLiteDatabase database, @Feature int feature) { public static int getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid) {
if (!tableExists(database, TABLE_NAME)) { if (!tableExists(database, TABLE_NAME)) {
return VERSION_UNSET; return VERSION_UNSET;
} }
@ -107,8 +124,8 @@ public final class VersionTable {
database.query( database.query(
TABLE_NAME, TABLE_NAME,
new String[] {COLUMN_VERSION}, new String[] {COLUMN_VERSION},
WHERE_FEATURE_EQUALS, WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,
featureArgument(feature), featureAndInstanceUidArguments(feature, instanceUid),
/* groupBy= */ null, /* groupBy= */ null,
/* having= */ null, /* having= */ null,
/* orderBy= */ null)) { /* orderBy= */ null)) {
@ -128,7 +145,7 @@ public final class VersionTable {
return count > 0; return count > 0;
} }
private static String[] featureArgument(int feature) { private static String[] featureAndInstanceUidArguments(int feature, String instance) {
return new String[] {Integer.toString(feature)}; return new String[] {Integer.toString(feature), instance};
} }
} }

View File

@ -37,6 +37,8 @@ public final class DefaultDownloadIndex implements DownloadIndex {
@VisibleForTesting @VisibleForTesting
/* package */ static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads"; /* package */ static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads";
// TODO: Support multiple instances. Probably using the underlying cache UID.
@VisibleForTesting /* package */ static final String INSTANCE_UID = "singleton";
@VisibleForTesting /* package */ static final int TABLE_VERSION = 1; @VisibleForTesting /* package */ static final int TABLE_VERSION = 1;
private static final String COLUMN_ID = "id"; private static final String COLUMN_ID = "id";
@ -218,12 +220,14 @@ public final class DefaultDownloadIndex implements DownloadIndex {
return; return;
} }
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE); int version =
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID);
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction(); writableDatabase.beginTransaction();
try { try {
VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, TABLE_VERSION); VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID, TABLE_VERSION);
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS);
writableDatabase.execSQL(SQL_CREATE_TABLE); writableDatabase.execSQL(SQL_CREATE_TABLE);
writableDatabase.setTransactionSuccessful(); writableDatabase.setTransactionSuccessful();

View File

@ -20,14 +20,16 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.VersionTable; import com.google.android.exoplayer2.database.VersionTable;
import com.google.android.exoplayer2.util.Assertions;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Maintains an index of cache file metadata. */ /** Maintains an index of cache file metadata. */
/* package */ final class CacheFileMetadataIndex { /* package */ final class CacheFileMetadataIndex {
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata"; private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata";
private static final int TABLE_VERSION = 1; private static final int TABLE_VERSION = 1;
private static final String COLUMN_NAME = "name"; private static final String COLUMN_NAME = "name";
@ -44,12 +46,8 @@ import java.util.Set;
new String[] { new String[] {
COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_ACCESS_TIMESTAMP, COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_ACCESS_TIMESTAMP,
}; };
private static final String TABLE_SCHEMA =
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 + COLUMN_NAME
+ " TEXT PRIMARY KEY NOT NULL," + " TEXT PRIMARY KEY NOT NULL,"
+ COLUMN_LENGTH + COLUMN_LENGTH
@ -59,18 +57,42 @@ import java.util.Set;
private final DatabaseProvider databaseProvider; private final DatabaseProvider databaseProvider;
private boolean initialized; @MonotonicNonNull private String tableName;
public CacheFileMetadataIndex(DatabaseProvider databaseProvider) { public CacheFileMetadataIndex(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
} }
/** Initializes the index for the given cache UID. */
public void initialize(long uid) {
String hexUid = Long.toHexString(uid);
tableName = TABLE_PREFIX + hexUid;
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
int version =
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION);
writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName);
writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
}
} else if (version < TABLE_VERSION) {
// There is no previous version currently.
throw new IllegalStateException();
}
}
/** /**
* Returns all file metadata keyed by file name. The returned map is mutable and may be modified * Returns all file metadata keyed by file name. The returned map is mutable and may be modified
* by the caller. * by the caller.
*/ */
public Map<String, CacheFileMetadata> getAll() { public Map<String, CacheFileMetadata> getAll() {
ensureInitialized();
try (Cursor cursor = getCursor()) { try (Cursor cursor = getCursor()) {
Map<String, CacheFileMetadata> fileMetadata = new HashMap<>(cursor.getCount()); Map<String, CacheFileMetadata> fileMetadata = new HashMap<>(cursor.getCount());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
@ -91,13 +113,13 @@ import java.util.Set;
* @param lastAccessTimestamp The file last access timestamp. * @param lastAccessTimestamp The file last access timestamp.
*/ */
public void set(String name, long length, long lastAccessTimestamp) { public void set(String name, long length, long lastAccessTimestamp) {
ensureInitialized(); Assertions.checkNotNull(tableName);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COLUMN_NAME, name); values.put(COLUMN_NAME, name);
values.put(COLUMN_LENGTH, length); values.put(COLUMN_LENGTH, length);
values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp); values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp);
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); writableDatabase.replace(tableName, /* nullColumnHack= */ null, values);
} }
/** /**
@ -106,9 +128,9 @@ import java.util.Set;
* @param name The name of the file whose metadata is to be removed. * @param name The name of the file whose metadata is to be removed.
*/ */
public void remove(String name) { public void remove(String name) {
ensureInitialized(); Assertions.checkNotNull(tableName);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.delete(TABLE_NAME, WHERE_NAME_EQUALS, new String[] {name}); writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
} }
/** /**
@ -117,12 +139,12 @@ import java.util.Set;
* @param names The names of the files whose metadata is to be removed. * @param names The names of the files whose metadata is to be removed.
*/ */
public void removeAll(Set<String> names) { public void removeAll(Set<String> names) {
ensureInitialized(); Assertions.checkNotNull(tableName);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction(); writableDatabase.beginTransaction();
try { try {
for (String name : names) { for (String name : names) {
writableDatabase.delete(TABLE_NAME, WHERE_NAME_EQUALS, new String[] {name}); writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
} }
writableDatabase.setTransactionSuccessful(); writableDatabase.setTransactionSuccessful();
} finally { } finally {
@ -130,37 +152,12 @@ import java.util.Set;
} }
} }
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() { private Cursor getCursor() {
Assertions.checkNotNull(tableName);
return databaseProvider return databaseProvider
.getReadableDatabase() .getReadableDatabase()
.query( .query(
TABLE_NAME, tableName,
COLUMNS, COLUMNS,
/* selection */ null, /* selection */ null,
/* selectionArgs= */ null, /* selectionArgs= */ null,

View File

@ -159,8 +159,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
} }
/** Loads the index file. */ /**
public void load() { * Loads the index file for the given cache UID.
*
* @param uid The UID of the cache whose index is to be loaded.
*/
public void initialize(long uid) {
storage.initialize(uid);
if (previousStorage != null) {
previousStorage.initialize(uid);
}
if (!storage.exists() && previousStorage != null && previousStorage.exists()) { if (!storage.exists() && previousStorage != null && previousStorage.exists()) {
// Copy from previous storage into current storage. // Copy from previous storage into current storage.
loadFrom(previousStorage); loadFrom(previousStorage);
@ -383,6 +391,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Interface for the persistent index. */ /** Interface for the persistent index. */
private interface Storage { private interface Storage {
/** Initializes the storage for the given cache UID. */
void initialize(long uid);
/** Returns whether the persisted index exists. */ /** Returns whether the persisted index exists. */
boolean exists(); boolean exists();
@ -409,9 +420,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
void storeFully(HashMap<String, CachedContent> content) throws CacheException; void storeFully(HashMap<String, CachedContent> content) throws CacheException;
/** /**
* Ensures incremental changes to the index since the last {@link #load()} or {@link * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last
* #storeFully(HashMap)} are persisted. The storage will have been notified of all such changes * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such
* via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}. * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}.
* *
* @param content The key to content map to persist. * @param content The key to content map to persist.
* @throws CacheException If an error occurs persisting the index. * @throws CacheException If an error occurs persisting the index.
@ -457,7 +468,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.checkState(!encrypt); Assertions.checkArgument(!encrypt);
} }
this.encrypt = encrypt; this.encrypt = encrypt;
this.cipher = cipher; this.cipher = cipher;
@ -466,6 +477,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
atomicFile = new AtomicFile(file); atomicFile = new AtomicFile(file);
} }
@Override
public void initialize(long uid) {
// Do nothing. Legacy storage uses a separate file for each cache.
}
@Override @Override
public boolean exists() { public boolean exists() {
return atomicFile.exists(); return atomicFile.exists();
@ -665,7 +681,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 DatabaseStorage implements Storage { private static final class DatabaseStorage implements Storage {
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata"; private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheIndex";
private static final int TABLE_VERSION = 1; private static final int TABLE_VERSION = 1;
private static final String COLUMN_ID = "id"; private static final String COLUMN_ID = "id";
@ -679,12 +695,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?";
private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA}; private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA};
private static final String TABLE_SCHEMA =
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_ID + COLUMN_ID
+ " INTEGER PRIMARY KEY NOT NULL," + " INTEGER PRIMARY KEY NOT NULL,"
+ COLUMN_KEY + COLUMN_KEY
@ -695,15 +707,26 @@ 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;
private String hexUid;
private String tableName;
public DatabaseStorage(DatabaseProvider databaseProvider) { public DatabaseStorage(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
pendingUpdates = new SparseArray<>(); pendingUpdates = new SparseArray<>();
} }
@Override
public void initialize(long uid) {
hexUid = Long.toHexString(uid);
tableName = TABLE_PREFIX + hexUid;
}
@Override @Override
public boolean exists() { public boolean exists() {
return VersionTable.getVersion( return VersionTable.getVersion(
databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE_CONTENT_METADATA) databaseProvider.getReadableDatabase(),
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
hexUid)
!= VersionTable.VERSION_UNSET; != VersionTable.VERSION_UNSET;
} }
@ -712,8 +735,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction(); writableDatabase.beginTransaction();
try { try {
VersionTable.removeVersion(writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA); VersionTable.removeVersion(
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid);
dropTable(writableDatabase);
writableDatabase.setTransactionSuccessful(); writableDatabase.setTransactionSuccessful();
} finally { } finally {
writableDatabase.endTransaction(); writableDatabase.endTransaction();
@ -728,7 +752,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int version = int version =
VersionTable.getVersion( VersionTable.getVersion(
databaseProvider.getReadableDatabase(), databaseProvider.getReadableDatabase(),
VersionTable.FEATURE_CACHE_CONTENT_METADATA); VersionTable.FEATURE_CACHE_CONTENT_METADATA,
hexUid);
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction(); writableDatabase.beginTransaction();
@ -821,7 +846,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return databaseProvider return databaseProvider
.getReadableDatabase() .getReadableDatabase()
.query( .query(
TABLE_NAME, tableName,
COLUMNS, COLUMNS,
/* selection= */ null, /* selection= */ null,
/* selectionArgs= */ null, /* selectionArgs= */ null,
@ -832,13 +857,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private void initializeTable(SQLiteDatabase writableDatabase) { private void initializeTable(SQLiteDatabase writableDatabase) {
VersionTable.setVersion( VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, TABLE_VERSION); writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION);
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); dropTable(writableDatabase);
writableDatabase.execSQL(SQL_CREATE_TABLE); writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
}
private void dropTable(SQLiteDatabase writableDatabase) {
writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName);
} }
private void deleteRow(SQLiteDatabase writableDatabase, int key) { private void deleteRow(SQLiteDatabase writableDatabase, int key) {
writableDatabase.delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {Integer.toString(key)}); writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
} }
private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent) private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)
@ -851,7 +880,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
values.put(COLUMN_ID, cachedContent.id); values.put(COLUMN_ID, cachedContent.id);
values.put(COLUMN_KEY, cachedContent.key); values.put(COLUMN_KEY, cachedContent.key);
values.put(COLUMN_METADATA, data); values.put(COLUMN_METADATA, data);
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); writableDatabase.replace(tableName, /* nullColumnHack= */ null, values);
} }
} }
} }

View File

@ -254,8 +254,8 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized @Nullable SimpleCacheSpan startReadWriteNonBlocking(String key, long position) public synchronized @Nullable SimpleCacheSpan startReadWriteNonBlocking(
throws CacheException { String key, long position) {
Assertions.checkState(!released); Assertions.checkState(!released);
SimpleCacheSpan span = getSpan(key, position); SimpleCacheSpan span = getSpan(key, position);
@ -290,7 +290,7 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized File startFile(String key, long position, long length) throws CacheException { public synchronized File startFile(String key, long position, long length) {
Assertions.checkState(!released); Assertions.checkState(!released);
CachedContent cachedContent = contentIndex.get(key); CachedContent cachedContent = contentIndex.get(key);
Assertions.checkNotNull(cachedContent); Assertions.checkNotNull(cachedContent);
@ -399,7 +399,7 @@ public final class SimpleCache implements Cache {
* @param position The position of the span being requested. * @param position The position of the span being requested.
* @return The corresponding cache {@link SimpleCacheSpan}. * @return The corresponding cache {@link SimpleCacheSpan}.
*/ */
private SimpleCacheSpan getSpan(String key, long position) throws CacheException { private SimpleCacheSpan getSpan(String key, long position) {
CachedContent cachedContent = contentIndex.get(key); CachedContent cachedContent = contentIndex.get(key);
if (cachedContent == null) { if (cachedContent == null) {
return SimpleCacheSpan.createOpenHole(key, position); return SimpleCacheSpan.createOpenHole(key, position);
@ -432,9 +432,9 @@ public final class SimpleCache implements Cache {
// TODO: Decide how to handle this. // TODO: Decide how to handle this.
} }
// TODO: Pass the UID to the index, and use it. contentIndex.initialize(uid);
contentIndex.load();
if (fileIndex != null) { if (fileIndex != null) {
fileIndex.initialize(uid);
Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll(); Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();
loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata); loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);
fileIndex.removeAll(fileMetadata.keySet()); fileIndex.removeAll(fileMetadata.keySet());

View File

@ -15,77 +15,86 @@
*/ */
package com.google.android.exoplayer2.database; package com.google.android.exoplayer2.database;
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; import static com.google.common.truth.Truth.assertThat;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import org.junit.After; import com.google.android.exoplayer2.testutil.TestUtil;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Unit tests for {@link VersionTable}. */ /** Unit tests for {@link VersionTable}. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class VersionTableTest { public class VersionTableTest {
private ExoDatabaseProvider databaseProvider; private static final int FEATURE_1 = 1;
private SQLiteDatabase readableDatabase; private static final int FEATURE_2 = 2;
private SQLiteDatabase writableDatabase; private static final String INSTANCE_1 = "1";
private static final String INSTANCE_2 = "2";
private DatabaseProvider databaseProvider;
private SQLiteDatabase database;
@Before @Before
public void setUp() { public void setUp() {
databaseProvider = new ExoDatabaseProvider(RuntimeEnvironment.application); databaseProvider = TestUtil.getTestDatabaseProvider();
readableDatabase = databaseProvider.getReadableDatabase(); database = databaseProvider.getWritableDatabase();
writableDatabase = databaseProvider.getWritableDatabase();
}
@After
public void tearDown() {
databaseProvider.close();
} }
@Test @Test
public void getVersion_nonExistingTable_returnsVersionUnset() { public void getVersion_unsetFeature_returnsVersionUnset() {
int version = VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE); int version = VersionTable.getVersion(database, FEATURE_1, INSTANCE_1);
assertThat(version).isEqualTo(VersionTable.VERSION_UNSET);
}
@Test
public void getVersion_unsetVersion_returnsVersionUnset() {
VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);
int version = VersionTable.getVersion(database, FEATURE_1, INSTANCE_2);
assertThat(version).isEqualTo(VersionTable.VERSION_UNSET); assertThat(version).isEqualTo(VersionTable.VERSION_UNSET);
} }
@Test @Test
public void getVersion_returnsSetVersion() { public void getVersion_returnsSetVersion() {
VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 1); VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(1); assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(1);
VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 10); VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 2);
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(2);
VersionTable.setVersion(writableDatabase, FEATURE_CACHE_CONTENT_METADATA, 5); VersionTable.setVersion(database, FEATURE_2, INSTANCE_1, 3);
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_CACHE_CONTENT_METADATA)) assertThat(VersionTable.getVersion(database, FEATURE_2, INSTANCE_1)).isEqualTo(3);
.isEqualTo(5); assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(2);
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10);
VersionTable.setVersion(database, FEATURE_2, INSTANCE_2, 4);
assertThat(VersionTable.getVersion(database, FEATURE_2, INSTANCE_2)).isEqualTo(4);
assertThat(VersionTable.getVersion(database, FEATURE_2, INSTANCE_1)).isEqualTo(3);
assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(2);
} }
@Test @Test
public void removeVersion_removesSetVersion() { public void removeVersion_removesSetVersion() {
VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 1); VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(1); VersionTable.setVersion(database, FEATURE_1, INSTANCE_2, 2);
assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(1);
assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_2)).isEqualTo(2);
VersionTable.removeVersion(writableDatabase, FEATURE_OFFLINE); VersionTable.removeVersion(database, FEATURE_1, INSTANCE_1);
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)) assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1))
.isEqualTo(VersionTable.VERSION_UNSET); .isEqualTo(VersionTable.VERSION_UNSET);
assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_2)).isEqualTo(2);
} }
@Test @Test
public void doesTableExist_nonExistingTable_returnsFalse() { public void doesTableExist_nonExistingTable_returnsFalse() {
assertThat(VersionTable.tableExists(readableDatabase, "NonExistingTable")).isFalse(); assertThat(VersionTable.tableExists(database, "NonExistingTable")).isFalse();
} }
@Test @Test
public void doesTableExist_existingTable_returnsTrue() { public void doesTableExist_existingTable_returnsTrue() {
String table = "TestTable"; String table = "TestTable";
databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + table + " (dummy INTEGER)"); databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + table + " (dummy INTEGER)");
assertThat(VersionTable.tableExists(readableDatabase, table)).isTrue(); assertThat(VersionTable.tableExists(database, table)).isTrue();
} }
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.INSTANCE_UID;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
@ -180,12 +181,14 @@ public class DefaultDownloadIndexTest {
@Test @Test
public void putDownloadState_setsVersion() { public void putDownloadState_setsVersion() {
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE)) assertThat(
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID))
.isEqualTo(VersionTable.VERSION_UNSET); .isEqualTo(VersionTable.VERSION_UNSET);
downloadIndex.putDownloadState(new DownloadStateBuilder("id1").build()); downloadIndex.putDownloadState(new DownloadStateBuilder("id1").build());
assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE)) assertThat(
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID))
.isEqualTo(DefaultDownloadIndex.TABLE_VERSION); .isEqualTo(DefaultDownloadIndex.TABLE_VERSION);
} }
@ -198,14 +201,16 @@ public class DefaultDownloadIndexTest {
cursor.close(); cursor.close();
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, Integer.MAX_VALUE); VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID, Integer.MAX_VALUE);
downloadIndex = new DefaultDownloadIndex(databaseProvider); downloadIndex = new DefaultDownloadIndex(databaseProvider);
cursor = downloadIndex.getDownloadStates(); cursor = downloadIndex.getDownloadStates();
assertThat(cursor.getCount()).isEqualTo(0); assertThat(cursor.getCount()).isEqualTo(0);
cursor.close(); cursor.close();
assertThat(VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE)) assertThat(
VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID))
.isEqualTo(DefaultDownloadIndex.TABLE_VERSION); .isEqualTo(DefaultDownloadIndex.TABLE_VERSION);
} }
} }

View File

@ -160,7 +160,7 @@ public class CachedContentIndexTest {
fos.write(testIndexV1File); fos.write(testIndexV1File);
fos.close(); fos.close();
index.load(); index.initialize(/* uid= */ 0);
assertThat(index.getAll()).hasSize(2); assertThat(index.getAll()).hasSize(2);
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
@ -181,7 +181,7 @@ public class CachedContentIndexTest {
fos.write(testIndexV2File); fos.write(testIndexV2File);
fos.close(); fos.close();
index.load(); index.initialize(/* uid= */ 0);
assertThat(index.getAll()).hasSize(2); assertThat(index.getAll()).hasSize(2);
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
@ -325,7 +325,7 @@ public class CachedContentIndexTest {
index.getOrAdd("ABCDE").applyMetadataMutations(mutations2); index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
index.store(); index.store();
index2.load(); index2.initialize(/* uid= */ 0);
Set<String> keys = index.getKeys(); Set<String> keys = index.getKeys();
Set<String> keys2 = index2.getKeys(); Set<String> keys2 = index2.getKeys();
assertThat(keys2).isEqualTo(keys); assertThat(keys2).isEqualTo(keys);