Commit 230a798f by eguven Committed by Oliver Woodman

Create only one task for all DownloadActions for the same content

PiperOrigin-RevId: 225060323
parent 05bfeca5
......@@ -25,6 +25,8 @@
skippping.
* Workaround for MiTV (dangal) issue when swapping output surface
([#5169](https://github.com/google/ExoPlayer/issues/5169)).
* DownloadManager:
* Create only one task for all DownloadActions for the same content.
### 2.9.2 ###
......
......@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.offline;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_CANCELED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED;
......@@ -35,6 +34,7 @@ import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
......@@ -187,11 +187,13 @@ public final class DownloadManager {
}
/**
* Handles the given action. A task is created and added to the task queue. If it's a remove
* action then any download tasks for the same media are immediately canceled.
* Handles the given action.
*
* <p>A task is created and added to the task queue if there isn't one already for the same
* content.
*
* @param action The action to be executed.
* @return The id of the newly created task.
* @return The id of the newly created or the existing task.
*/
public int handleAction(DownloadAction action) {
Assertions.checkState(!released);
......@@ -218,10 +220,12 @@ public final class DownloadManager {
public int getDownloadCount() {
int count = 0;
for (int i = 0; i < tasks.size(); i++) {
if (!tasks.get(i).action.isRemoveAction) {
for (DownloadAction action : tasks.get(i).actionQueue) {
if (!action.isRemoveAction) {
count++;
}
}
}
return count;
}
......@@ -287,6 +291,14 @@ public final class DownloadManager {
}
private Task addTaskForAction(DownloadAction action) {
for (int i = 0; i < tasks.size(); i++) {
Task task = tasks.get(i);
if (task.action.isSameMedia(action)) {
task.addAction(action);
logd("Action is added to existing task", task);
return task;
}
}
Task task = new Task(nextTaskId++, this, downloaderFactory, action, minRetryCount);
tasks.add(task);
logd("Task is added", task);
......@@ -298,12 +310,8 @@ public final class DownloadManager {
*
* <ul>
* <li>It hasn't started yet.
* <li>There are no preceding conflicting tasks.
* <li>If it's a download task then there are no preceding download tasks on hold and the
* maximum number of active downloads hasn't been reached.
* <li>If it's a download task then the maximum number of active downloads hasn't been reached.
* </ul>
*
* If the task is a remove action then preceding conflicting tasks are canceled.
*/
private void maybeStartTasks() {
if (!initialized || released) {
......@@ -317,31 +325,8 @@ public final class DownloadManager {
if (!task.canStart()) {
continue;
}
DownloadAction action = task.action;
boolean isRemoveAction = action.isRemoveAction;
if (!isRemoveAction && skipDownloadActions) {
continue;
}
boolean canStartTask = true;
for (int j = 0; j < i; j++) {
Task otherTask = tasks.get(j);
if (otherTask.action.isSameMedia(action)) {
if (isRemoveAction) {
canStartTask = false;
logd(task + " clashes with " + otherTask);
otherTask.cancel();
// Continue loop to cancel any other preceding clashing tasks.
} else if (otherTask.action.isRemoveAction) {
canStartTask = false;
skipDownloadActions = true;
break;
}
}
}
if (canStartTask) {
boolean isRemoveAction = task.action.isRemoveAction;
if (isRemoveAction || !skipDownloadActions) {
task.start();
if (!isRemoveAction) {
activeDownloadTasks.add(task);
......@@ -436,14 +421,15 @@ public final class DownloadManager {
if (released) {
return;
}
final DownloadAction[] actions = new DownloadAction[tasks.size()];
ArrayList<DownloadAction> actions = new ArrayList<>(tasks.size());
for (int i = 0; i < tasks.size(); i++) {
actions[i] = tasks.get(i).action;
actions.addAll(tasks.get(i).actionQueue);
}
final DownloadAction[] actionsArray = actions.toArray(new DownloadAction[0]);
fileIOHandler.post(
() -> {
try {
actionFile.store(actions);
actionFile.store(actionsArray);
logd("Actions persisted.");
} catch (IOException e) {
Log.e(TAG, "Persisting actions failed.", e);
......@@ -465,20 +451,19 @@ public final class DownloadManager {
public static final class TaskState {
/**
* Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED},
* {@link #STATE_CANCELED} or {@link #STATE_FAILED}.
* Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED}
* or {@link #STATE_FAILED}.
*
* <p>Transition diagram:
*
* <pre>
* ┌────────┬─────→ canceled
* queued ↔ started ┬→ completed
* └→ failed
* </pre>
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED, STATE_FAILED})
@IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_FAILED})
public @interface State {}
/** The task is waiting to be started. */
public static final int STATE_QUEUED = 0;
......@@ -486,10 +471,8 @@ public final class DownloadManager {
public static final int STATE_STARTED = 1;
/** The task completed. */
public static final int STATE_COMPLETED = 2;
/** The task was canceled. */
public static final int STATE_CANCELED = 3;
/** The task failed. */
public static final int STATE_FAILED = 4;
public static final int STATE_FAILED = 3;
/** Returns the state string for the given state value. */
public static String getStateString(@State int state) {
......@@ -500,8 +483,6 @@ public final class DownloadManager {
return "STARTED";
case STATE_COMPLETED:
return "COMPLETED";
case STATE_CANCELED:
return "CANCELED";
case STATE_FAILED:
return "FAILED";
default:
......@@ -553,14 +534,15 @@ public final class DownloadManager {
/** Target states for the download thread. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_COMPLETED, STATE_QUEUED, STATE_CANCELED})
@IntDef({STATE_QUEUED, STATE_COMPLETED})
public @interface TargetState {}
private final int id;
private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
private final DownloadAction action;
private final int minRetryCount;
private final ArrayDeque<DownloadAction> actionQueue;
private DownloadAction action;
/** The current state of the task. */
@TaskState.State private int state;
/**
......@@ -586,6 +568,26 @@ public final class DownloadManager {
this.minRetryCount = minRetryCount;
state = STATE_QUEUED;
targetState = STATE_COMPLETED;
actionQueue = new ArrayDeque<>();
actionQueue.add(action);
}
public void addAction(DownloadAction newAction) {
Assertions.checkState(action.type.equals(newAction.type));
actionQueue.add(newAction);
DownloadAction updatedAction = DownloadActionUtil.mergeActions(actionQueue);
if (action.equals(updatedAction)) {
return;
}
if (state == STATE_STARTED) {
if (targetState == STATE_COMPLETED) {
stopDownloadThread();
}
} else {
Assertions.checkState(state == STATE_QUEUED);
action = updatedAction;
downloadManager.onTaskStateChange(this);
}
}
public TaskState getTaskState() {
......@@ -603,7 +605,7 @@ public final class DownloadManager {
/** Returns whether the task is finished. */
public boolean isFinished() {
return state == STATE_FAILED || state == STATE_COMPLETED || state == STATE_CANCELED;
return state == STATE_FAILED || state == STATE_COMPLETED;
}
/** Returns whether the task is started. */
......@@ -629,46 +631,45 @@ public final class DownloadManager {
public void start() {
if (state == STATE_QUEUED) {
state = STATE_STARTED;
action = actionQueue.peek();
targetState = STATE_COMPLETED;
downloadManager.onTaskStateChange(this);
downloader = downloaderFactory.createDownloader(action);
downloadThread =
new DownloadThread(
this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
}
}
public void cancel() {
if (state == STATE_STARTED) {
stopDownloadThread(STATE_CANCELED);
} else if (state == STATE_QUEUED) {
state = STATE_CANCELED;
downloadManager.handler.post(() -> downloadManager.onTaskStateChange(this));
downloadManager.onTaskStateChange(this);
}
}
public void stop() {
if (state == STATE_STARTED && targetState == STATE_COMPLETED) {
stopDownloadThread(STATE_QUEUED);
if (state == STATE_STARTED) {
stopDownloadThread();
}
}
// Internal methods running on the main thread.
private void stopDownloadThread(@TargetState int targetState) {
this.targetState = targetState;
private void stopDownloadThread() {
this.targetState = TaskState.STATE_QUEUED;
Assertions.checkNotNull(downloadThread).cancel();
}
private void onDownloadThreadStopped(@Nullable Throwable finalError) {
@TaskState.State int finalState = targetState;
if (targetState == STATE_COMPLETED && finalError != null) {
finalState = STATE_FAILED;
state = targetState;
error = null;
if (targetState == STATE_COMPLETED) {
if (finalError != null) {
state = STATE_FAILED;
error = finalError;
} else {
finalError = null;
actionQueue.remove();
if (!actionQueue.isEmpty()) {
// Don't continue running. Wait to be restarted by maybeStartTasks().
state = STATE_QUEUED;
action = actionQueue.peek();
}
}
}
state = finalState;
error = finalError;
downloadManager.onTaskStateChange(this);
}
}
......
......@@ -28,10 +28,12 @@ import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
......@@ -52,7 +54,9 @@ public class DownloadManagerTest {
private static final int ASSERT_FALSE_TIME = 1000;
/* Maximum retry delay in DownloadManager. */
private static final int MAX_RETRY_DELAY = 5000;
/* Maximum number of times a downloader can be restarted before doing a released check. */
private static final int MAX_STARTS_BEFORE_RELEASED = 1;
/** The minimum number of times a task must be retried before failing. */
private static final int MIN_RETRY_COUNT = 3;
private Uri uri1;
......@@ -84,309 +88,329 @@ public class DownloadManagerTest {
}
@Test
public void testDownloadActionRuns() throws Throwable {
doTestDownloaderRuns(createDownloadRunner(uri1));
public void downloadRunner_multipleInstancePerContent_throwsException() {
boolean exceptionThrown = false;
try {
new DownloadRunner(uri1);
new DownloadRunner(uri1);
// can't put fail() here as it would be caught in the catch below.
} catch (Throwable e) {
exceptionThrown = true;
}
@Test
public void testRemoveActionRuns() throws Throwable {
doTestDownloaderRuns(createRemoveRunner(uri1));
assertThat(exceptionThrown).isTrue();
}
@Test
public void testDownloadRetriesThenFails() throws Throwable {
DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true;
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock();
public void downloadRunner_handleActionReturnsDifferentTaskId_throwsException() {
DownloadRunner runner = new DownloadRunner(uri1).postDownloadAction();
TaskWrapper task = runner.getTask();
runner.setTask(new TaskWrapper(task.taskId + 10000));
boolean exceptionThrown = false;
try {
runner.postDownloadAction();
// can't put fail() here as it would be caught in the catch below.
} catch (Throwable e) {
exceptionThrown = true;
}
downloadRunner.assertFailed();
downloadManagerListener.clearDownloadError();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
assertThat(exceptionThrown).isTrue();
}
@Test
public void testDownloadNoRetryWhenCanceled() throws Throwable {
DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
downloadRunner.downloader.enableDownloadIOException = true;
downloadRunner.postAction().assertStarted();
DownloadRunner removeRunner = createRemoveRunner(uri1).postAction();
downloadRunner.unblock().assertCanceled();
removeRunner.unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
public void multipleActionsForTheSameContent_executedOnTheSameTask() {
// Two download actions on first task
new DownloadRunner(uri1).postDownloadAction().postDownloadAction();
// One download, one remove actions on second task
new DownloadRunner(uri2).postDownloadAction().postRemoveAction();
// Two remove actions on third task
new DownloadRunner(uri3).postRemoveAction().postRemoveAction();
}
@Test
public void testDownloadRetriesThenContinues() throws Throwable {
DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true;
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY);
if (i == MIN_RETRY_COUNT) {
fakeDownloader.enableDownloadIOException = false;
}
fakeDownloader.unblock();
}
downloadRunner.assertCompleted();
public void actionsForDifferentContent_executedOnDifferentTasks() {
TaskWrapper task1 = new DownloadRunner(uri1).postDownloadAction().getTask();
TaskWrapper task2 = new DownloadRunner(uri2).postDownloadAction().getTask();
TaskWrapper task3 = new DownloadRunner(uri3).postRemoveAction().getTask();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
assertThat(task1).isNoneOf(task2, task3);
assertThat(task2).isNotEqualTo(task3);
}
@Test
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public void testDownloadRetryCountResetsOnProgress() throws Throwable {
DownloadRunner downloadRunner = createDownloadRunner(uri1);
downloadRunner.postAction();
FakeDownloader fakeDownloader = downloadRunner.downloader;
fakeDownloader.enableDownloadIOException = true;
fakeDownloader.downloadedBytes = 0;
for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) {
fakeDownloader.assertStarted(MAX_RETRY_DELAY);
fakeDownloader.downloadedBytes++;
if (i == MIN_RETRY_COUNT + 10) {
fakeDownloader.enableDownloadIOException = false;
}
fakeDownloader.unblock();
}
downloadRunner.assertCompleted();
public void postDownloadAction_downloads() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
TaskWrapper task = runner.postDownloadAction().getTask();
task.assertStarted();
runner.getDownloader(0).unblock().assertReleased().assertStartCount(1);
task.assertCompleted();
runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri2));
public void postRemoveAction_removes() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
TaskWrapper task = runner.postRemoveAction().getTask();
task.assertStarted();
runner.getDownloader(0).unblock().assertReleased().assertStartCount(1);
task.assertCompleted();
runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createRemoveRunner(uri2));
public void downloadFails_retriesThenTaskFails() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
runner.postDownloadAction();
FakeDownloader downloader = runner.getDownloader(0);
for (int i = 0; i <= MIN_RETRY_COUNT; i++) {
downloader.assertStarted(MAX_RETRY_DELAY).fail();
}
@Test
public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
doTestDownloadersRunInParallel(createDownloadRunner(uri1), createDownloadRunner(uri1));
downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);
runner.getTask().assertFailed();
downloadManagerListener.blockUntilTasksComplete();
}
@Test
public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
doTestDownloadersRunSequentially(createDownloadRunner(uri1), createRemoveRunner(uri1));
public void downloadFails_retries() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
runner.postDownloadAction();
FakeDownloader downloader = runner.getDownloader(0);
for (int i = 0; i < MIN_RETRY_COUNT; i++) {
downloader.assertStarted(MAX_RETRY_DELAY).fail();
}
downloader.assertStarted(MAX_RETRY_DELAY).unblock();
@Test
public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
doTestDownloadersRunSequentially(createRemoveRunner(uri1), createDownloadRunner(uri1));
downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);
runner.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksComplete();
}
@Test
public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
doTestDownloadersRunSequentially(createRemoveRunner(uri1), createRemoveRunner(uri1));
public void downloadProgressOnRetry_retryCountResets() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
runner.postDownloadAction();
FakeDownloader downloader = runner.getDownloader(0);
int tooManyRetries = MIN_RETRY_COUNT + 10;
for (int i = 0; i < tooManyRetries; i++) {
downloader.increaseDownloadedByteCount();
downloader.assertStarted(MAX_RETRY_DELAY).fail();
}
downloader.assertStarted(MAX_RETRY_DELAY).unblock();
@Test
public void testSameMediaMultipleActions() throws Throwable {
DownloadRunner downloadAction1 = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner downloadAction2 = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner removeAction1 = createRemoveRunner(uri1);
DownloadRunner downloadAction3 = createDownloadRunner(uri1);
DownloadRunner removeAction2 = createRemoveRunner(uri1);
// Two download actions run in parallel.
downloadAction1.postAction().assertStarted();
downloadAction2.postAction().assertStarted();
// removeAction1 is added. It interrupts the two download actions' threads but they are
// configured to ignore it so removeAction1 doesn't start.
removeAction1.postAction().assertDoesNotStart();
// downloadAction2 finishes but it isn't enough to start removeAction1.
downloadAction2.unblock().assertCanceled();
removeAction1.assertDoesNotStart();
// downloadAction3 is postAction to DownloadManager but it waits for removeAction1 to finish.
downloadAction3.postAction().assertDoesNotStart();
// When downloadAction1 finishes, removeAction1 starts.
downloadAction1.unblock().assertCanceled();
removeAction1.assertStarted();
// downloadAction3 still waits removeAction1
downloadAction3.assertDoesNotStart();
// removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2
// starts immediately.
removeAction2.postAction();
removeAction1.assertCanceled();
downloadAction3.assertCanceled();
removeAction2.assertStarted().unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
downloader.assertReleased().assertStartCount(tooManyRetries + 1);
runner.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksComplete();
}
@Test
public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable {
DownloadRunner removeAction1 = createRemoveRunner(uri1).ignoreInterrupts();
DownloadRunner removeAction2 = createRemoveRunner(uri1);
DownloadRunner removeAction3 = createRemoveRunner(uri1);
removeAction1.postAction().assertStarted();
removeAction2.postAction().assertDoesNotStart();
removeAction3.postAction().assertDoesNotStart();
public void removeCancelsDownload() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
FakeDownloader downloader1 = runner.getDownloader(0);
removeAction2.assertCanceled();
removeAction1.unblock().assertCanceled();
removeAction3.assertStarted().unblock().assertCompleted();
runner.postDownloadAction();
downloader1.assertStarted();
runner.postRemoveAction();
downloader1.assertCanceled().assertStartCount(1);
runner.getDownloader(1).unblock().assertNotCanceled();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testGetTasks() throws Throwable {
DownloadRunner removeAction = createRemoveRunner(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1);
DownloadRunner downloadAction2 = createDownloadRunner(uri1);
public void downloadNotCancelRemove() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
FakeDownloader downloader1 = runner.getDownloader(0);
removeAction.postAction().assertStarted();
downloadAction1.postAction().assertDoesNotStart();
downloadAction2.postAction().assertDoesNotStart();
runner.postRemoveAction();
downloader1.assertStarted();
runner.postDownloadAction();
TaskState[] states = downloadManager.getAllTaskStates();
assertThat(states).hasLength(3);
assertThat(states[0].action).isEqualTo(removeAction.action);
assertThat(states[1].action).isEqualTo(downloadAction1.action);
assertThat(states[2].action).isEqualTo(downloadAction2.action);
downloader1.unblock().assertNotCanceled();
runner.getDownloader(1).unblock().assertNotCanceled();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable {
DownloadRunner removeAction = createRemoveRunner(uri1);
DownloadRunner downloadAction1 = createDownloadRunner(uri1);
DownloadRunner downloadAction2 = createDownloadRunner(uri1);
public void secondSameRemoveActionIgnored() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
FakeDownloader downloader1 = runner.getDownloader(0);
removeAction.postAction().assertStarted();
downloadAction1.postAction().assertDoesNotStart();
downloadAction2.postAction().assertDoesNotStart();
removeAction.unblock().assertCompleted();
downloadAction1.assertStarted();
downloadAction2.assertStarted();
downloadAction1.unblock().assertCompleted();
downloadAction2.unblock().assertCompleted();
runner.postRemoveAction();
downloader1.assertStarted();
runner.postRemoveAction();
downloader1.unblock().assertNotCanceled();
runner.getTask().assertCompleted();
runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable {
DownloadRunner removeRunner = createRemoveRunner(uri1).ignoreInterrupts();
DownloadRunner downloadRunner1 = createDownloadRunner(uri1);
DownloadRunner downloadRunner2 = createDownloadRunner(uri2);
removeRunner.postAction().assertStarted();
downloadRunner1.postAction().assertDoesNotStart();
downloadRunner2.postAction().assertDoesNotStart();
public void secondSameDownloadActionIgnored() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
FakeDownloader downloader1 = runner.getDownloader(0);
removeRunner.unblock().assertCompleted();
downloadRunner1.assertStarted();
downloadRunner2.assertStarted();
downloadRunner1.unblock().assertCompleted();
downloadRunner2.unblock().assertCompleted();
runner.postDownloadAction();
downloader1.assertStarted();
runner.postDownloadAction();
downloader1.unblock().assertNotCanceled();
runner.getTask().assertCompleted();
runner.assertCreatedDownloaderCount(1);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable {
DownloadRunner downloadRunner = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner removeRunner1 = createRemoveRunner(uri1);
DownloadRunner removeRunner2 = createRemoveRunner(uri2);
public void differentDownloadActionsMerged() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1);
FakeDownloader downloader1 = runner.getDownloader(0);
downloadRunner.postAction().assertStarted();
removeRunner1.postAction().assertDoesNotStart();
removeRunner2.postAction().assertStarted();
StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
downloadRunner.unblock().assertCanceled();
removeRunner2.unblock().assertCompleted();
runner.postDownloadAction(streamKey1);
downloader1.assertStarted();
runner.postDownloadAction(streamKey2);
removeRunner1.assertStarted();
removeRunner1.unblock().assertCompleted();
downloader1.unblock().assertCanceled();
FakeDownloader downloader2 = runner.getDownloader(1);
downloader2.assertStarted();
assertThat(downloader2.action.keys).containsExactly(streamKey1, streamKey2);
downloader2.unblock();
runner.getTask().assertCompleted();
runner.assertCreatedDownloaderCount(2);
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void testStopAndResume() throws Throwable {
DownloadRunner download1Runner = createDownloadRunner(uri1);
DownloadRunner remove2Runner = createRemoveRunner(uri2);
DownloadRunner download2Runner = createDownloadRunner(uri2);
DownloadRunner remove1Runner = createRemoveRunner(uri1);
DownloadRunner download3Runner = createDownloadRunner(uri3);
download1Runner.postAction().assertStarted();
remove2Runner.postAction().assertStarted();
download2Runner.postAction().assertDoesNotStart();
runOnMainThread(() -> downloadManager.stopDownloads());
download1Runner.assertStopped();
public void actionsForDifferentContent_executedInParallel() throws Throwable {
DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadAction();
FakeDownloader downloader1 = runner1.getDownloader(0);
FakeDownloader downloader2 = runner2.getDownloader(0);
downloader1.assertStarted();
downloader2.assertStarted();
downloader1.unblock();
downloader2.unblock();
runner1.getTask().assertCompleted();
runner2.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
// remove actions aren't stopped.
remove2Runner.unblock().assertCompleted();
// Although remove2 is finished, download2 doesn't start.
download2Runner.assertDoesNotStart();
@Test
public void actionsForDifferentContent_ifMaxDownloadIs1_executedSequentially() throws Throwable {
setUpDownloadManager(1);
DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadAction();
FakeDownloader downloader1 = runner1.getDownloader(0);
FakeDownloader downloader2 = runner2.getDownloader(0);
downloader1.assertStarted();
downloader2.assertDoesNotStart();
downloader1.unblock();
downloader2.assertStarted();
downloader2.unblock();
runner1.getTask().assertCompleted();
runner2.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
// When a new remove action is added, it cancels stopped download actions with the same media.
remove1Runner.postAction();
download1Runner.assertCanceled();
remove1Runner.assertStarted().unblock().assertCompleted();
@Test
public void removeActionForDifferentContent_ifMaxDownloadIs1_executedInParallel()
throws Throwable {
setUpDownloadManager(1);
DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
DownloadRunner runner2 = new DownloadRunner(uri2).postRemoveAction();
FakeDownloader downloader1 = runner1.getDownloader(0);
FakeDownloader downloader2 = runner2.getDownloader(0);
downloader1.assertStarted();
downloader2.assertStarted();
downloader1.unblock();
downloader2.unblock();
runner1.getTask().assertCompleted();
runner2.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
// New download actions can be added but they don't start.
download3Runner.postAction().assertDoesNotStart();
@Test
public void downloadActionFollowingRemove_ifMaxDownloadIs1_isNotStarted() throws Throwable {
setUpDownloadManager(1);
DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadAction();
DownloadRunner runner2 = new DownloadRunner(uri2).postRemoveAction().postDownloadAction();
FakeDownloader downloader1 = runner1.getDownloader(0);
FakeDownloader downloader2 = runner2.getDownloader(0);
FakeDownloader downloader3 = runner2.getDownloader(1);
downloader1.assertStarted();
downloader2.assertStarted();
downloader2.unblock();
downloader3.assertDoesNotStart();
downloader1.unblock();
downloader3.assertStarted();
downloader3.unblock();
runner1.getTask().assertCompleted();
runner2.getTask().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
runOnMainThread(() -> downloadManager.startDownloads());
@Test
public void getTasks_returnTasks() {
TaskWrapper task1 = new DownloadRunner(uri1).postDownloadAction().getTask();
TaskWrapper task2 = new DownloadRunner(uri2).postDownloadAction().getTask();
TaskWrapper task3 = new DownloadRunner(uri3).postRemoveAction().getTask();
download2Runner.assertStarted().unblock().assertCompleted();
download3Runner.assertStarted().unblock().assertCompleted();
TaskState[] states = downloadManager.getAllTaskStates();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
assertThat(states).hasLength(3);
int[] taskIds = {task1.taskId, task2.taskId, task3.taskId};
int[] stateTaskIds = {states[0].taskId, states[1].taskId, states[2].taskId};
assertThat(stateTaskIds).isEqualTo(taskIds);
}
@Test
public void testResumeBeforeTotallyStopped() throws Throwable {
setUpDownloadManager(2);
DownloadRunner download1Runner = createDownloadRunner(uri1).ignoreInterrupts();
DownloadRunner download2Runner = createDownloadRunner(uri2);
DownloadRunner download3Runner = createDownloadRunner(uri3);
public void stopAndResume() throws Throwable {
DownloadRunner runner1 = new DownloadRunner(uri1);
DownloadRunner runner2 = new DownloadRunner(uri2);
DownloadRunner runner3 = new DownloadRunner(uri3);
download1Runner.postAction().assertStarted();
download2Runner.postAction().assertStarted();
// download3 doesn't start as DM was configured to run two downloads in parallel.
download3Runner.postAction().assertDoesNotStart();
runner1.postDownloadAction().getTask().assertStarted();
runner2.postRemoveAction().getTask().assertStarted();
runner2.postDownloadAction();
runOnMainThread(() -> downloadManager.stopDownloads());
// download1 doesn't stop yet as it ignores interrupts.
download2Runner.assertStopped();
runner1.getTask().assertQueued();
runOnMainThread(() -> downloadManager.startDownloads());
// remove actions aren't stopped.
runner2.getDownloader(0).unblock().assertReleased();
runner2.getTask().assertQueued();
// Although remove2 is finished, download2 doesn't start.
runner2.getDownloader(1).assertDoesNotStart();
// download2 starts immediately.
download2Runner.assertStarted();
// When a new remove action is added, it cancels stopped download actions with the same media.
runner1.postRemoveAction();
runner1.getDownloader(1).assertStarted().unblock();
runner1.getTask().assertCompleted();
// download3 doesn't start as download1 still holds its slot.
download3Runner.assertDoesNotStart();
// New download actions can be added but they don't start.
runner3.postDownloadAction().getDownloader(0).assertDoesNotStart();
// when unblocked download1 stops and starts immediately.
download1Runner.unblock().assertStopped().assertStarted();
runOnMainThread(() -> downloadManager.startDownloads());
download1Runner.unblock();
download2Runner.unblock();
download3Runner.unblock();
runner2.getDownloader(1).assertStarted().unblock();
runner3.getDownloader(0).assertStarted().unblock();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
......@@ -419,101 +443,105 @@ public class DownloadManagerTest {
}
}
private void doTestDownloaderRuns(DownloadRunner runner) throws Throwable {
runner.postAction().assertStarted().unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
private void runOnMainThread(final Runnable r) {
dummyMainThread.runOnMainThread(r);
}
private void doTestDownloadersRunSequentially(DownloadRunner runner1, DownloadRunner runner2)
throws Throwable {
runner1.ignoreInterrupts().postAction().assertStarted();
runner2.postAction().assertDoesNotStart();
private final class DownloadRunner {
runner1.unblock();
runner2.assertStarted();
private final Uri uri;
private final ArrayList<FakeDownloader> downloaders;
private int createdDownloaderCount = 0;
private FakeDownloader downloader;
private TaskWrapper taskWrapper;
runner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
private DownloadRunner(Uri uri) {
this.uri = uri;
downloaders = new ArrayList<>();
downloader = addDownloader();
downloaderFactory.registerDownloadRunner(this);
}
private void doTestDownloadersRunInParallel(DownloadRunner runner1, DownloadRunner runner2)
throws Throwable {
runner1.postAction().assertStarted();
runner2.postAction().assertStarted();
runner1.unblock().assertCompleted();
runner2.unblock().assertCompleted();
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
private DownloadRunner postRemoveAction() {
return postAction(createRemoveAction(uri));
}
private DownloadRunner createDownloadRunner(Uri uri) {
return new DownloadRunner(uri, /* isRemoveAction= */ false);
private DownloadRunner postDownloadAction(StreamKey... keys) {
return postAction(createDownloadAction(uri, keys));
}
private DownloadRunner createRemoveRunner(Uri uri) {
return new DownloadRunner(uri, /* isRemoveAction= */ true);
private DownloadRunner postAction(DownloadAction action) {
AtomicInteger taskIdHolder = new AtomicInteger();
runOnMainThread(() -> taskIdHolder.set(downloadManager.handleAction(action)));
int taskId = taskIdHolder.get();
if (taskWrapper == null) {
taskWrapper = new TaskWrapper(taskId);
} else {
assertThat(taskId).isEqualTo(taskWrapper.taskId);
}
return this;
}
private void runOnMainThread(final Runnable r) {
dummyMainThread.runOnMainThread(r);
private synchronized FakeDownloader addDownloader() {
FakeDownloader fakeDownloader = new FakeDownloader();
downloaders.add(fakeDownloader);
return fakeDownloader;
}
private class DownloadRunner {
private synchronized FakeDownloader getDownloader(int index) {
while (downloaders.size() <= index) {
addDownloader();
}
return downloaders.get(index);
}
public final DownloadAction action;
public final FakeDownloader downloader;
private synchronized Downloader createDownloader(DownloadAction action) {
downloader = getDownloader(createdDownloaderCount++);
downloader.action = action;
return downloader;
}
private DownloadRunner(Uri uri, boolean isRemoveAction) {
action =
isRemoveAction
? DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null)
: DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri,
/* keys= */ Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
downloader = new FakeDownloader(isRemoveAction);
downloaderFactory.putFakeDownloader(action, downloader);
private TaskWrapper getTask() {
return taskWrapper;
}
private DownloadRunner postAction() {
runOnMainThread(() -> downloadManager.handleAction(action));
return this;
public void setTask(TaskWrapper taskWrapper) {
this.taskWrapper = taskWrapper;
}
private DownloadRunner assertDoesNotStart() throws InterruptedException {
Thread.sleep(ASSERT_FALSE_TIME);
assertThat(downloader.started.getCount()).isEqualTo(1);
return this;
private void assertCreatedDownloaderCount(int count) {
assertThat(createdDownloaderCount).isEqualTo(count);
}
}
private DownloadRunner assertStarted() throws InterruptedException {
downloader.assertStarted(ASSERT_TRUE_TIMEOUT);
private final class TaskWrapper {
private final int taskId;
private TaskWrapper(int taskId) {
this.taskId = taskId;
}
private TaskWrapper assertStarted() throws InterruptedException {
return assertState(TaskState.STATE_STARTED);
}
private DownloadRunner assertCompleted() {
private TaskWrapper assertCompleted() {
return assertState(TaskState.STATE_COMPLETED);
}
private DownloadRunner assertFailed() {
private TaskWrapper assertFailed() {
return assertState(TaskState.STATE_FAILED);
}
private DownloadRunner assertCanceled() {
return assertState(TaskState.STATE_CANCELED);
}
private DownloadRunner assertStopped() {
private TaskWrapper assertQueued() {
return assertState(TaskState.STATE_QUEUED);
}
private DownloadRunner assertState(@State int expectedState) {
private TaskWrapper assertState(@State int expectedState) {
while (true) {
Integer state = null;
try {
state = downloadManagerListener.pollStateChange(action, ASSERT_TRUE_TIMEOUT);
state = downloadManagerListener.pollStateChange(taskId, ASSERT_TRUE_TIMEOUT);
} catch (InterruptedException e) {
fail(e.getMessage());
}
......@@ -523,69 +551,98 @@ public class DownloadManagerTest {
}
}
private DownloadRunner unblock() {
downloader.unblock();
return this;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
return taskId == ((TaskWrapper) o).taskId;
}
private DownloadRunner ignoreInterrupts() {
downloader.ignoreInterrupts = true;
return this;
@Override
public int hashCode() {
return taskId;
}
}
private static class FakeDownloaderFactory implements DownloaderFactory {
private static DownloadAction createDownloadAction(Uri uri, StreamKey... keys) {
return DownloadAction.createDownloadAction(
DownloadAction.TYPE_PROGRESSIVE,
uri,
Arrays.asList(keys),
/* customCacheKey= */ null,
/* data= */ null);
}
public IdentityHashMap<DownloadAction, FakeDownloader> downloaders;
private static DownloadAction createRemoveAction(Uri uri) {
return DownloadAction.createRemoveAction(
DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null);
}
private static final class FakeDownloaderFactory implements DownloaderFactory {
private final HashMap<Uri, DownloadRunner> downloaders;
public FakeDownloaderFactory() {
downloaders = new IdentityHashMap<>();
downloaders = new HashMap<>();
}
public void putFakeDownloader(DownloadAction action, FakeDownloader downloader) {
downloaders.put(action, downloader);
public void registerDownloadRunner(DownloadRunner downloadRunner) {
assertThat(downloaders.put(downloadRunner.uri, downloadRunner)).isNull();
}
@Override
public Downloader createDownloader(DownloadAction action) {
return downloaders.get(action);
return downloaders.get(action.uri).createDownloader(action);
}
}
private static class FakeDownloader implements Downloader {
private static final class FakeDownloader implements Downloader {
private final com.google.android.exoplayer2.util.ConditionVariable blocker;
private final boolean isRemove;
private DownloadAction action;
private CountDownLatch started;
private boolean ignoreInterrupts;
private volatile boolean interrupted;
private volatile boolean cancelled;
private volatile boolean enableDownloadIOException;
private volatile int downloadedBytes = C.LENGTH_UNSET;
private volatile int downloadedBytes;
private volatile int startCount;
private FakeDownloader(boolean isRemove) {
this.isRemove = isRemove;
private FakeDownloader() {
this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
downloadedBytes = C.LENGTH_UNSET;
}
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override
public void download() throws InterruptedException, IOException {
assertThat(isRemove).isFalse();
// It's ok to update this directly as no other thread will update it.
startCount++;
assertThat(action.isRemoveAction).isFalse();
started.countDown();
block();
if (enableDownloadIOException) {
enableDownloadIOException = false;
throw new IOException();
}
}
@Override
public void cancel() {
// Do nothing.
cancelled = true;
}
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override
public void remove() throws InterruptedException {
assertThat(isRemove).isTrue();
// It's ok to update this directly as no other thread will update it.
startCount++;
assertThat(action.isRemoveAction).isTrue();
started.countDown();
block();
}
......@@ -597,27 +654,65 @@ public class DownloadManagerTest {
blocker.block();
break;
} catch (InterruptedException e) {
if (!ignoreInterrupts) {
interrupted = true;
throw e;
}
}
}
} finally {
blocker.close();
}
}
private FakeDownloader assertStarted() throws InterruptedException {
return assertStarted(ASSERT_TRUE_TIMEOUT);
}
private FakeDownloader assertStarted(int timeout) throws InterruptedException {
assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue();
started = new CountDownLatch(1);
return this;
}
private FakeDownloader assertStartCount(int count) throws InterruptedException {
assertThat(startCount).isEqualTo(count);
return this;
}
private FakeDownloader assertReleased() throws InterruptedException {
int count = 0;
while (started.await(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS)) {
if (count++ >= MAX_STARTS_BEFORE_RELEASED) {
fail();
}
started = new CountDownLatch(1);
}
return this;
}
private FakeDownloader assertCanceled() throws InterruptedException {
assertReleased();
assertThat(interrupted).isTrue();
assertThat(cancelled).isTrue();
return this;
}
private FakeDownloader assertNotCanceled() throws InterruptedException {
assertReleased();
assertThat(interrupted).isFalse();
assertThat(cancelled).isFalse();
return this;
}
private FakeDownloader unblock() {
blocker.open();
return this;
}
private FakeDownloader fail() {
enableDownloadIOException = true;
return unblock();
}
@Override
public long getDownloadedBytes() {
return downloadedBytes;
......@@ -632,5 +727,15 @@ public class DownloadManagerTest {
public float getDownloadPercentage() {
return C.PERCENTAGE_UNSET;
}
private void assertDoesNotStart() throws InterruptedException {
Thread.sleep(ASSERT_FALSE_TIME);
assertThat(started.getCount()).isEqualTo(1);
}
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
private void increaseDownloadedByteCount() {
downloadedBytes++;
}
}
}
......@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
......@@ -31,7 +30,7 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
private final DownloadManager downloadManager;
private final DummyMainThread dummyMainThread;
private final HashMap<DownloadAction, ArrayBlockingQueue<Integer>> actionStates;
private final HashMap<Integer, ArrayBlockingQueue<Integer>> actionStates;
private CountDownLatch downloadFinishedCondition;
private Throwable downloadError;
......@@ -43,8 +42,8 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
actionStates = new HashMap<>();
}
public int pollStateChange(DownloadAction action, long timeoutMs) throws InterruptedException {
return getStateQueue(action).poll(timeoutMs, TimeUnit.MILLISECONDS);
public Integer pollStateChange(int taskId, long timeoutMs) throws InterruptedException {
return getStateQueue(taskId).poll(timeoutMs, TimeUnit.MILLISECONDS);
}
public void clearDownloadError() {
......@@ -62,7 +61,7 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
if (taskState.state == DownloadManager.TaskState.STATE_FAILED && downloadError == null) {
downloadError = taskState.error;
}
getStateQueue(taskState.action).add(taskState.state);
getStateQueue(taskState.taskId).add(taskState.state);
}
@Override
......@@ -77,6 +76,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
* error.
*/
public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
blockUntilTasksComplete();
if (downloadError != null) {
throw new Exception(downloadError);
}
}
/** Blocks until all remove and download tasks are complete. Task errors are ignored. */
public void blockUntilTasksComplete() throws InterruptedException {
synchronized (this) {
downloadFinishedCondition = new CountDownLatch(1);
}
......@@ -87,17 +94,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
}
});
assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue();
if (downloadError != null) {
throw new Exception(downloadError);
}
}
private ArrayBlockingQueue<Integer> getStateQueue(DownloadAction action) {
private ArrayBlockingQueue<Integer> getStateQueue(int taskId) {
synchronized (actionStates) {
if (!actionStates.containsKey(action)) {
actionStates.put(action, new ArrayBlockingQueue<>(10));
if (!actionStates.containsKey(taskId)) {
actionStates.put(taskId, new ArrayBlockingQueue<>(10));
}
return actionStates.get(action);
return actionStates.get(taskId);
}
}
}
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