Put DownloadTasks on hold until preceding conflicting tasks are complete

Tasks conflict if both of them work on the same media and at least one
of them is remove action.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=172741795
This commit is contained in:
eguven 2017-10-19 07:35:51 -07:00 committed by Oliver Woodman
parent bb3dea5191
commit 7d0ec68d86
3 changed files with 376 additions and 2 deletions

View file

@ -0,0 +1,352 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
import android.os.ConditionVariable;
import android.support.annotation.Nullable;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask.State;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.ClosedSource;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.concurrent.Executors;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/** Tests {@link DownloadManager}. */
@ClosedSource(reason = "Not ready yet")
public class DownloadManagerTest extends InstrumentationTestCase {
/* Used to check if condition becomes true in this time interval. */
private static final int ASSERT_TRUE_TIMEOUT = 1000;
/* Used to check if condition stays false for this time interval. */
private static final int ASSERT_FALSE_TIME = 1000;
private DownloadManager downloadManager;
private ConditionVariable downloadFinishedCondition;
private Throwable downloadError;
@Override
public void setUp() throws Exception {
super.setUp();
setUpMockito(this);
downloadManager = new DownloadManager(
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY),
Executors.newCachedThreadPool());
downloadFinishedCondition = new ConditionVariable();
downloadManager.setListener(new TestDownloadListener());
}
class TestDownloadListener implements DownloadListener {
@Override
public void onStateChange(DownloadManager downloadManager, DownloadTask downloadTask, int state,
Throwable error) {
if (state == DownloadTask.STATE_ERROR && downloadError == null) {
downloadError = error;
}
((FakeDownloadAction) downloadTask.getDownloadAction()).onStateChange();
}
@Override
public void onTasksFinished(DownloadManager downloadManager) {
downloadFinishedCondition.open();
}
}
public void testDownloadActionRuns() throws Throwable {
doTestActionRuns(createDownloadAction("media 1"));
}
public void testRemoveActionRuns() throws Throwable {
doTestActionRuns(createRemoveAction("media 1"));
}
public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction("media 1"),
createDownloadAction("media 2"));
}
public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction("media 1"),
createRemoveAction("media 2"));
}
public void testSameMediaDownloadActionsStartInParallel() throws Throwable {
doTestActionsRunInParallel(createDownloadAction("media 1"),
createDownloadAction("media 1"));
}
public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable {
doTestActionsRunSequentially(createDownloadAction("media 1"),
createRemoveAction("media 1"));
}
public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction("media 1"),
createDownloadAction("media 1"));
}
public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable {
doTestActionsRunSequentially(createRemoveAction("media 1"),
createRemoveAction("media 1"));
}
public void testSameMediaMultipleActions() throws Throwable {
FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts();
FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts();
FakeDownloadAction removeAction1 = createRemoveAction("media 1");
FakeDownloadAction downloadAction3 = createDownloadAction("media 1");
FakeDownloadAction removeAction2 = createRemoveAction("media 1");
// Two download actions run in parallel.
downloadAction1.post().assertStarted();
downloadAction2.post().assertStarted();
// removeAction1 is added. It interrupts the two download actions' threads but they are
// configured to ignore it so removeAction1 doesn't start.
removeAction1.post().assertDoesNotStart();
// downloadAction2 finishes but it isn't enough to start removeAction1.
downloadAction2.finishAndAssertFinished();
removeAction1.assertDoesNotStart();
// downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish.
downloadAction3.post().assertDoesNotStart();
// When downloadAction1 finishes, removeAction1 starts.
downloadAction1.finishAndAssertFinished();
removeAction1.assertStarted();
// downloadAction3 still waits removeAction1
downloadAction3.assertDoesNotStart();
// removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2
// starts immediately.
removeAction2.post().assertStarted().finishAndAssertFinished();
blockUntilTasksCompleteAndThrowAnyDownloadError();
}
public void testMultipleWaitingAction() throws Throwable {
FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts();
FakeDownloadAction removeAction2 = createRemoveAction("media 1");
FakeDownloadAction removeAction3 = createRemoveAction("media 1");
removeAction1.post().assertStarted();
removeAction2.post().assertDoesNotStart();
removeAction3.post().assertDoesNotStart();
removeAction1.finishAndAssertFinished();
removeAction2.assertTaskState(DownloadTask.STATE_CANCELLED);
removeAction3.assertStarted().finishAndAssertFinished();
blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private void doTestActionRuns(FakeDownloadAction action) throws Throwable {
action.post().assertStarted().finishAndAssertFinished();
blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private void doTestActionsRunSequentially(FakeDownloadAction action1,
FakeDownloadAction action2) throws Throwable {
action1.ignoreInterrupts().post().assertStarted();
action2.post().assertDoesNotStart();
action1.finishAndAssertFinished();
action2.assertStarted();
action2.finishAndAssertFinished();
blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private void doTestActionsRunInParallel(FakeDownloadAction action1,
FakeDownloadAction action2) throws Throwable {
action1.post().assertStarted();
action2.post().assertStarted();
action1.finishAndAssertFinished();
action2.finishAndAssertFinished();
blockUntilTasksCompleteAndThrowAnyDownloadError();
}
private FakeDownloadAction createDownloadAction(String mediaId) {
return new FakeDownloadAction(downloadManager, mediaId, false);
}
private FakeDownloadAction createRemoveAction(String mediaId) {
return new FakeDownloadAction(downloadManager, mediaId, true);
}
private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
assertTrue(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT));
downloadFinishedCondition.close();
if (downloadError != null) {
throw downloadError;
}
}
/**
* Sets up Mockito for an instrumentation test.
*/
private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System.setProperty("dexmaker.dexcache",
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
MockitoAnnotations.initMocks(instrumentationTestCase);
}
private static class FakeDownloadAction extends DownloadAction {
private final DownloadManager downloadManager;
private final String mediaId;
private final boolean removeAction;
private final FakeDownloader downloader;
private final ConditionVariable stateChanged;
private DownloadTask downloadTask;
private FakeDownloadAction(DownloadManager downloadManager, String mediaId,
boolean removeAction) {
this.downloadManager = downloadManager;
this.mediaId = mediaId;
this.removeAction = removeAction;
this.downloader = new FakeDownloader(removeAction);
this.stateChanged = new ConditionVariable();
}
@Override
protected String getType() {
return "FakeDownloadAction";
}
@Override
protected void writeToStream(DataOutputStream output) throws IOException {
throw new UnsupportedOperationException();
}
@Override
protected boolean isRemoveAction() {
return removeAction;
}
@Override
protected boolean isSameMedia(DownloadAction other) {
return other instanceof FakeDownloadAction
&& mediaId.equals(((FakeDownloadAction) other).mediaId);
}
@Override
protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) {
return downloader;
}
private FakeDownloadAction post() throws DownloadException {
downloadTask = downloadManager.handleAction(this);
return this;
}
private FakeDownloadAction assertDoesNotStart() {
assertFalse(downloader.started.block(ASSERT_FALSE_TIME));
return this;
}
private FakeDownloadAction assertStarted() {
assertTrue(downloader.started.block(ASSERT_TRUE_TIMEOUT));
assertTaskState(DownloadTask.STATE_STARTED);
return this;
}
private FakeDownloadAction assertTaskState(@State int state) {
assertTrue(stateChanged.block(ASSERT_TRUE_TIMEOUT));
stateChanged.close();
assertEquals(state, downloadTask.getState());
return this;
}
private FakeDownloadAction finishAndAssertFinished() {
downloader.finish.open();
assertTaskState(DownloadTask.STATE_ENDED);
return this;
}
private FakeDownloadAction ignoreInterrupts() {
downloader.ignoreInterrupts = true;
return this;
}
private void onStateChange() {
stateChanged.open();
}
}
private static class FakeDownloader implements Downloader {
private final ConditionVariable started;
private final com.google.android.exoplayer2.util.ConditionVariable finish;
private final boolean removeAction;
private boolean ignoreInterrupts;
private FakeDownloader(boolean removeAction) {
this.removeAction = removeAction;
this.started = new ConditionVariable();
this.finish = new com.google.android.exoplayer2.util.ConditionVariable();
}
@Override
public void init() throws InterruptedException, IOException {
// do nothing.
}
@Override
public void download(@Nullable ProgressListener listener)
throws InterruptedException, IOException {
assertFalse(removeAction);
started.open();
blockUntilFinish();
}
@Override
public void remove() throws InterruptedException {
assertTrue(removeAction);
started.open();
blockUntilFinish();
}
private void blockUntilFinish() throws InterruptedException {
while (true){
try {
finish.block();
break;
} catch (InterruptedException e) {
if (!ignoreInterrupts) {
throw e;
}
}
}
}
@Override
public long getDownloadedBytes() {
return 0;
}
@Override
public float getDownloadPercentage() {
return 0;
}
}
}

View file

@ -25,4 +25,9 @@ public final class DownloadException extends IOException {
super(message);
}
/** @param cause The cause for the exception. */
public DownloadException(Throwable cause) {
super(cause);
}
}

View file

@ -16,8 +16,8 @@
package com.google.android.exoplayer2.util;
/**
* A condition variable whose {@link #open()} and {@link #close()} methods return whether they
* resulted in a change of state.
* An interruptible condition variable whose {@link #open()} and {@link #close()} methods return
* whether they resulted in a change of state.
*/
public final class ConditionVariable {
@ -59,4 +59,21 @@ public final class ConditionVariable {
}
}
/**
* Blocks until the condition is opened or until timeout milliseconds have passed.
*
* @param timeout The maximum time to wait in milliseconds.
* @return true If the condition was opened, false if the call returns because of the timeout.
* @throws InterruptedException If the thread is interrupted.
*/
public synchronized boolean block(int timeout) throws InterruptedException {
long now = System.currentTimeMillis();
long end = now + timeout;
while (!isOpen && now < end) {
wait(end - now);
now = System.currentTimeMillis();
}
return isOpen;
}
}