Add DownloadIndex and DefaultDownloadIndex

DownloadIndex will be used to store and query DownloadStates.

PiperOrigin-RevId: 228673766
This commit is contained in:
eguven 2019-01-10 10:33:11 +00:00 committed by Oliver Woodman
parent 637b52ae0e
commit 92bec21c03
6 changed files with 1199 additions and 2 deletions

View File

@ -0,0 +1,528 @@
/*
* 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.offline;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A {@link DownloadIndex} which uses SQLite to persist {@link DownloadState}s.
*
* <p class="caution">Database access may take a long time, do not call methods of this class from
* the application main thread.
*/
public final class DefaultDownloadIndex implements DownloadIndex {
/** Provides {@link SQLiteDatabase} instances. */
public interface DatabaseProvider {
/** Closes any open database object. */
void close();
/**
* Creates and/or opens a database that will be used for reading and writing.
*
* <p>Once opened successfully, the database is cached, so you can call this method every time
* you need to write to the database. (Make sure to call {@link #close} when you no longer need
* the database.) Errors such as bad permissions or a full disk may cause this method to fail,
* but future attempts may succeed if the problem is fixed.
*
* @throws SQLiteException If the database cannot be opened for writing.
* @return A read/write database object valid until {@link #close} is called.
*/
SQLiteDatabase getWritableDatabase();
/**
* Creates and/or opens a database. This will be the same object returned by {@link
* #getWritableDatabase} unless some problem, such as a full disk, requires the database to be
* opened read-only. In that case, a read-only database object will be returned. If the problem
* is fixed, a future call to {@link #getWritableDatabase} may succeed, in which case the
* read-only database object will be closed and the read/write object will be returned in the
* future.
*
* <p>Once opened successfully, the database should be cached. When the database is no longer
* needed, {@link #close} will be called.
*
* @throws SQLiteException If the database cannot be opened.
* @return A database object valid until {@link #getWritableDatabase} or {@link #close} is
* called.
*/
SQLiteDatabase getReadableDatabase();
}
private static final String DATABASE_NAME = "exoplayer_internal.db";
private final DatabaseProvider databaseProvider;
@Nullable private DownloadStateTable downloadStateTable;
/**
* Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database.
*
* @param context A Context.
*/
public DefaultDownloadIndex(Context context) {
this(new DefaultDatabaseProvider(context));
}
/**
* Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database
* provided by {@code databaseProvider}.
*
* @param databaseProvider A DatabaseProvider which provides the database which will be used to
* store DownloadStatus table.
*/
public DefaultDownloadIndex(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
}
@Override
public void release() {
databaseProvider.close();
}
@Override
@Nullable
public DownloadState getDownloadState(String id) {
return getDownloadStateTable().get(id);
}
@Override
public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) {
return getDownloadStateTable().get(states);
}
@Override
public void putDownloadState(DownloadState downloadState) {
getDownloadStateTable().replace(downloadState);
}
@Override
public void removeDownloadState(String id) {
getDownloadStateTable().delete(id);
}
private DownloadStateTable getDownloadStateTable() {
if (downloadStateTable == null) {
downloadStateTable = new DownloadStateTable(databaseProvider);
}
return downloadStateTable;
}
@VisibleForTesting
/* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) {
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
long count =
DatabaseUtils.queryNumEntries(
readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName});
return count > 0;
}
private static final class DownloadStateCursorImpl implements DownloadStateCursor {
private final Cursor cursor;
private DownloadStateCursorImpl(Cursor cursor) {
this.cursor = cursor;
}
@Override
public DownloadState getDownloadState() {
return DownloadStateTable.getDownloadState(cursor);
}
@Override
public int getCount() {
return cursor.getCount();
}
@Override
public int getPosition() {
return cursor.getPosition();
}
@Override
public boolean moveToPosition(int position) {
return cursor.moveToPosition(position);
}
@Override
public void close() {
cursor.close();
}
@Override
public boolean isClosed() {
return cursor.isClosed();
}
}
@VisibleForTesting
/* package */ static final class DownloadStateTable {
@VisibleForTesting /* package */ static final String TABLE_NAME = "ExoPlayerDownloadStates";
@VisibleForTesting /* package */ static final int TABLE_VERSION = 1;
private static final String COLUMN_ID = "id";
private static final String COLUMN_TYPE = "title";
private static final String COLUMN_URI = "subtitle";
private static final String COLUMN_CACHE_KEY = "cache_key";
private static final String COLUMN_STATE = "state";
private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage";
private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes";
private static final String COLUMN_TOTAL_BYTES = "total_bytes";
private static final String COLUMN_FAILURE_REASON = "failure_reason";
private static final String COLUMN_STOP_FLAGS = "stop_flags";
private static final String COLUMN_START_TIME_MS = "start_time_ms";
private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms";
private static final String COLUMN_STREAM_KEYS = "stream_keys";
private static final String COLUMN_CUSTOM_METADATA = "custom_metadata";
private static final int COLUMN_INDEX_ID = 0;
private static final int COLUMN_INDEX_TYPE = 1;
private static final int COLUMN_INDEX_URI = 2;
private static final int COLUMN_INDEX_CACHE_KEY = 3;
private static final int COLUMN_INDEX_STATE = 4;
private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 5;
private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 6;
private static final int COLUMN_INDEX_TOTAL_BYTES = 7;
private static final int COLUMN_INDEX_FAILURE_REASON = 8;
private static final int COLUMN_INDEX_STOP_FLAGS = 9;
private static final int COLUMN_INDEX_START_TIME_MS = 10;
private static final int COLUMN_INDEX_UPDATE_TIME_MS = 11;
private static final int COLUMN_INDEX_STREAM_KEYS = 12;
private static final int COLUMN_INDEX_CUSTOM_METADATA = 13;
private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?";
private static final String[] COLUMNS =
new String[] {
COLUMN_ID,
COLUMN_TYPE,
COLUMN_URI,
COLUMN_CACHE_KEY,
COLUMN_STATE,
COLUMN_DOWNLOAD_PERCENTAGE,
COLUMN_DOWNLOADED_BYTES,
COLUMN_TOTAL_BYTES,
COLUMN_FAILURE_REASON,
COLUMN_STOP_FLAGS,
COLUMN_START_TIME_MS,
COLUMN_UPDATE_TIME_MS,
COLUMN_STREAM_KEYS,
COLUMN_CUSTOM_METADATA
};
private static final String SQL_DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME;
private static final String SQL_CREATE_TABLE =
"CREATE TABLE IF NOT EXISTS "
+ TABLE_NAME
+ " ("
+ COLUMN_ID
+ " TEXT PRIMARY KEY NOT NULL,"
+ COLUMN_TYPE
+ " TEXT NOT NULL,"
+ COLUMN_URI
+ " TEXT NOT NULL,"
+ COLUMN_CACHE_KEY
+ " TEXT,"
+ COLUMN_STATE
+ " INTEGER NOT NULL,"
+ COLUMN_DOWNLOAD_PERCENTAGE
+ " REAL NOT NULL,"
+ COLUMN_DOWNLOADED_BYTES
+ " INTEGER NOT NULL,"
+ COLUMN_TOTAL_BYTES
+ " INTEGER NOT NULL,"
+ COLUMN_FAILURE_REASON
+ " INTEGER NOT NULL,"
+ COLUMN_STOP_FLAGS
+ " INTEGER NOT NULL,"
+ COLUMN_START_TIME_MS
+ " INTEGER NOT NULL,"
+ COLUMN_UPDATE_TIME_MS
+ " INTEGER NOT NULL,"
+ COLUMN_STREAM_KEYS
+ " TEXT NOT NULL,"
+ COLUMN_CUSTOM_METADATA
+ " BLOB NOT NULL)";
private final DatabaseProvider databaseProvider;
public DownloadStateTable(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
VersionTable versionTable = new VersionTable(databaseProvider);
int version = versionTable.getVersion(VersionTable.FEATURE_OFFLINE);
if (!doesTableExist(databaseProvider, TABLE_NAME)
|| version == 0
|| version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
writableDatabase.execSQL(SQL_DROP_TABLE);
writableDatabase.execSQL(SQL_CREATE_TABLE);
versionTable.setVersion(VersionTable.FEATURE_OFFLINE, TABLE_VERSION);
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
}
} else if (version < TABLE_VERSION) {
// There is no previous version currently.
throw new IllegalStateException();
}
}
public void replace(DownloadState downloadState) {
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_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);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
}
@Nullable
public DownloadState get(String id) {
String[] selectionArgs = {id};
try (Cursor cursor = query(COLUMN_SELECTION_ID, selectionArgs)) {
if (cursor.getCount() == 0) {
return null;
}
cursor.moveToNext();
DownloadState downloadState = getDownloadState(cursor);
Assertions.checkState(id.equals(downloadState.id));
return downloadState;
}
}
public DownloadStateCursor get(@DownloadState.State int... states) {
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(')');
selection = selectionBuilder.toString();
}
Cursor cursor = query(selection, /* selectionArgs= */ null);
return new DownloadStateCursorImpl(cursor);
}
public void delete(String id) {
String[] selectionArgs = {id};
databaseProvider.getWritableDatabase().delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs);
}
private Cursor query(@Nullable String selection, @Nullable String[] selectionArgs) {
String sortOrder = COLUMN_START_TIME_MS + " ASC";
return databaseProvider
.getReadableDatabase()
.query(
TABLE_NAME,
COLUMNS,
selection,
selectionArgs,
/* groupBy= */ null,
/* having= */ null,
sortOrder);
}
private static DownloadState getDownloadState(Cursor cursor) {
return new DownloadState(
cursor.getString(COLUMN_INDEX_ID),
cursor.getString(COLUMN_INDEX_TYPE),
Uri.parse(cursor.getString(COLUMN_INDEX_URI)),
cursor.getString(COLUMN_INDEX_CACHE_KEY),
cursor.getInt(COLUMN_INDEX_STATE),
cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE),
cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES),
cursor.getLong(COLUMN_INDEX_TOTAL_BYTES),
cursor.getInt(COLUMN_INDEX_FAILURE_REASON),
cursor.getInt(COLUMN_INDEX_STOP_FLAGS),
cursor.getLong(COLUMN_INDEX_START_TIME_MS),
cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),
decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),
cursor.getBlob(COLUMN_INDEX_CUSTOM_METADATA));
}
private static String encodeStreamKeys(StreamKey[] streamKeys) {
StringBuilder stringBuilder = new StringBuilder();
for (StreamKey streamKey : streamKeys) {
stringBuilder
.append(streamKey.periodIndex)
.append('.')
.append(streamKey.groupIndex)
.append('.')
.append(streamKey.trackIndex)
.append(',');
}
if (stringBuilder.length() > 0) {
stringBuilder.setLength(stringBuilder.length() - 1);
}
return stringBuilder.toString();
}
private static StreamKey[] decodeStreamKeys(String encodedStreamKeys) {
if (encodedStreamKeys.isEmpty()) {
return new StreamKey[0];
}
String[] streamKeysStrings = Util.split(encodedStreamKeys, ",");
int streamKeysCount = streamKeysStrings.length;
StreamKey[] streamKeys = new StreamKey[streamKeysCount];
for (int i = 0; i < streamKeysCount; i++) {
String[] indices = Util.split(streamKeysStrings[i], "\\.");
Assertions.checkState(indices.length == 3);
streamKeys[i] =
new StreamKey(
Integer.parseInt(indices[0]),
Integer.parseInt(indices[1]),
Integer.parseInt(indices[2]));
}
return streamKeys;
}
}
@VisibleForTesting
/* package */ static final class VersionTable {
private static final String TABLE_NAME = "ExoPlayerVersions";
private static final String COLUMN_FEATURE = "feature";
private static final String COLUMN_VERSION = "version";
private static final String SQL_CREATE_TABLE =
"CREATE TABLE IF NOT EXISTS "
+ TABLE_NAME
+ " ("
+ COLUMN_FEATURE
+ " INTEGER PRIMARY KEY NOT NULL,"
+ COLUMN_VERSION
+ " INTEGER NOT NULL)";
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({FEATURE_OFFLINE, FEATURE_CACHE})
private @interface Feature {}
public static final int FEATURE_OFFLINE = 0;
public static final int FEATURE_CACHE = 1;
private final DatabaseProvider databaseProvider;
public VersionTable(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
if (!doesTableExist(databaseProvider, TABLE_NAME)) {
databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE);
}
}
public void setVersion(@Feature int feature, int version) {
ContentValues values = new ContentValues();
values.put(COLUMN_FEATURE, feature);
values.put(COLUMN_VERSION, version);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
}
public int getVersion(@Feature int feature) {
String selection = COLUMN_FEATURE + " = ?";
String[] selectionArgs = {Integer.toString(feature)};
try (Cursor cursor =
databaseProvider
.getReadableDatabase()
.query(
TABLE_NAME,
new String[] {COLUMN_VERSION},
selection,
selectionArgs,
/* groupBy= */ null,
/* having= */ null,
/* orderBy= */ null)) {
if (cursor.getCount() == 0) {
return 0;
}
cursor.moveToNext();
return cursor.getInt(/* COLUMN_VERSION index */ 0);
}
}
}
private static final class DefaultDatabaseProvider extends SQLiteOpenHelper
implements DatabaseProvider {
public DefaultDatabaseProvider(Context context) {
super(context, DATABASE_NAME, /* factory= */ null, /* version= */ 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
// Table creation is done in DownloadStateTable constructor.
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Upgrade is handled in DownloadStateTable constructor.
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO: Wipe the database.
super.onDowngrade(db, oldVersion, newVersion);
}
// DatabaseProvider implementation.
@Override
public synchronized void close() {
super.close();
}
@Override
public SQLiteDatabase getWritableDatabase() {
return super.getWritableDatabase();
}
@Override
public SQLiteDatabase getReadableDatabase() {
return super.getReadableDatabase();
}
}
}

