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
This commit is contained in:
olly 2019-03-06 14:51:03 +00:00 committed by Oliver Woodman
parent dd06a2d5f1
commit ab5dae64b9
15 changed files with 485 additions and 288 deletions

View file

@ -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.
}
});
}

View file

@ -0,0 +1,31 @@
/*
* 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);
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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);

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -18,15 +18,16 @@ package com.google.android.exoplayer2.upstream.cache;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
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.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile;
import com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
@ -160,28 +161,23 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
/**
* Loads the index file for the given cache UID.
* Loads the index data for the given cache UID.
*
* @param uid The UID of the cache whose index is to be loaded.
* @throws IOException If an error occurs initializing the index data.
*/
public void initialize(long uid) {
public void initialize(long uid) throws IOException {
storage.initialize(uid);
if (previousStorage != null) {
previousStorage.initialize(uid);
}
if (!storage.exists() && previousStorage != null && previousStorage.exists()) {
// Copy from previous storage into current storage.
loadFrom(previousStorage);
try {
storage.storeFully(keyToContent);
} catch (CacheException e) {
// We failed to copy into current storage, so keep using previous storage.
storage = previousStorage;
previousStorage = null;
}
previousStorage.load(keyToContent, idToKey);
storage.storeFully(keyToContent);
} else {
// Load from the current storage.
loadFrom(storage);
storage.load(keyToContent, idToKey);
}
if (previousStorage != null) {
previousStorage.delete();
@ -189,8 +185,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
}
/** Stores the index data to index file if there is a change. */
public void store() throws CacheException {
/**
* Stores the index data to index file if there is a change.
*
* @throws IOException If an error occurs storing the index data.
*/
public void store() throws IOException {
storage.storeIncremental(keyToContent);
// Make ids that were removed since the index was last stored eligible for re-use.
int removedIdCount = removedIds.size();
@ -286,14 +286,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
}
/** Loads the index from the specified storage. */
private void loadFrom(Storage storage) {
if (!storage.load(keyToContent, idToKey)) {
keyToContent.clear();
idToKey.clear();
}
}
private CachedContent addNew(String key) {
int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key);
@ -341,7 +333,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*
* @param input Input stream to read from.
* @return a {@link DefaultContentMetadata} instance.
* @throws IOException If an error occurs during reading from input.
* @throws IOException If an error occurs during reading from the input.
*/
private static DefaultContentMetadata readContentMetadata(DataInputStream input)
throws IOException {
@ -374,7 +366,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* Serializes itself to a {@link DataOutputStream}.
*
* @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output.
* @throws IOException If an error occurs writing to the output.
*/
private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output)
throws IOException {
@ -394,30 +386,43 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Initializes the storage for the given cache UID. */
void initialize(long uid);
/** Returns whether the persisted index exists. */
boolean exists();
/**
* Returns whether the persisted index exists.
*
* @throws IOException If an error occurs determining whether the persisted index exists.
*/
boolean exists() throws IOException;
/** Deletes the persisted index. */
void delete();
/**
* Deletes the persisted index.
*
* @throws IOException If an error occurs deleting the index.
*/
void delete() throws IOException;
/**
* Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't
* already exist.
*
* <p>If the persisted index is in a permanently bad state (i.e. all further attempts to load it
* are also expected to fail) then it will be deleted and the call will return successfully. For
* transient failures, {@link IOException} will be thrown.
*
* @param content The key to content map to populate with persisted data.
* @param idToKey The id to key map to populate with persisted data.
* @return Whether the load was successful.
* @throws IOException If an error occurs loading the index.
*/
boolean load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey);
void load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)
throws IOException;
/**
* Writes the persisted index, creating it if it doesn't already exist and replacing any
* existing content if it does.
*
* @param content The key to content map to persist.
* @throws CacheException If an error occurs persisting the index.
* @throws IOException If an error occurs persisting the index.
*/
void storeFully(HashMap<String, CachedContent> content) throws CacheException;
void storeFully(HashMap<String, CachedContent> content) throws IOException;
/**
* Ensures incremental changes to the index since the initial {@link #initialize(long)} or last
@ -425,9 +430,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}.
*
* @param content The key to content map to persist.
* @throws CacheException If an error occurs persisting the index.
* @throws IOException If an error occurs persisting the index.
*/
void storeIncremental(HashMap<String, CachedContent> content) throws CacheException;
void storeIncremental(HashMap<String, CachedContent> content) throws IOException;
/**
* Called when a {@link CachedContent} is added or updated.
@ -493,24 +498,24 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
public boolean load(
public void load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
Assertions.checkState(!changed);
if (!readFile(content, idToKey)) {
content.clear();
idToKey.clear();
atomicFile.delete();
return false;
}
return true;
}
@Override
public void storeFully(HashMap<String, CachedContent> content) throws CacheException {
public void storeFully(HashMap<String, CachedContent> content) throws IOException {
writeFile(content);
changed = false;
}
@Override
public void storeIncremental(HashMap<String, CachedContent> content) throws CacheException {
public void storeIncremental(HashMap<String, CachedContent> content) throws IOException {
if (!changed) {
return;
}
@ -529,6 +534,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private boolean readFile(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
if (!atomicFile.exists()) {
return true;
}
DataInputStream input = null;
try {
InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
@ -579,7 +588,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return true;
}
private void writeFile(HashMap<String, CachedContent> content) throws CacheException {
private void writeFile(HashMap<String, CachedContent> content) throws IOException {
DataOutputStream output = null;
try {
OutputStream outputStream = atomicFile.startWrite();
@ -619,8 +628,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// Avoid calling close twice. Duplicate CipherOutputStream.close calls did
// not used to be no-ops: https://android-review.googlesource.com/#/c/272799/
output = null;
} catch (IOException e) {
throw new CacheException(e);
} finally {
Util.closeQuietly(output);
}
@ -722,7 +729,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
public boolean exists() {
public boolean exists() throws DatabaseIOException {
return VersionTable.getVersion(
databaseProvider.getReadableDatabase(),
VersionTable.FEATURE_CACHE_CONTENT_METADATA,
@ -731,22 +738,27 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
public void delete() {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
public void delete() throws DatabaseIOException {
try {
VersionTable.removeVersion(
writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid);
dropTable(writableDatabase);
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
VersionTable.removeVersion(
writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid);
dropTable(writableDatabase);
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
}
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
@Override
public boolean load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
public void load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)
throws IOException {
Assertions.checkState(pendingUpdates.size() == 0);
try {
int version =
@ -783,52 +795,57 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
idToKey.put(cachedContent.id, cachedContent.key);
}
}
return true;
} catch (IOException | SQLiteException e) {
return false;
} catch (SQLiteException e) {
content.clear();
idToKey.clear();
throw new DatabaseIOException(e);
}
}
@Override
public void storeFully(HashMap<String, CachedContent> content) throws CacheException {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
public void storeFully(HashMap<String, CachedContent> content) throws IOException {
try {
initializeTable(writableDatabase);
for (CachedContent cachedContent : content.values()) {
addOrUpdateRow(writableDatabase, cachedContent);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
initializeTable(writableDatabase);
for (CachedContent cachedContent : content.values()) {
addOrUpdateRow(writableDatabase, cachedContent);
}
writableDatabase.setTransactionSuccessful();
pendingUpdates.clear();
} finally {
writableDatabase.endTransaction();
}
writableDatabase.setTransactionSuccessful();
pendingUpdates.clear();
} catch (IOException | SQLiteException e) {
throw new CacheException(e);
} finally {
writableDatabase.endTransaction();
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
@Override
public void storeIncremental(HashMap<String, CachedContent> content) throws CacheException {
public void storeIncremental(HashMap<String, CachedContent> content) throws IOException {
if (pendingUpdates.size() == 0) {
return;
}
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
for (int i = 0; i < pendingUpdates.size(); i++) {
CachedContent cachedContent = pendingUpdates.valueAt(i);
if (cachedContent == null) {
deleteRow(writableDatabase, pendingUpdates.keyAt(i));
} else {
addOrUpdateRow(writableDatabase, cachedContent);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
for (int i = 0; i < pendingUpdates.size(); i++) {
CachedContent cachedContent = pendingUpdates.valueAt(i);
if (cachedContent == null) {
deleteRow(writableDatabase, pendingUpdates.keyAt(i));
} else {
addOrUpdateRow(writableDatabase, cachedContent);
}
}
writableDatabase.setTransactionSuccessful();
pendingUpdates.clear();
} finally {
writableDatabase.endTransaction();
}
writableDatabase.setTransactionSuccessful();
pendingUpdates.clear();
} catch (IOException | SQLiteException e) {
throw new CacheException(e);
} finally {
writableDatabase.endTransaction();
} catch (SQLException e) {
throw new DatabaseIOException(e);
}
}
@ -855,7 +872,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* orderBy= */ null);
}
private void initializeTable(SQLiteDatabase writableDatabase) {
private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException {
VersionTable.setVersion(
writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION);
dropTable(writableDatabase);
@ -880,7 +897,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
values.put(COLUMN_ID, cachedContent.id);
values.put(COLUMN_KEY, cachedContent.key);
values.put(COLUMN_METADATA, data);
writableDatabase.replace(tableName, /* nullColumnHack= */ null, values);
writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
}
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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();

View file

@ -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);

View file

@ -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));

View file

@ -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