Commit ab5dae64 by olly Committed by Oliver Woodman

Make sure we handle SQLiteException and other IO errors properly

SQLiteException is a runtime exception, which makes it easy to
forget to handle it. This change converts SQLiteExceptions into
a checked exception, which is then handled appropriately.

PiperOrigin-RevId: 237038793
parent dd06a2d5
......@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.offline.ActionFile;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.DownloadState;
......@@ -71,8 +72,8 @@ public class DownloadTracker implements DownloadManager.Listener {
private final DataSource.Factory dataSourceFactory;
private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, DownloadState> trackedDownloadStates;
private final DefaultDownloadIndex downloadIndex;
private final Handler actionFileIOHandler;
private final DownloadIndex downloadIndex;
private final Handler indexHandler;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
......@@ -83,9 +84,9 @@ public class DownloadTracker implements DownloadManager.Listener {
this.downloadIndex = downloadIndex;
listeners = new CopyOnWriteArraySet<>();
trackedDownloadStates = new HashMap<>();
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
actionFileWriteThread.start();
actionFileIOHandler = new Handler(actionFileWriteThread.getLooper());
HandlerThread indexThread = new HandlerThread("DownloadTracker");
indexThread.start();
indexHandler = new Handler(indexThread.getLooper());
loadTrackedActions();
}
......@@ -163,24 +164,32 @@ public class DownloadTracker implements DownloadManager.Listener {
// Internal methods
private void loadTrackedActions() {
DownloadStateCursor downloadStates = downloadIndex.getDownloadStates();
while (downloadStates.moveToNext()) {
DownloadState downloadState = downloadStates.getDownloadState();
trackedDownloadStates.put(downloadState.uri, downloadState);
try {
DownloadStateCursor downloadStates = downloadIndex.getDownloadStates();
while (downloadStates.moveToNext()) {
DownloadState downloadState = downloadStates.getDownloadState();
trackedDownloadStates.put(downloadState.uri, downloadState);
}
downloadStates.close();
} catch (IOException e) {
Log.w(TAG, "Failed to query download states", e);
}
downloadStates.close();
}
private void handleTrackedDownloadStateChanged(DownloadState downloadState) {
for (Listener listener : listeners) {
listener.onDownloadsChanged();
}
actionFileIOHandler.post(
indexHandler.post(
() -> {
if (downloadState.state == DownloadState.STATE_REMOVED) {
downloadIndex.removeDownloadState(downloadState.id);
} else {
downloadIndex.putDownloadState(downloadState);
try {
if (downloadState.state == DownloadState.STATE_REMOVED) {
downloadIndex.removeDownloadState(downloadState.id);
} else {
downloadIndex.putDownloadState(downloadState);
}
} catch (IOException e) {
// TODO: This whole method is going away in cr/232854678.
}
});
}
......
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.database;
import android.database.SQLException;
import java.io.IOException;
/** An {@link IOException} whose cause is an {@link SQLException}. */
public final class DatabaseIOException extends IOException {
public DatabaseIOException(SQLException cause) {
super(cause);
}
public DatabaseIOException(SQLException cause, String message) {
super(message, cause);
}
}
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.database;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
......@@ -78,15 +79,21 @@ public final class VersionTable {
* @param feature The feature.
* @param instanceUid The unique identifier of the instance of the feature.
* @param version The version.
* @throws DatabaseIOException If an error occurs executing the SQL.
*/
public static void setVersion(
SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version) {
writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);
ContentValues values = new ContentValues();
values.put(COLUMN_FEATURE, feature);
values.put(COLUMN_INSTANCE_UID, instanceUid);
values.put(COLUMN_VERSION, version);
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version)
throws DatabaseIOException {
try {
writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);
ContentValues values = new ContentValues();
values.put(COLUMN_FEATURE, feature);
values.put(COLUMN_INSTANCE_UID, instanceUid);
values.put(COLUMN_VERSION, version);
writableDatabase.replaceOrThrow(TABLE_NAME, /* nullColumnHack= */ null, values);
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
/**
......@@ -95,16 +102,22 @@ public final class VersionTable {
* @param writableDatabase The database to update.
* @param feature The feature.
* @param instanceUid The unique identifier of the instance of the feature.
* @throws DatabaseIOException If an error occurs executing the SQL.
*/
public static void removeVersion(
SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid) {
if (!tableExists(writableDatabase, TABLE_NAME)) {
return;
SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid)
throws DatabaseIOException {
try {
if (!tableExists(writableDatabase, TABLE_NAME)) {
return;
}
writableDatabase.delete(
TABLE_NAME,
WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,
featureAndInstanceUidArguments(feature, instanceUid));
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
writableDatabase.delete(
TABLE_NAME,
WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,
featureAndInstanceUidArguments(feature, instanceUid));
}
/**
......@@ -115,25 +128,31 @@ public final class VersionTable {
* @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.
* @throws DatabaseIOException If an error occurs executing the SQL.
*/
public static int getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid) {
if (!tableExists(database, TABLE_NAME)) {
return VERSION_UNSET;
}
try (Cursor cursor =
database.query(
TABLE_NAME,
new String[] {COLUMN_VERSION},
WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,
featureAndInstanceUidArguments(feature, instanceUid),
/* groupBy= */ null,
/* having= */ null,
/* orderBy= */ null)) {
if (cursor.getCount() == 0) {
public static int getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid)
throws DatabaseIOException {
try {
if (!tableExists(database, TABLE_NAME)) {
return VERSION_UNSET;
}
cursor.moveToNext();
return cursor.getInt(/* COLUMN_VERSION index */ 0);
try (Cursor cursor =
database.query(
TABLE_NAME,
new String[] {COLUMN_VERSION},
WHERE_FEATURE_AND_INSTANCE_UID_EQUALS,
featureAndInstanceUidArguments(feature, instanceUid),
/* groupBy= */ null,
/* having= */ null,
/* orderBy= */ null)) {
if (cursor.getCount() == 0) {
return VERSION_UNSET;
}
cursor.moveToNext();
return cursor.getInt(/* COLUMN_VERSION index */ 0);
}
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
......
......@@ -17,10 +17,13 @@ package com.google.android.exoplayer2.offline;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.database.DatabaseIOException;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.VersionTable;
import com.google.android.exoplayer2.util.Assertions;
......@@ -152,7 +155,7 @@ public final class DefaultDownloadIndex implements DownloadIndex {
@Override
@Nullable
public DownloadState getDownloadState(String id) {
public DownloadState getDownloadState(String id) throws DatabaseIOException {
ensureInitialized();
try (Cursor cursor = getCursor(WHERE_ID_EQUALS, new String[] {id})) {
if (cursor.getCount() == 0) {
......@@ -162,83 +165,102 @@ public final class DefaultDownloadIndex implements DownloadIndex {
DownloadState downloadState = getDownloadStateForCurrentRow(cursor);
Assertions.checkState(id.equals(downloadState.id));
return downloadState;
} catch (SQLiteException e) {
throw new DatabaseIOException(e);
}
}
@Override
public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) {
public DownloadStateCursor getDownloadStates(@DownloadState.State int... states)
throws DatabaseIOException {
ensureInitialized();
String selection = null;
if (states.length > 0) {
StringBuilder selectionBuilder = new StringBuilder();
selectionBuilder.append(COLUMN_STATE).append(" IN (");
for (int i = 0; i < states.length; i++) {
if (i > 0) {
selectionBuilder.append(',');
try {
String selection = null;
if (states.length > 0) {
StringBuilder selectionBuilder = new StringBuilder();
selectionBuilder.append(COLUMN_STATE).append(" IN (");
for (int i = 0; i < states.length; i++) {
if (i > 0) {
selectionBuilder.append(',');
}
selectionBuilder.append(states[i]);
}
selectionBuilder.append(states[i]);
selectionBuilder.append(')');
selection = selectionBuilder.toString();
}
selectionBuilder.append(')');
selection = selectionBuilder.toString();
Cursor cursor = getCursor(selection, /* selectionArgs= */ null);
return new DownloadStateCursorImpl(cursor);
} catch (SQLiteException e) {
throw new DatabaseIOException(e);
}
Cursor cursor = getCursor(selection, /* selectionArgs= */ null);
return new DownloadStateCursorImpl(cursor);
}
@Override
public void putDownloadState(DownloadState downloadState) {
public void putDownloadState(DownloadState downloadState) throws DatabaseIOException {
ensureInitialized();
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ID, downloadState.id);
values.put(COLUMN_TYPE, downloadState.type);
values.put(COLUMN_URI, downloadState.uri.toString());
values.put(COLUMN_CACHE_KEY, downloadState.cacheKey);
values.put(COLUMN_STATE, downloadState.state);
values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage);
values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes);
values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes);
values.put(COLUMN_FAILURE_REASON, downloadState.failureReason);
values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags);
values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements);
values.put(COLUMN_MANUAL_STOP_REASON, downloadState.manualStopReason);
values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs);
values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs);
values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys));
values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata);
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
try {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ID, downloadState.id);
values.put(COLUMN_TYPE, downloadState.type);
values.put(COLUMN_URI, downloadState.uri.toString());
values.put(COLUMN_CACHE_KEY, downloadState.cacheKey);
values.put(COLUMN_STATE, downloadState.state);
values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage);
values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes);
values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes);
values.put(COLUMN_FAILURE_REASON, downloadState.failureReason);
values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags);
values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements);
values.put(COLUMN_MANUAL_STOP_REASON, downloadState.manualStopReason);
values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs);
values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs);
values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys));
values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata);
writableDatabase.replaceOrThrow(TABLE_NAME, /* nullColumnHack= */ null, values);
} catch (SQLiteException e) {
throw new DatabaseIOException(e);
}
}
@Override
public void removeDownloadState(String id) {
public void removeDownloadState(String id) throws DatabaseIOException {
ensureInitialized();
databaseProvider.getWritableDatabase().delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {id});
try {
databaseProvider.getWritableDatabase().delete(TABLE_NAME, WHERE_ID_EQUALS, new String[] {id});
} catch (SQLiteException e) {
throw new DatabaseIOException(e);
}
}
private void ensureInitialized() {
private void ensureInitialized() throws DatabaseIOException {
if (initialized) {
return;
}
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
int version =
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID);
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID, TABLE_VERSION);
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS);
writableDatabase.execSQL(SQL_CREATE_TABLE);
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
try {
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
int version =
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID);
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID, 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();
}
} else if (version < TABLE_VERSION) {
// There is no previous version currently.
throw new IllegalStateException();
initialized = true;
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
initialized = true;
}
private Cursor getCursor(@Nullable String selection, @Nullable String[] selectionArgs) {
......
......@@ -16,9 +16,10 @@
package com.google.android.exoplayer2.offline;
import androidx.annotation.Nullable;
import java.io.IOException;
/** Persists {@link DownloadState}s. */
interface DownloadIndex {
public interface DownloadIndex {
/**
* Returns the {@link DownloadState} with the given {@code id}, or null.
......@@ -26,25 +27,32 @@ interface DownloadIndex {
* @param id ID of a {@link DownloadState}.
* @return The {@link DownloadState} with the given {@code id}, or null if a download state with
* this id doesn't exist.
* @throws IOException If an error occurs reading the state.
*/
@Nullable
DownloadState getDownloadState(String id);
DownloadState getDownloadState(String id) throws IOException;
/**
* Returns a {@link DownloadStateCursor} to {@link DownloadState}s with the given {@code states}.
*
* @param states Returns only the {@link DownloadState}s with this states. If empty, returns all.
* @return A cursor to {@link DownloadState}s with the given {@code states}.
* @throws IOException If an error occurs reading the state.
*/
DownloadStateCursor getDownloadStates(@DownloadState.State int... states);
DownloadStateCursor getDownloadStates(@DownloadState.State int... states) throws IOException;
/**
* Adds or replaces a {@link DownloadState}.
*
* @param downloadState The {@link DownloadState} to be added.
* @throws IOException If an error occurs setting the state.
*/
void putDownloadState(DownloadState downloadState);
void putDownloadState(DownloadState downloadState) throws IOException;
/** Removes the {@link DownloadState} with the given {@code id}. */
void removeDownloadState(String id);
/**
* Removes the {@link DownloadState} with the given {@code id}.
*
* @throws IOException If an error occurs removing the state.
*/
void removeDownloadState(String id) throws IOException;
}
......@@ -69,9 +69,10 @@ public final class DownloadIndexUtil {
* @param downloadIndex The action is converted to {@link DownloadState} and stored in this index.
* @param id A nullable custom download id which overwrites {@link DownloadAction#id}.
* @param action The action to be stored in {@link DownloadIndex}.
* @throws IOException If an error occurs storing the state in the {@link DownloadIndex}.
*/
public static void addAction(
DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) {
DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) throws IOException {
DownloadState downloadState = downloadIndex.getDownloadState(id != null ? id : action.id);
if (downloadState != null) {
downloadState = downloadState.mergeAction(action);
......
......@@ -810,6 +810,7 @@ public final class DownloadManager {
setState(STATE_REMOVED);
} else { // STATE_DOWNLOADING
if (error != null) {
Log.e(TAG, "Download failed: " + downloadState.id, error);
failureReason = FAILURE_REASON_UNKNOWN;
setState(STATE_FAILED);
} else {
......
......@@ -17,7 +17,9 @@ package com.google.android.exoplayer2.upstream.cache;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import com.google.android.exoplayer2.database.DatabaseIOException;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.VersionTable;
import com.google.android.exoplayer2.util.Assertions;
......@@ -63,36 +65,48 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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();
/**
* Initializes the index for the given cache UID.
*
* @throws DatabaseIOException If an error occurs initializing the index.
*/
public void initialize(long uid) throws DatabaseIOException {
try {
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();
}
} else if (version < TABLE_VERSION) {
// There is no previous version currently.
throw new IllegalStateException();
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
/**
* Returns all file metadata keyed by file name. The returned map is mutable and may be modified
* by the caller.
*
* @return The file metadata keyed by file name.
* @throws DatabaseIOException If an error occurs loading the metadata.
*/
public Map<String, CacheFileMetadata> getAll() {
public Map<String, CacheFileMetadata> getAll() throws DatabaseIOException {
try (Cursor cursor = getCursor()) {
Map<String, CacheFileMetadata> fileMetadata = new HashMap<>(cursor.getCount());
while (cursor.moveToNext()) {
......@@ -102,6 +116,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
fileMetadata.put(name, new CacheFileMetadata(length, lastAccessTimestamp));
}
return fileMetadata;
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
......@@ -111,44 +127,59 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @param name The name of the file.
* @param length The file length.
* @param lastAccessTimestamp The file last access timestamp.
* @throws DatabaseIOException If an error occurs setting the metadata.
*/
public void set(String name, long length, long lastAccessTimestamp) {
public void set(String name, long length, long lastAccessTimestamp) throws DatabaseIOException {
Assertions.checkNotNull(tableName);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, name);
values.put(COLUMN_LENGTH, length);
values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp);
writableDatabase.replace(tableName, /* nullColumnHack= */ null, values);
try {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_NAME, name);
values.put(COLUMN_LENGTH, length);
values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp);
writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
/**
* Removes metadata.
*
* @param name The name of the file whose metadata is to be removed.
* @throws DatabaseIOException If an error occurs removing the metadata.
*/
public void remove(String name) {
public void remove(String name) throws DatabaseIOException {
Assertions.checkNotNull(tableName);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
try {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
/**
* Removes metadata.
*
* @param names The names of the files whose metadata is to be removed.
* @throws DatabaseIOException If an error occurs removing the metadata.
*/
public void removeAll(Set<String> names) {
public void removeAll(Set<String> names) throws DatabaseIOException {
Assertions.checkNotNull(tableName);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
for (String name : names) {
writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
for (String name : names) {
writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
}
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
}
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
......
......@@ -33,6 +33,7 @@ import java.util.NavigableSet;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link Cache} implementation that maintains an in-memory representation. Note, only one
......@@ -65,6 +66,7 @@ public final class SimpleCache implements Cache {
private long uid;
private long totalSpace;
private boolean released;
@MonotonicNonNull private CacheException initializationException;
/**
* Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the
......@@ -218,6 +220,17 @@ public final class SimpleCache implements Cache {
conditionVariable.block();
}
/**
* Checks whether the cache was initialized successfully.
*
* @throws CacheException If an error occurred during initialization.
*/
public synchronized void checkInitialization() throws CacheException {
if (initializationException != null) {
throw initializationException;
}
}
@Override
public synchronized void release() {
if (released) {
......@@ -227,7 +240,7 @@ public final class SimpleCache implements Cache {
removeStaleSpans();
try {
contentIndex.store();
} catch (CacheException e) {
} catch (IOException e) {
Log.e(TAG, "Storing index file failed", e);
} finally {
unlockFolder(cacheDir);
......@@ -286,6 +299,9 @@ public final class SimpleCache implements Cache {
@Override
public synchronized SimpleCacheSpan startReadWrite(String key, long position)
throws InterruptedException, CacheException {
Assertions.checkState(!released);
checkInitialization();
while (true) {
SimpleCacheSpan span = startReadWriteNonBlocking(key, position);
if (span != null) {
......@@ -301,9 +317,12 @@ public final class SimpleCache implements Cache {
}
@Override
public synchronized @Nullable SimpleCacheSpan startReadWriteNonBlocking(
String key, long position) {
@Nullable
public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
throws CacheException {
Assertions.checkState(!released);
checkInitialization();
SimpleCacheSpan span = getSpan(key, position);
// Read case.
......@@ -313,7 +332,11 @@ public final class SimpleCache implements Cache {
long lastAccessTimestamp = System.currentTimeMillis();
boolean updateFile = false;
if (fileIndex != null) {
fileIndex.set(fileName, length, lastAccessTimestamp);
try {
fileIndex.set(fileName, length, lastAccessTimestamp);
} catch (IOException e) {
throw new CacheException(e);
}
} else {
// Updating the file itself to incorporate the new last access timestamp is much slower than
// updating the file index. Hence we only update the file if we don't have a file index.
......@@ -337,8 +360,10 @@ public final class SimpleCache implements Cache {
}
@Override
public synchronized File startFile(String key, long position, long length) {
public synchronized File startFile(String key, long position, long length) throws CacheException {
Assertions.checkState(!released);
checkInitialization();
CachedContent cachedContent = contentIndex.get(key);
Assertions.checkNotNull(cachedContent);
Assertions.checkState(cachedContent.isLocked());
......@@ -368,10 +393,9 @@ public final class SimpleCache implements Cache {
return;
}
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, length, contentIndex);
Assertions.checkState(span != null);
CachedContent cachedContent = contentIndex.get(span.key);
Assertions.checkNotNull(cachedContent);
SimpleCacheSpan span =
Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex));
CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key));
Assertions.checkState(cachedContent.isLocked());
// Check if the span conflicts with the set content length
......@@ -381,10 +405,19 @@ public final class SimpleCache implements Cache {
}
if (fileIndex != null) {
fileIndex.set(file.getName(), span.length, span.lastAccessTimestamp);
String fileName = file.getName();
try {
fileIndex.set(fileName, span.length, span.lastAccessTimestamp);
} catch (IOException e) {
throw new CacheException(e);
}
}
addSpan(span);
contentIndex.store();
try {
contentIndex.store();
} catch (IOException e) {
throw new CacheException(e);
}
notifyAll();
}
......@@ -423,8 +456,14 @@ public final class SimpleCache implements Cache {
public synchronized void applyContentMetadataMutations(
String key, ContentMetadataMutations mutations) throws CacheException {
Assertions.checkState(!released);
checkInitialization();
contentIndex.applyContentMetadataMutations(key, mutations);
contentIndex.store();
try {
contentIndex.store();
} catch (IOException e) {
throw new CacheException(e);
}
}
@Override
......@@ -471,41 +510,46 @@ public final class SimpleCache implements Cache {
/** Ensures that the cache's in-memory representation has been initialized. */
private void initialize() {
if (!cacheDir.exists()) {
// Attempt to create the cache directory.
if (!cacheDir.mkdirs()) {
// TODO: Initialization failed. Decide how to handle this.
initializationException =
new CacheException("Failed to create cache directory: " + cacheDir);
return;
}
}
File[] files = cacheDir.listFiles();
if (files == null) {
// TODO: Initialization failed. Decide how to handle this.
initializationException =
new CacheException("Failed to list cache directory files: " + cacheDir);
return;
}
try {
uid = loadUid(cacheDir, files);
} catch (IOException e) {
// TODO: Initialization failed. Decide how to handle this.
initializationException = new CacheException("Failed to load cache UID: " + cacheDir);
return;
}
// TODO: Handle content index initialization failures.
contentIndex.initialize(uid);
if (fileIndex != null) {
// TODO: Handle file index initialization failures.
fileIndex.initialize(uid);
Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();
loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);
fileIndex.removeAll(fileMetadata.keySet());
} else {
loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null);
try {
contentIndex.initialize(uid);
if (fileIndex != null) {
fileIndex.initialize(uid);
Map<String, CacheFileMetadata> fileMetadata = fileIndex.getAll();
loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata);
fileIndex.removeAll(fileMetadata.keySet());
} else {
loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null);
}
} catch (IOException e) {
initializationException = new CacheException(e);
return;
}
contentIndex.removeEmpty();
try {
contentIndex.store();
} catch (CacheException e) {
} catch (IOException e) {
Log.e(TAG, "Storing index file failed", e);
}
}
......@@ -580,7 +624,14 @@ public final class SimpleCache implements Cache {
}
totalSpace -= span.length;
if (fileIndex != null) {
fileIndex.remove(span.file.getName());
String fileName = span.file.getName();
try {
fileIndex.remove(fileName);
} catch (IOException e) {
// This will leave a stale entry in the file index. It will be removed next time the cache
// is initialized.
Log.w(TAG, "Failed to remove file index entry for: " + fileName);
}
}
contentIndex.maybeRemove(cachedContent.key);
notifySpanRemoved(span);
......
......@@ -43,20 +43,20 @@ public class VersionTableTest {
}
@Test
public void getVersion_unsetFeature_returnsVersionUnset() {
public void getVersion_unsetFeature_returnsVersionUnset() throws DatabaseIOException {
int version = VersionTable.getVersion(database, FEATURE_1, INSTANCE_1);
assertThat(version).isEqualTo(VersionTable.VERSION_UNSET);
}
@Test
public void getVersion_unsetVersion_returnsVersionUnset() {
public void getVersion_unsetVersion_returnsVersionUnset() throws DatabaseIOException {
VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);
int version = VersionTable.getVersion(database, FEATURE_1, INSTANCE_2);
assertThat(version).isEqualTo(VersionTable.VERSION_UNSET);
}
@Test
public void getVersion_returnsSetVersion() {
public void getVersion_returnsSetVersion() throws DatabaseIOException {
VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);
assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(1);
......@@ -74,7 +74,7 @@ public class VersionTableTest {
}
@Test
public void removeVersion_removesSetVersion() {
public void removeVersion_removesSetVersion() throws DatabaseIOException {
VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);
VersionTable.setVersion(database, FEATURE_1, INSTANCE_2, 2);
assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(1);
......
......@@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.INSTANC
import static com.google.common.truth.Truth.assertThat;
import android.database.sqlite.SQLiteDatabase;
import com.google.android.exoplayer2.database.DatabaseIOException;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.database.VersionTable;
import org.junit.After;
......@@ -47,12 +48,13 @@ public class DefaultDownloadIndexTest {
}
@Test
public void getDownloadState_nonExistingId_returnsNull() {
public void getDownloadState_nonExistingId_returnsNull() throws DatabaseIOException {
assertThat(downloadIndex.getDownloadState("non existing id")).isNull();
}
@Test
public void addAndGetDownloadState_nonExistingId_returnsTheSameDownloadState() {
public void addAndGetDownloadState_nonExistingId_returnsTheSameDownloadState()
throws DatabaseIOException {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
......@@ -63,7 +65,8 @@ public class DefaultDownloadIndexTest {
}
@Test
public void addAndGetDownloadState_existingId_returnsUpdatedDownloadState() {
public void addAndGetDownloadState_existingId_returnsUpdatedDownloadState()
throws DatabaseIOException {
String id = "id";
DownloadStateBuilder downloadStateBuilder = new DownloadStateBuilder(id);
downloadIndex.putDownloadState(downloadStateBuilder.build());
......@@ -97,7 +100,8 @@ public class DefaultDownloadIndexTest {
}
@Test
public void releaseAndRecreateDownloadIndex_returnsTheSameDownloadState() {
public void releaseAndRecreateDownloadIndex_returnsTheSameDownloadState()
throws DatabaseIOException {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadIndex.putDownloadState(downloadState);
......@@ -109,12 +113,13 @@ public class DefaultDownloadIndexTest {
}
@Test
public void removeDownloadState_nonExistingId_doesNotFail() {
public void removeDownloadState_nonExistingId_doesNotFail() throws DatabaseIOException {
downloadIndex.removeDownloadState("non existing id");
}
@Test
public void removeDownloadState_existingId_getDownloadStateReturnsNull() {
public void removeDownloadState_existingId_getDownloadStateReturnsNull()
throws DatabaseIOException {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadIndex.putDownloadState(downloadState);
......@@ -125,12 +130,13 @@ public class DefaultDownloadIndexTest {
}
@Test
public void getDownloadStates_emptyDownloadIndex_returnsEmptyArray() {
public void getDownloadStates_emptyDownloadIndex_returnsEmptyArray() throws DatabaseIOException {
assertThat(downloadIndex.getDownloadStates().getCount()).isEqualTo(0);
}
@Test
public void getDownloadStates_noState_returnsAllDownloadStatusSortedByStartTime() {
public void getDownloadStates_noState_returnsAllDownloadStatusSortedByStartTime()
throws DatabaseIOException {
DownloadState downloadState1 = new DownloadStateBuilder("id1").setStartTimeMs(1).build();
downloadIndex.putDownloadState(downloadState1);
DownloadState downloadState2 = new DownloadStateBuilder("id2").setStartTimeMs(0).build();
......@@ -147,7 +153,8 @@ public class DefaultDownloadIndexTest {
}
@Test
public void getDownloadStates_withStates_returnsAllDownloadStatusWithTheSameStates() {
public void getDownloadStates_withStates_returnsAllDownloadStatusWithTheSameStates()
throws DatabaseIOException {
DownloadState downloadState1 =
new DownloadStateBuilder("id1")
.setStartTimeMs(0)
......@@ -179,7 +186,7 @@ public class DefaultDownloadIndexTest {
}
@Test
public void putDownloadState_setsVersion() {
public void putDownloadState_setsVersion() throws DatabaseIOException {
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
assertThat(
VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, INSTANCE_UID))
......@@ -193,7 +200,7 @@ public class DefaultDownloadIndexTest {
}
@Test
public void downloadIndex_versionDowngradeWipesData() {
public void downloadIndex_versionDowngradeWipesData() throws DatabaseIOException {
DownloadState downloadState1 = new DownloadStateBuilder("id1").build();
downloadIndex.putDownloadState(downloadState1);
DownloadStateCursor cursor = downloadIndex.getDownloadStates();
......
......@@ -22,6 +22,7 @@ import android.net.Uri;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
......@@ -53,7 +54,7 @@ public class DownloadIndexUtilTest {
}
@Test
public void addAction_nonExistingDownloadState_createsNewDownloadState() {
public void addAction_nonExistingDownloadState_createsNewDownloadState() throws IOException {
byte[] data = new byte[] {1, 2, 3, 4};
DownloadAction action =
DownloadAction.createDownloadAction(
......@@ -71,7 +72,7 @@ public class DownloadIndexUtilTest {
}
@Test
public void addAction_existingDownloadState_createsMergedDownloadState() {
public void addAction_existingDownloadState_createsMergedDownloadState() throws IOException {
StreamKey streamKey1 =
new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
StreamKey streamKey2 =
......@@ -105,7 +106,7 @@ public class DownloadIndexUtilTest {
}
@Test
public void upgradeActionFile_createsDownloadStates() throws Exception {
public void upgradeActionFile_createsDownloadStates() throws IOException {
ActionFile actionFile = new ActionFile(tempFile);
StreamKey streamKey1 =
new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
......@@ -138,7 +139,8 @@ public class DownloadIndexUtilTest {
assertDownloadIndexContainsAction(action3, DownloadState.STATE_REMOVING);
}
private void assertDownloadIndexContainsAction(DownloadAction action, int state) {
private void assertDownloadIndexContainsAction(DownloadAction action, int state)
throws IOException {
DownloadState downloadState = downloadIndex.getDownloadState(action.id);
assertThat(downloadState).isNotNull();
assertThat(downloadState.type).isEqualTo(action.type);
......
......@@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertWithMessage;
import android.net.Uri;
import androidx.annotation.Nullable;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
......@@ -222,8 +221,8 @@ public class CachedContentIndexTest {
@Test
public void testLegacyEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
byte[] key2 = Util.getUtf8Bytes("Foo12345Foo12345"); // 128 bit key
assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));
......
......@@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.doAnswer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Util;
......@@ -229,7 +228,7 @@ public class SimpleCacheTest {
@Test
public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
......@@ -248,7 +247,7 @@ public class SimpleCacheTest {
@Test
public void testEncryptedIndexWrongKey() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
......@@ -258,7 +257,7 @@ public class SimpleCacheTest {
simpleCache.release();
// Reload cache
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = Util.getUtf8Bytes("Foo12345Foo12345"); // 128 bit key
simpleCache = getEncryptedSimpleCache(key2);
// Cache should be cleared
......@@ -268,7 +267,7 @@ public class SimpleCacheTest {
@Test
public void testEncryptedIndexLostKey() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment