Changes for SQLite databases on internal storage
- Remove ability to encrypt content index for SQLite storage - Remove hack for specifying arbitrary database location PiperOrigin-RevId: 232863763
This commit is contained in:
parent
a7324061b3
commit
dd99fdcb82
@ -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.
|
||||
*
|
||||
* <p>Suitable for use by applications that do not already have their own database, or which would
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)};
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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,11 +462,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release(boolean delete) {
|
||||
if (delete) {
|
||||
public void delete() {
|
||||
atomicFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean load(
|
||||
@ -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<CachedContent> 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)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user