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