diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java index ece8e57ae7..32dda5965c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -16,20 +16,16 @@ package com.google.android.exoplayer2.database; import android.content.Context; -import android.content.ContextWrapper; import android.database.Cursor; -import android.database.DatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Log; -import java.io.File; /** * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database. * - *

Suitable for use by applications that do not already have their own database, or which would + *

Suitable for use by applications that do not already have their own database, or that would * prefer to keep ExoPlayer tables isolated in their own database. Other applications should prefer * to use {@link DefaultDatabaseProvider} with their own {@link SQLiteOpenHelper}. */ @@ -51,15 +47,6 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION); } - /** - * Provides instances of the database located at the specified file. - * - * @param file The database file. - */ - public ExoDatabaseProvider(File file) { - super(new DatabaseFileProvidingContext(file), file.getName(), /* factory= */ null, VERSION); - } - @Override public void onCreate(SQLiteDatabase db) { // Features create their own tables. @@ -105,48 +92,4 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab } } } - - // TODO: This is fragile. Stop using it if/when SQLiteOpenHelper can be instantiated without a - // context [Internal ref: b/123351819], or by injecting a Context into all components that need - // to instantiate an ExoDatabaseProvider. - /** A {@link Context} that implements methods called by {@link SQLiteOpenHelper}. */ - private static class DatabaseFileProvidingContext extends ContextWrapper { - - private final File file; - - @SuppressWarnings("nullness:argument.type.incompatible") - public DatabaseFileProvidingContext(File file) { - super(/* base= */ null); - this.file = file; - } - - @Override - public File getDatabasePath(String name) { - return file; - } - - @Override - public SQLiteDatabase openOrCreateDatabase( - String name, int mode, SQLiteDatabase.CursorFactory factory) { - return openOrCreateDatabase(name, mode, factory, /* errorHandler= */ null); - } - - @Override - @SuppressWarnings("nullness:argument.type.incompatible") - public SQLiteDatabase openOrCreateDatabase( - String name, - int mode, - SQLiteDatabase.CursorFactory factory, - @Nullable DatabaseErrorHandler errorHandler) { - File databasePath = getDatabasePath(name); - int flags = SQLiteDatabase.CREATE_IF_NECESSARY; - if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) { - flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING; - } - if ((mode & MODE_NO_LOCALIZED_COLLATORS) != 0) { - flags |= SQLiteDatabase.NO_LOCALIZED_COLLATORS; - } - return SQLiteDatabase.openDatabase(databasePath.getPath(), factory, flags, errorHandler); - } - } } 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 cdcca7a350..471b7706ca 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 @@ -45,6 +45,8 @@ public final class VersionTable { private static final String COLUMN_FEATURE = "feature"; private static final String COLUMN_VERSION = "version"; + private static final String WHERE_FEATURE_EQUALS = COLUMN_FEATURE + " = ?"; + private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME @@ -62,7 +64,7 @@ public final class VersionTable { private VersionTable() {} /** - * Sets the version of tables belonging to the specified feature. + * Sets the version of the specified feature. * * @param writableDatabase The database to update. * @param feature The feature. @@ -78,8 +80,21 @@ public final class VersionTable { } /** - * Returns the version of tables belonging to the specified feature, or {@link #VERSION_UNSET} if - * no version information is available. + * Removes the version of the specified feature. + * + * @param writableDatabase The database to update. + * @param feature The feature. + */ + public static void removeVersion(SQLiteDatabase writableDatabase, @Feature int feature) { + if (!tableExists(writableDatabase, TABLE_NAME)) { + return; + } + writableDatabase.delete(TABLE_NAME, WHERE_FEATURE_EQUALS, featureArgument(feature)); + } + + /** + * Returns the version of the specified feature, or {@link #VERSION_UNSET} if no version + * information is available. * * @param database The database to query. * @param feature The feature. @@ -88,14 +103,12 @@ public final class VersionTable { if (!tableExists(database, TABLE_NAME)) { return VERSION_UNSET; } - String selection = COLUMN_FEATURE + " = ?"; - String[] selectionArgs = {Integer.toString(feature)}; try (Cursor cursor = database.query( TABLE_NAME, new String[] {COLUMN_VERSION}, - selection, - selectionArgs, + WHERE_FEATURE_EQUALS, + featureArgument(feature), /* groupBy= */ null, /* having= */ null, /* orderBy= */ null)) { @@ -114,4 +127,8 @@ public final class VersionTable { readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); return count > 0; } + + private static String[] featureArgument(int feature) { + return new String[] {Integer.toString(feature)}; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index c30260b11b..37b0f43f8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -73,7 +73,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { private static final int COLUMN_INDEX_STREAM_KEYS = 14; private static final int COLUMN_INDEX_CUSTOM_METADATA = 15; - private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; + private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; private static final String[] COLUMNS = new String[] { @@ -152,7 +152,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { @Nullable public DownloadState getDownloadState(String id) { ensureInitialized(); - try (Cursor cursor = getCursor(COLUMN_SELECTION_ID, new String[] {id})) { + try (Cursor cursor = getCursor(WHERE_ID_EQUALS, new String[] {id})) { if (cursor.getCount() == 0) { return null; } @@ -210,9 +210,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { @Override public void removeDownloadState(String id) { ensureInitialized(); - databaseProvider - .getWritableDatabase() - .delete(TABLE_NAME, COLUMN_SELECTION_ID, new String[] {id}); + databaseProvider.getWritableDatabase().delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {id}); } private void ensureInitialized() { 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 b22645c9d2..e7289bf29e 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 @@ -24,7 +24,6 @@ import android.support.annotation.VisibleForTesting; import android.util.SparseArray; import android.util.SparseBooleanArray; import com.google.android.exoplayer2.database.DatabaseProvider; -import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; @@ -154,9 +153,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Storage atomicFileStorage = new AtomicFileStorage( new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec); - // Storage sqliteStorage = - // new SQLiteStorage( - // new File(cacheDir, FILE_NAME_DATABASE), random, encrypt, cipher, secretKeySpec); + // Storage sqliteStorage = new SQLiteStorage(databaseProvider); storage = atomicFileStorage; previousStorage = null; } @@ -170,7 +167,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; storage.storeFully(keyToContent); } catch (CacheException e) { // We failed to copy into current storage, so keep using previous storage. - storage.release(); storage = previousStorage; previousStorage = null; } @@ -179,7 +175,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; loadFrom(storage); } if (previousStorage != null) { - previousStorage.release(/* delete= */ true); + previousStorage.delete(); previousStorage = null; } } @@ -195,14 +191,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; removedIds.clear(); } - /** Releases any underlying resources. */ - public void release() { - storage.release(); - if (previousStorage != null) { - previousStorage.release(); - } - } - /** * Adds the given key to the index if it isn't there already. * @@ -396,13 +384,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns whether the persisted index exists. */ boolean exists(); - /** Releases any held resources. */ - default void release() { - release(/* delete= */ false); - } - - /** Releases and held resources and optionally deletes the persisted index. */ - void release(boolean delete); + /** Deletes the persisted index. */ + void delete(); /** * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't @@ -479,10 +462,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void release(boolean delete) { - if (delete) { - atomicFile.delete(); - } + public void delete() { + atomicFile.delete(); } @Override @@ -672,26 +653,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** {@link Storage} implementation that uses an SQL database. */ - // TODO: - // 1. Implement upgrade/downgrade paths from/to AtomicFileStorage. - // 2. If encryption is enabled having previously written data, decide whether we need to rewrite - // 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 + "CacheContentMetadata"; private static final int TABLE_VERSION = 1; private static final String COLUMN_ID = "id"; - private static final String COLUMN_FLAGS = "flags"; - private static final String COLUMN_DATA = "data"; + private static final String COLUMN_KEY = "key"; + private static final String COLUMN_METADATA = "metadata"; private static final int COLUMN_INDEX_ID = 0; - private static final int COLUMN_INDEX_FLAGS = 1; - private static final int COLUMN_INDEX_DATA = 2; + private static final int COLUMN_INDEX_KEY = 1; + private static final int COLUMN_INDEX_METADATA = 2; - private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; + private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; - private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_FLAGS, COLUMN_DATA}; + private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA}; private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; private static final String SQL_CREATE_TABLE = @@ -700,52 +677,36 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; + " (" + COLUMN_ID + " INTEGER PRIMARY KEY NOT NULL," - + COLUMN_FLAGS - + " INTEGER NOT NULL," - + COLUMN_DATA + + COLUMN_KEY + + " TEXT NOT NULL," + + COLUMN_METADATA + " BLOB NOT NULL)"; - private static final int FLAG_ENCRYPTED = 1; - - private final File file; - private final Random random; - private final boolean encrypt; - @Nullable private final Cipher cipher; - @Nullable private final SecretKeySpec secretKeySpec; - private final ExoDatabaseProvider databaseProvider; + private final DatabaseProvider databaseProvider; private final SparseArray pendingUpdates; - @Nullable private ReusableBufferedOutputStream bufferedOutputStream; - - public SQLiteStorage( - File file, - Random random, - boolean encrypt, - @Nullable Cipher cipher, - @Nullable SecretKeySpec secretKeySpec) { - this.file = file; - this.random = random; - this.encrypt = encrypt; - this.cipher = cipher; - this.secretKeySpec = secretKeySpec; - databaseProvider = new ExoDatabaseProvider(file); + public SQLiteStorage(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; pendingUpdates = new SparseArray<>(); } @Override public boolean exists() { - return file.exists() - && VersionTable.getVersion( - databaseProvider.getReadableDatabase(), - VersionTable.FEATURE_CACHE_CONTENT_METADATA) - != VersionTable.VERSION_UNSET; + return VersionTable.getVersion( + databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE_CONTENT_METADATA) + != VersionTable.VERSION_UNSET; } @Override - public void release(boolean delete) { - release(); - if (delete) { - SQLiteDatabase.deleteDatabase(file); + public void delete() { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + VersionTable.removeVersion(writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); } } @@ -775,23 +736,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; try (Cursor cursor = getCursor()) { while (cursor.moveToNext()) { int id = cursor.getInt(COLUMN_INDEX_ID); - boolean encrypted = (cursor.getInt(COLUMN_INDEX_FLAGS) & FLAG_ENCRYPTED) != 0; - byte[] data = cursor.getBlob(COLUMN_INDEX_DATA); + String key = cursor.getString(COLUMN_INDEX_KEY); + byte[] metadataBytes = cursor.getBlob(COLUMN_INDEX_METADATA); - ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + ByteArrayInputStream inputStream = new ByteArrayInputStream(metadataBytes); DataInputStream input = new DataInputStream(inputStream); - if (encrypted) { - byte[] initializationVector = new byte[16]; - input.readFully(initializationVector); - IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); - try { - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); - } - input = new DataInputStream(new CipherInputStream(inputStream, cipher)); - } - String key = input.readUTF(); DefaultContentMetadata metadata = readContentMetadata(input); CachedContent cachedContent = new CachedContent(id, key, metadata); @@ -879,45 +828,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void deleteRow(SQLiteDatabase writableDatabase, int key) { - String[] selectionArgs = {Integer.toString(key)}; - writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); + writableDatabase.delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {Integer.toString(key)}); } private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - if (bufferedOutputStream == null) { - bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); - } else { - bufferedOutputStream.reset(outputStream); - } - DataOutputStream output = new DataOutputStream(bufferedOutputStream); - try { - if (encrypt) { - byte[] initializationVector = new byte[16]; - random.nextBytes(initializationVector); - output.write(initializationVector); - IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); - try { - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); // Should never happen. - } - output.flush(); - output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); - } - output.writeUTF(cachedContent.key); - writeContentMetadata(cachedContent.getMetadata(), output); - } finally { - // Necessary to finalize the cipher. - Util.closeQuietly(output); - } + writeContentMetadata(cachedContent.getMetadata(), new DataOutputStream(outputStream)); byte[] data = outputStream.toByteArray(); ContentValues values = new ContentValues(); values.put(COLUMN_ID, cachedContent.id); - values.put(COLUMN_FLAGS, encrypt ? FLAG_ENCRYPTED : 0); - values.put(COLUMN_DATA, data); + values.put(COLUMN_KEY, cachedContent.key); + values.put(COLUMN_METADATA, data); writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); } } 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 f66471ba1f..253108bd93 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 @@ -171,7 +171,6 @@ public final class SimpleCache implements Cache { } catch (CacheException e) { Log.e(TAG, "Storing index file failed", e); } finally { - contentIndex.release(); unlockFolder(cacheDir); released = true; } 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 44961e8681..2e17b25a45 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 @@ -67,6 +67,16 @@ public class VersionTableTest { assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); } + @Test + public void removeVersion_removesSetVersion() { + VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 1); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(1); + + VersionTable.removeVersion(writableDatabase, FEATURE_OFFLINE); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)) + .isEqualTo(VersionTable.VERSION_UNSET); + } + @Test public void doesTableExist_nonExistingTable_returnsFalse() { assertThat(VersionTable.tableExists(readableDatabase, "NonExistingTable")).isFalse();