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