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;
|
package com.google.android.exoplayer2.database;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.ContextWrapper;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseErrorHandler;
|
|
||||||
import android.database.SQLException;
|
import android.database.SQLException;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database.
|
* 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
|
* prefer to keep ExoPlayer tables isolated in their own database. Other applications should prefer
|
||||||
* to use {@link DefaultDatabaseProvider} with their own {@link SQLiteOpenHelper}.
|
* 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);
|
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
|
@Override
|
||||||
public void onCreate(SQLiteDatabase db) {
|
public void onCreate(SQLiteDatabase db) {
|
||||||
// Features create their own tables.
|
// 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_FEATURE = "feature";
|
||||||
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 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
|
||||||
@ -62,7 +64,7 @@ public final class VersionTable {
|
|||||||
private 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 writableDatabase The database to update.
|
||||||
* @param feature The feature.
|
* @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
|
* Removes the version of the specified feature.
|
||||||
* no version information is available.
|
*
|
||||||
|
* @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 database The database to query.
|
||||||
* @param feature The feature.
|
* @param feature The feature.
|
||||||
@ -88,14 +103,12 @@ public final class VersionTable {
|
|||||||
if (!tableExists(database, TABLE_NAME)) {
|
if (!tableExists(database, TABLE_NAME)) {
|
||||||
return VERSION_UNSET;
|
return VERSION_UNSET;
|
||||||
}
|
}
|
||||||
String selection = COLUMN_FEATURE + " = ?";
|
|
||||||
String[] selectionArgs = {Integer.toString(feature)};
|
|
||||||
try (Cursor cursor =
|
try (Cursor cursor =
|
||||||
database.query(
|
database.query(
|
||||||
TABLE_NAME,
|
TABLE_NAME,
|
||||||
new String[] {COLUMN_VERSION},
|
new String[] {COLUMN_VERSION},
|
||||||
selection,
|
WHERE_FEATURE_EQUALS,
|
||||||
selectionArgs,
|
featureArgument(feature),
|
||||||
/* groupBy= */ null,
|
/* groupBy= */ null,
|
||||||
/* having= */ null,
|
/* having= */ null,
|
||||||
/* orderBy= */ null)) {
|
/* orderBy= */ null)) {
|
||||||
@ -114,4 +127,8 @@ public final class VersionTable {
|
|||||||
readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName});
|
readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName});
|
||||||
return count > 0;
|
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_STREAM_KEYS = 14;
|
||||||
private static final int COLUMN_INDEX_CUSTOM_METADATA = 15;
|
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 =
|
private static final String[] COLUMNS =
|
||||||
new String[] {
|
new String[] {
|
||||||
@ -152,7 +152,7 @@ public final class DefaultDownloadIndex implements DownloadIndex {
|
|||||||
@Nullable
|
@Nullable
|
||||||
public DownloadState getDownloadState(String id) {
|
public DownloadState getDownloadState(String id) {
|
||||||
ensureInitialized();
|
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) {
|
if (cursor.getCount() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -210,9 +210,7 @@ public final class DefaultDownloadIndex implements DownloadIndex {
|
|||||||
@Override
|
@Override
|
||||||
public void removeDownloadState(String id) {
|
public void removeDownloadState(String id) {
|
||||||
ensureInitialized();
|
ensureInitialized();
|
||||||
databaseProvider
|
databaseProvider.getWritableDatabase().delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {id});
|
||||||
.getWritableDatabase()
|
|
||||||
.delete(TABLE_NAME, COLUMN_SELECTION_ID, new String[] {id});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureInitialized() {
|
private void ensureInitialized() {
|
||||||
|
@ -24,7 +24,6 @@ import android.support.annotation.VisibleForTesting;
|
|||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.util.SparseBooleanArray;
|
import android.util.SparseBooleanArray;
|
||||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
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.database.VersionTable;
|
||||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
@ -154,9 +153,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
Storage atomicFileStorage =
|
Storage atomicFileStorage =
|
||||||
new AtomicFileStorage(
|
new AtomicFileStorage(
|
||||||
new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec);
|
new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec);
|
||||||
// Storage sqliteStorage =
|
// Storage sqliteStorage = new SQLiteStorage(databaseProvider);
|
||||||
// new SQLiteStorage(
|
|
||||||
// new File(cacheDir, FILE_NAME_DATABASE), random, encrypt, cipher, secretKeySpec);
|
|
||||||
storage = atomicFileStorage;
|
storage = atomicFileStorage;
|
||||||
previousStorage = null;
|
previousStorage = null;
|
||||||
}
|
}
|
||||||
@ -170,7 +167,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
storage.storeFully(keyToContent);
|
storage.storeFully(keyToContent);
|
||||||
} catch (CacheException e) {
|
} catch (CacheException e) {
|
||||||
// We failed to copy into current storage, so keep using previous storage.
|
// We failed to copy into current storage, so keep using previous storage.
|
||||||
storage.release();
|
|
||||||
storage = previousStorage;
|
storage = previousStorage;
|
||||||
previousStorage = null;
|
previousStorage = null;
|
||||||
}
|
}
|
||||||
@ -179,7 +175,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
loadFrom(storage);
|
loadFrom(storage);
|
||||||
}
|
}
|
||||||
if (previousStorage != null) {
|
if (previousStorage != null) {
|
||||||
previousStorage.release(/* delete= */ true);
|
previousStorage.delete();
|
||||||
previousStorage = null;
|
previousStorage = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,14 +191,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
removedIds.clear();
|
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.
|
* 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. */
|
/** Returns whether the persisted index exists. */
|
||||||
boolean exists();
|
boolean exists();
|
||||||
|
|
||||||
/** Releases any held resources. */
|
/** Deletes the persisted index. */
|
||||||
default void release() {
|
void delete();
|
||||||
release(/* delete= */ false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Releases and held resources and optionally deletes the persisted index. */
|
|
||||||
void release(boolean delete);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't
|
* 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
|
@Override
|
||||||
public void release(boolean delete) {
|
public void delete() {
|
||||||
if (delete) {
|
atomicFile.delete();
|
||||||
atomicFile.delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -672,26 +653,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** {@link Storage} implementation that uses an SQL database. */
|
/** {@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 class SQLiteStorage implements Storage {
|
||||||
|
|
||||||
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata";
|
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata";
|
||||||
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";
|
||||||
private static final String COLUMN_FLAGS = "flags";
|
private static final String COLUMN_KEY = "key";
|
||||||
private static final String COLUMN_DATA = "data";
|
private static final String COLUMN_METADATA = "metadata";
|
||||||
|
|
||||||
private static final int COLUMN_INDEX_ID = 0;
|
private static final int COLUMN_INDEX_ID = 0;
|
||||||
private static final int COLUMN_INDEX_FLAGS = 1;
|
private static final int COLUMN_INDEX_KEY = 1;
|
||||||
private static final int COLUMN_INDEX_DATA = 2;
|
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_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
private static final String SQL_CREATE_TABLE =
|
private static final String SQL_CREATE_TABLE =
|
||||||
@ -700,52 +677,36 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
+ " ("
|
+ " ("
|
||||||
+ COLUMN_ID
|
+ COLUMN_ID
|
||||||
+ " INTEGER PRIMARY KEY NOT NULL,"
|
+ " INTEGER PRIMARY KEY NOT NULL,"
|
||||||
+ COLUMN_FLAGS
|
+ COLUMN_KEY
|
||||||
+ " INTEGER NOT NULL,"
|
+ " TEXT NOT NULL,"
|
||||||
+ COLUMN_DATA
|
+ COLUMN_METADATA
|
||||||
+ " BLOB NOT NULL)";
|
+ " BLOB NOT NULL)";
|
||||||
|
|
||||||
private static final int FLAG_ENCRYPTED = 1;
|
private final DatabaseProvider databaseProvider;
|
||||||
|
|
||||||
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 SparseArray<CachedContent> pendingUpdates;
|
private final SparseArray<CachedContent> pendingUpdates;
|
||||||
|
|
||||||
@Nullable private ReusableBufferedOutputStream bufferedOutputStream;
|
public SQLiteStorage(DatabaseProvider databaseProvider) {
|
||||||
|
this.databaseProvider = databaseProvider;
|
||||||
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);
|
|
||||||
pendingUpdates = new SparseArray<>();
|
pendingUpdates = new SparseArray<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists() {
|
public boolean exists() {
|
||||||
return file.exists()
|
return VersionTable.getVersion(
|
||||||
&& VersionTable.getVersion(
|
databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE_CONTENT_METADATA)
|
||||||
databaseProvider.getReadableDatabase(),
|
!= VersionTable.VERSION_UNSET;
|
||||||
VersionTable.FEATURE_CACHE_CONTENT_METADATA)
|
|
||||||
!= VersionTable.VERSION_UNSET;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release(boolean delete) {
|
public void delete() {
|
||||||
release();
|
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
|
||||||
if (delete) {
|
writableDatabase.beginTransaction();
|
||||||
SQLiteDatabase.deleteDatabase(file);
|
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()) {
|
try (Cursor cursor = getCursor()) {
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
int id = cursor.getInt(COLUMN_INDEX_ID);
|
int id = cursor.getInt(COLUMN_INDEX_ID);
|
||||||
boolean encrypted = (cursor.getInt(COLUMN_INDEX_FLAGS) & FLAG_ENCRYPTED) != 0;
|
String key = cursor.getString(COLUMN_INDEX_KEY);
|
||||||
byte[] data = cursor.getBlob(COLUMN_INDEX_DATA);
|
byte[] metadataBytes = cursor.getBlob(COLUMN_INDEX_METADATA);
|
||||||
|
|
||||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(metadataBytes);
|
||||||
DataInputStream input = new DataInputStream(inputStream);
|
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);
|
DefaultContentMetadata metadata = readContentMetadata(input);
|
||||||
|
|
||||||
CachedContent cachedContent = new CachedContent(id, key, metadata);
|
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) {
|
private void deleteRow(SQLiteDatabase writableDatabase, int key) {
|
||||||
String[] selectionArgs = {Integer.toString(key)};
|
writableDatabase.delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {Integer.toString(key)});
|
||||||
writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)
|
private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
if (bufferedOutputStream == null) {
|
writeContentMetadata(cachedContent.getMetadata(), new DataOutputStream(outputStream));
|
||||||
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);
|
|
||||||
}
|
|
||||||
byte[] data = outputStream.toByteArray();
|
byte[] data = outputStream.toByteArray();
|
||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(COLUMN_ID, cachedContent.id);
|
values.put(COLUMN_ID, cachedContent.id);
|
||||||
values.put(COLUMN_FLAGS, encrypt ? FLAG_ENCRYPTED : 0);
|
values.put(COLUMN_KEY, cachedContent.key);
|
||||||
values.put(COLUMN_DATA, data);
|
values.put(COLUMN_METADATA, data);
|
||||||
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
|
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,6 @@ public final class SimpleCache implements Cache {
|
|||||||
} catch (CacheException e) {
|
} catch (CacheException e) {
|
||||||
Log.e(TAG, "Storing index file failed", e);
|
Log.e(TAG, "Storing index file failed", e);
|
||||||
} finally {
|
} finally {
|
||||||
contentIndex.release();
|
|
||||||
unlockFolder(cacheDir);
|
unlockFolder(cacheDir);
|
||||||
released = true;
|
released = true;
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,16 @@ public class VersionTableTest {
|
|||||||
assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10);
|
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
|
@Test
|
||||||
public void doesTableExist_nonExistingTable_returnsFalse() {
|
public void doesTableExist_nonExistingTable_returnsFalse() {
|
||||||
assertThat(VersionTable.tableExists(readableDatabase, "NonExistingTable")).isFalse();
|
assertThat(VersionTable.tableExists(readableDatabase, "NonExistingTable")).isFalse();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user