View File

@ -156,7 +156,7 @@ public final class DownloadAction {
ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys);
Collections.sort(mutableKeys);
this.keys = Collections.unmodifiableList(mutableKeys);
this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY;
this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.offline;
import android.support.annotation.Nullable;
/** Persists {@link DownloadState}s. */
interface DownloadIndex {
/** Releases the used resources. */
void release();
/**
* Returns the {@link DownloadState} with the given {@code id}, or null.
*
* @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.
*/
@Nullable
DownloadState getDownloadState(String id);
/**
* 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}.
*/
DownloadStateCursor getDownloadStates(@DownloadState.State int... states);
/**
* Adds or replaces a {@link DownloadState}.
*
* @param downloadState The {@link DownloadState} to be added.
*/
void putDownloadState(DownloadState downloadState);
/** Removes the {@link DownloadState} with the given {@code id}. */
void removeDownloadState(String id);
}

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
import android.net.Uri;

View File

@ -0,0 +1,127 @@
/*
* 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.offline;
/** Provides random read-write access to the result set returned by a database query. */
interface DownloadStateCursor {
/** Returns the DownloadState at the current position. */
DownloadState getDownloadState();
/** Returns the numbers of DownloadStates in the cursor. */
int getCount();
/**
* Returns the current position of the cursor in the DownloadState set. The value is zero-based.
* When the DownloadState set is first returned the cursor will be at positon -1, which is before
* the first DownloadState. After the last DownloadState is returned another call to next() will
* leave the cursor past the last entry, at a position of count().
*
* @return the current cursor position.
*/
int getPosition();
/**
* Move the cursor to an absolute position. The valid range of values is -1 &lt;= position &lt;=
* count.
*
* <p>This method will return true if the request destination was reachable, otherwise, it returns
* false.
*
* @param position the zero-based position to move to.
* @return whether the requested move fully succeeded.
*/
boolean moveToPosition(int position);
/**
* Move the cursor to the first DownloadState.
*
* <p>This method will return false if the cursor is empty.
*
* @return whether the move succeeded.
*/
default boolean moveToFirst() {
return moveToPosition(0);
}
/**
* Move the cursor to the last DownloadState.
*
* <p>This method will return false if the cursor is empty.
*
* @return whether the move succeeded.
*/
default boolean moveToLast() {
return moveToPosition(getCount() - 1);
}
/**
* Move the cursor to the next DownloadState.
*
* <p>This method will return false if the cursor is already past the last entry in the result
* set.
*
* @return whether the move succeeded.
*/
default boolean moveToNext() {
return moveToPosition(getPosition() + 1);
}
/**
* Move the cursor to the previous DownloadState.
*
* <p>This method will return false if the cursor is already before the first entry in the result
* set.
*
* @return whether the move succeeded.
*/
default boolean moveToPrevious() {
return moveToPosition(getPosition() - 1);
}
/** Returns whether the cursor is pointing to the first DownloadState. */
default boolean isFirst() {
return getPosition() == 0 && getCount() != 0;
}
/** Returns whether the cursor is pointing to the last DownloadState. */
default boolean isLast() {
int count = getCount();
return getPosition() == (count - 1) && count != 0;
}
/** Returns whether the cursor is pointing to the position before the first DownloadState. */
default boolean isBeforeFirst() {
if (getCount() == 0) {
return true;
}
return getPosition() == -1;
}
/** Returns whether the cursor is pointing to the position after the last DownloadState. */
default boolean isAfterLast() {
if (getCount() == 0) {
return true;
}
return getPosition() == getCount();
}
/** Closes the Cursor, releasing all of its resources and making it completely invalid. */
void close();
/** Returns whether the cursor is closed */
boolean isClosed();
}

View File

@ -0,0 +1,491 @@
/*
* 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.offline;
import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.VersionTable.FEATURE_CACHE;
import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.VersionTable.FEATURE_OFFLINE;
import static com.google.common.truth.Truth.assertThat;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.util.Arrays;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Unit tests for {@link DefaultDownloadIndex}. */
@RunWith(RobolectricTestRunner.class)
public class DefaultDownloadIndexTest {
private DefaultDownloadIndex downloadIndex;
@Before
public void setUp() {
downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application);
}
@After
public void tearDown() {
downloadIndex.release();
}
@Test
public void getDownloadState_nonExistingId_returnsNull() {
assertThat(downloadIndex.getDownloadState("non existing id")).isNull();
}
@Test
public void addAndGetDownloadState_nonExistingId_returnsTheSameDownloadState() {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadIndex.putDownloadState(downloadState);
DownloadState readDownloadState = downloadIndex.getDownloadState(id);
assertEqual(readDownloadState, downloadState);
}
@Test
public void addAndGetDownloadState_existingId_returnsUpdatedDownloadState() {
String id = "id";
DownloadStateBuilder downloadStateBuilder = new DownloadStateBuilder(id);
downloadIndex.putDownloadState(downloadStateBuilder.build());
DownloadState downloadState =
downloadStateBuilder
.setType("different type")
.setUri("different uri")
.setCacheKey("different cacheKey")
.setState(DownloadState.STATE_FAILED)
.setDownloadPercentage(50)
.setDownloadedBytes(200)
.setTotalBytes(400)
.setFailureReason(DownloadState.FAILURE_REASON_UNKNOWN)
.setStopFlags(DownloadState.STOP_FLAG_STOPPED)
.setStartTimeMs(10)
.setUpdateTimeMs(20)
.setStreamKeys(
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2),
new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5))
.setCustomMetadata(new byte[] {0, 1, 2, 3})
.build();
downloadIndex.putDownloadState(downloadState);
DownloadState readDownloadState = downloadIndex.getDownloadState(id);
assertThat(readDownloadState).isNotNull();
assertEqual(readDownloadState, downloadState);
}
@Test
public void releaseAndRecreateDownloadIndex_returnsTheSameDownloadState() {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadIndex.putDownloadState(downloadState);
downloadIndex.release();
downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application);
DownloadState readDownloadState = downloadIndex.getDownloadState(id);
assertThat(readDownloadState).isNotNull();
assertEqual(readDownloadState, downloadState);
}
@Test
public void customDatabaseProvider_getDownloadStateReturnsNull() {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadIndex.putDownloadState(downloadState);
downloadIndex.release();
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
downloadIndex = new DefaultDownloadIndex(databaseProvider);
DownloadState readDownloadState = downloadIndex.getDownloadState(id);
assertThat(readDownloadState).isNull();
databaseProvider.close();
}
@Test
public void removeDownloadState_nonExistingId_doesNotFail() {
downloadIndex.removeDownloadState("non existing id");
}
@Test
public void removeDownloadState_existingId_getDownloadStateReturnsNull() {
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadIndex.putDownloadState(downloadState);
downloadIndex.removeDownloadState(id);
DownloadState readDownloadState = downloadIndex.getDownloadState(id);
assertThat(readDownloadState).isNull();
}
@Test
public void getDownloadStates_emptyDownloadIndex_returnsEmptyArray() {
assertThat(downloadIndex.getDownloadStates().getCount()).isEqualTo(0);
}
@Test
public void getDownloadStates_noState_returnsAllDownloadStatusSortedByStartTime() {
DownloadState downloadState1 = new DownloadStateBuilder("id1").setStartTimeMs(1).build();
downloadIndex.putDownloadState(downloadState1);
DownloadState downloadState2 = new DownloadStateBuilder("id2").setStartTimeMs(0).build();
downloadIndex.putDownloadState(downloadState2);
DownloadStateCursor cursor = downloadIndex.getDownloadStates();
assertThat(cursor.getCount()).isEqualTo(2);
cursor.moveToNext();
assertEqual(cursor.getDownloadState(), downloadState2);
cursor.moveToNext();
assertEqual(cursor.getDownloadState(), downloadState1);
}
@Test
public void getDownloadStates_withStates_returnsAllDownloadStatusWithTheSameStates() {
DownloadState downloadState1 =
new DownloadStateBuilder("id1")
.setStartTimeMs(0)
.setState(DownloadState.STATE_REMOVED)
.build();
downloadIndex.putDownloadState(downloadState1);
DownloadState downloadState2 =
new DownloadStateBuilder("id2")
.setStartTimeMs(1)
.setState(DownloadState.STATE_STOPPED)
.build();
downloadIndex.putDownloadState(downloadState2);
DownloadState downloadState3 =
new DownloadStateBuilder("id3")
.setStartTimeMs(2)
.setState(DownloadState.STATE_COMPLETED)
.build();
downloadIndex.putDownloadState(downloadState3);
DownloadStateCursor cursor =
downloadIndex.getDownloadStates(DownloadState.STATE_REMOVED, DownloadState.STATE_COMPLETED);
assertThat(cursor.getCount()).isEqualTo(2);
cursor.moveToNext();
assertEqual(cursor.getDownloadState(), downloadState1);
cursor.moveToNext();
assertEqual(cursor.getDownloadState(), downloadState3);
}
@Test
public void doesTableExist_nonExistingTable_returnsFalse() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
assertThat(DefaultDownloadIndex.doesTableExist(databaseProvider, "NonExistingTable")).isFalse();
databaseProvider.close();
}
@Test
public void doesTableExist_existingTable_returnsTrue() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
String tableName = "ExistingTable";
databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + tableName + "(dummy)");
assertThat(DefaultDownloadIndex.doesTableExist(databaseProvider, tableName)).isTrue();
databaseProvider.close();
}
@Test
public void getVersion_nonExistingTable_returnsZero() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
DefaultDownloadIndex.VersionTable versionTable =
new DefaultDownloadIndex.VersionTable(databaseProvider);
int version = versionTable.getVersion(FEATURE_OFFLINE);
assertThat(version).isEqualTo(0);
databaseProvider.close();
}
@Test
public void getVersion_returnsSetVersion() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
DefaultDownloadIndex.VersionTable versionTable =
new DefaultDownloadIndex.VersionTable(databaseProvider);
versionTable.setVersion(FEATURE_OFFLINE, 1);
assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(1);
versionTable.setVersion(FEATURE_OFFLINE, 10);
assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10);
versionTable.setVersion(FEATURE_CACHE, 5);
assertThat(versionTable.getVersion(FEATURE_CACHE)).isEqualTo(5);
assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10);
databaseProvider.close();
}
@Test
public void downloadStateTableConstructor_noTable_createsTable() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
assertThat(
DefaultDownloadIndex.doesTableExist(
databaseProvider, DefaultDownloadIndex.DownloadStateTable.TABLE_NAME))
.isFalse();
new DefaultDownloadIndex.DownloadStateTable(databaseProvider);
assertThat(
DefaultDownloadIndex.doesTableExist(
databaseProvider, DefaultDownloadIndex.DownloadStateTable.TABLE_NAME))
.isTrue();
databaseProvider.close();
}
@Test
public void downloadStateTableConstructor_versionZero_versionSet() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
new DefaultDownloadIndex.DownloadStateTable(databaseProvider);
DefaultDownloadIndex.VersionTable versionTable =
new DefaultDownloadIndex.VersionTable(databaseProvider);
assertThat(versionTable.getVersion(FEATURE_OFFLINE))
.isEqualTo(DefaultDownloadIndex.DownloadStateTable.TABLE_VERSION);
databaseProvider.close();
}
@Test
public void downloadStateTableConstructor_greaterVersion_tableRecreated() {
DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl();
databaseProvider
.getWritableDatabase()
.execSQL("CREATE TABLE " + DefaultDownloadIndex.DownloadStateTable.TABLE_NAME + "(dummy)");
DefaultDownloadIndex.VersionTable versionTable =
new DefaultDownloadIndex.VersionTable(databaseProvider);
versionTable.setVersion(FEATURE_OFFLINE, Integer.MAX_VALUE);
DefaultDownloadIndex.DownloadStateTable downloadStateTable =
new DefaultDownloadIndex.DownloadStateTable(databaseProvider);
String id = "id";
DownloadState downloadState = new DownloadStateBuilder(id).build();
downloadStateTable.replace(downloadState);
DownloadState readDownloadState = downloadStateTable.get(id);
assertEqual(readDownloadState, downloadState);
assertThat(versionTable.getVersion(FEATURE_OFFLINE))
.isEqualTo(DefaultDownloadIndex.DownloadStateTable.TABLE_VERSION);
databaseProvider.close();
}
private static void assertEqual(DownloadState downloadState, DownloadState expected) {
assertThat(areEqual(downloadState, expected)).isTrue();
}
private static boolean areEqual(DownloadState downloadState, DownloadState that) {
if (downloadState.state != that.state) {
return false;
}
if (Float.compare(that.downloadPercentage, downloadState.downloadPercentage) != 0) {
return false;
}
if (downloadState.downloadedBytes != that.downloadedBytes) {
return false;
}
if (downloadState.totalBytes != that.totalBytes) {
return false;
}
if (downloadState.startTimeMs != that.startTimeMs) {
return false;
}
if (downloadState.updateTimeMs != that.updateTimeMs) {
return false;
}
if (downloadState.failureReason != that.failureReason) {
return false;
}
if (downloadState.stopFlags != that.stopFlags) {
return false;
}
if (!downloadState.id.equals(that.id)) {
return false;
}
if (!downloadState.type.equals(that.type)) {
return false;
}
if (!downloadState.uri.equals(that.uri)) {
return false;
}
if (downloadState.cacheKey != null
? !downloadState.cacheKey.equals(that.cacheKey)
: that.cacheKey != null) {
return false;
}
if (!Arrays.equals(downloadState.streamKeys, that.streamKeys)) {
return false;
}
return Arrays.equals(downloadState.customMetadata, that.customMetadata);
}
private static class DownloadStateBuilder {
private String id;
private String type;
private String uri;
@Nullable private String cacheKey;
private int state;
private float downloadPercentage;
private long downloadedBytes;
private long totalBytes;
private int failureReason;
private int stopFlags;
private long startTimeMs;
private long updateTimeMs;
private StreamKey[] streamKeys;
private byte[] customMetadata;
private DownloadStateBuilder(String id) {
this.id = id;
this.type = "type";
this.uri = "uri";
this.cacheKey = null;
this.state = DownloadState.STATE_QUEUED;
this.downloadPercentage = (float) C.PERCENTAGE_UNSET;
this.downloadedBytes = (long) 0;
this.totalBytes = (long) C.LENGTH_UNSET;
this.failureReason = DownloadState.FAILURE_REASON_NONE;
this.stopFlags = 0;
this.startTimeMs = (long) 0;
this.updateTimeMs = (long) 0;
this.streamKeys = new StreamKey[0];
this.customMetadata = new byte[0];
}
public DownloadStateBuilder setId(String id) {
this.id = id;
return this;
}
public DownloadStateBuilder setType(String type) {
this.type = type;
return this;
}
public DownloadStateBuilder setUri(String uri) {
this.uri = uri;
return this;
}
public DownloadStateBuilder setCacheKey(@Nullable String cacheKey) {
this.cacheKey = cacheKey;
return this;
}
public DownloadStateBuilder setState(int state) {
this.state = state;
return this;
}
public DownloadStateBuilder setDownloadPercentage(float downloadPercentage) {
this.downloadPercentage = downloadPercentage;
return this;
}
public DownloadStateBuilder setDownloadedBytes(long downloadedBytes) {
this.downloadedBytes = downloadedBytes;
return this;
}
public DownloadStateBuilder setTotalBytes(long totalBytes) {
this.totalBytes = totalBytes;
return this;
}
public DownloadStateBuilder setFailureReason(int failureReason) {
this.failureReason = failureReason;
return this;
}
public DownloadStateBuilder setStopFlags(int stopFlags) {
this.stopFlags = stopFlags;
return this;
}
public DownloadStateBuilder setStartTimeMs(long startTimeMs) {
this.startTimeMs = startTimeMs;
return this;
}
public DownloadStateBuilder setUpdateTimeMs(long updateTimeMs) {
this.updateTimeMs = updateTimeMs;
return this;
}
public DownloadStateBuilder setStreamKeys(StreamKey... streamKeys) {
this.streamKeys = streamKeys;
return this;
}
public DownloadStateBuilder setCustomMetadata(byte[] customMetadata) {
this.customMetadata = customMetadata;
return this;
}
public DownloadState build() {
return new DownloadState(
id,
type,
Uri.parse(uri),
cacheKey,
state,
downloadPercentage,
downloadedBytes,
totalBytes,
failureReason,
stopFlags,
startTimeMs,
updateTimeMs,
streamKeys,
customMetadata);
}
}
private static final class DatabaseProviderImpl extends SQLiteOpenHelper
implements DefaultDownloadIndex.DatabaseProvider {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "TestExoPlayerDownloadIndex.db";
public DatabaseProviderImpl() {
super(RuntimeEnvironment.application, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// Do nothing.
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Do nothing.
}
}
}