Commit 3c010775 by eguven Committed by Oliver Woodman

Add STOPPED state to DownloadManager

PiperOrigin-RevId: 226460891
parent 173f3689
...@@ -21,6 +21,8 @@ import static com.google.android.exoplayer2.offline.DownloadManager.DownloadStat ...@@ -21,6 +21,8 @@ import static com.google.android.exoplayer2.offline.DownloadManager.DownloadStat
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_FAILED; import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_FAILED;
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_QUEUED; import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_QUEUED;
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED; import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED;
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STOPPED;
import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STOP_FLAG_STOPPED;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.os.Handler; import android.os.Handler;
...@@ -96,7 +98,7 @@ public final class DownloadManager { ...@@ -96,7 +98,7 @@ public final class DownloadManager {
private boolean initialized; private boolean initialized;
private boolean released; private boolean released;
private boolean downloadsStopped; @DownloadState.StopFlags private int stickyStopFlags;
/** /**
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
...@@ -126,7 +128,7 @@ public final class DownloadManager { ...@@ -126,7 +128,7 @@ public final class DownloadManager {
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.maxActiveDownloads = maxSimultaneousDownloads; this.maxActiveDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.downloadsStopped = true; this.stickyStopFlags = STOP_FLAG_STOPPED;
downloads = new ArrayList<>(); downloads = new ArrayList<>();
activeDownloads = new ArrayList<>(); activeDownloads = new ArrayList<>();
...@@ -169,8 +171,11 @@ public final class DownloadManager { ...@@ -169,8 +171,11 @@ public final class DownloadManager {
/** Starts the downloads. */ /** Starts the downloads. */
public void startDownloads() { public void startDownloads() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (downloadsStopped) { if (stickyStopFlags != 0) {
downloadsStopped = false; stickyStopFlags = 0;
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).clearStopFlags(STOP_FLAG_STOPPED);
}
maybeStartDownloads(); maybeStartDownloads();
logd("Downloads are started"); logd("Downloads are started");
} }
...@@ -179,10 +184,10 @@ public final class DownloadManager { ...@@ -179,10 +184,10 @@ public final class DownloadManager {
/** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */ /** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */
public void stopDownloads() { public void stopDownloads() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (!downloadsStopped) { if (stickyStopFlags == 0) {
downloadsStopped = true; stickyStopFlags = STOP_FLAG_STOPPED;
for (int i = 0; i < activeDownloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
activeDownloads.get(i).stop(); downloads.get(i).setStopFlags(STOP_FLAG_STOPPED);
} }
logd("Downloads are stopping"); logd("Downloads are stopping");
} }
...@@ -268,7 +273,7 @@ public final class DownloadManager { ...@@ -268,7 +273,7 @@ public final class DownloadManager {
} }
released = true; released = true;
for (int i = 0; i < downloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).stop(); downloads.get(i).queue();
} }
final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
fileIOHandler.post(fileIOFinishedCondition::open); fileIOHandler.post(fileIOFinishedCondition::open);
...@@ -286,7 +291,8 @@ public final class DownloadManager { ...@@ -286,7 +291,8 @@ public final class DownloadManager {
return; return;
} }
} }
Download download = new Download(this, downloaderFactory, action, minRetryCount); Download download =
new Download(this, downloaderFactory, action, minRetryCount, stickyStopFlags);
downloads.add(download); downloads.add(download);
logd("Download is added", download); logd("Download is added", download);
} }
...@@ -308,16 +314,14 @@ public final class DownloadManager { ...@@ -308,16 +314,14 @@ public final class DownloadManager {
} }
} }
private boolean maybeStartDownload(Download download) { private void maybeStartDownload(Download download) {
if (download.action.isRemoveAction) { if (download.action.isRemoveAction) {
return download.start(); download.start();
} else if (!downloadsStopped && activeDownloads.size() < maxActiveDownloads) { } else if (activeDownloads.size() < maxActiveDownloads) {
if (download.start()) { if (download.start()) {
activeDownloads.add(download); activeDownloads.add(download);
return true;
} }
} }
return false;
} }
private void maybeNotifyListenersIdle() { private void maybeNotifyListenersIdle() {
...@@ -426,28 +430,30 @@ public final class DownloadManager { ...@@ -426,28 +430,30 @@ public final class DownloadManager {
public static final class DownloadState { public static final class DownloadState {
/** /**
* Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link * Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STOPPED}, {@link #STATE_STARTED}
* #STATE_COMPLETED} or {@link #STATE_FAILED}. * , {@link #STATE_COMPLETED} or {@link #STATE_FAILED}.
* *
* <p>Transition diagram: * <p>Transition diagram:
* *
* <pre> * <pre>
* queued started ┬→ completed * queued ←┬→ started ┬→ completed
* └→ failed * └→ stopped └→ failed
* </pre> * </pre>
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_FAILED}) @IntDef({STATE_QUEUED, STATE_STOPPED, STATE_STARTED, STATE_COMPLETED, STATE_FAILED})
public @interface State {} public @interface State {}
/** The download is waiting to be started. */ /** The download is waiting to be started. */
public static final int STATE_QUEUED = 0; public static final int STATE_QUEUED = 0;
/** The download is stopped. */
public static final int STATE_STOPPED = 1;
/** The download is currently started. */ /** The download is currently started. */
public static final int STATE_STARTED = 1; public static final int STATE_STARTED = 2;
/** The download completed. */ /** The download completed. */
public static final int STATE_COMPLETED = 2; public static final int STATE_COMPLETED = 3;
/** The download failed. */ /** The download failed. */
public static final int STATE_FAILED = 3; public static final int STATE_FAILED = 4;
/** Failure reasons. Either {@link #FAILURE_REASON_NONE} or {@link #FAILURE_REASON_UNKNOWN}. */ /** Failure reasons. Either {@link #FAILURE_REASON_NONE} or {@link #FAILURE_REASON_UNKNOWN}. */
@Documented @Documented
...@@ -459,11 +465,23 @@ public final class DownloadManager { ...@@ -459,11 +465,23 @@ public final class DownloadManager {
/** The download is failed because of unknown reason. */ /** The download is failed because of unknown reason. */
public static final int FAILURE_REASON_UNKNOWN = 1; public static final int FAILURE_REASON_UNKNOWN = 1;
/** Download stop flags. Possible flag value is {@link #STOP_FLAG_STOPPED}. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {STOP_FLAG_STOPPED})
public @interface StopFlags {}
/** All downloads are stopped by the application. */
public static final int STOP_FLAG_STOPPED = 1;
/** 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) {
switch (state) { switch (state) {
case STATE_QUEUED: case STATE_QUEUED:
return "QUEUED"; return "QUEUED";
case STATE_STOPPED:
return "STOPPED";
case STATE_STARTED: case STATE_STARTED:
return "STARTED"; return "STARTED";
case STATE_COMPLETED: case STATE_COMPLETED:
...@@ -503,12 +521,13 @@ public final class DownloadManager { ...@@ -503,12 +521,13 @@ public final class DownloadManager {
public final long startTimeMs; public final long startTimeMs;
/** The last update time. */ /** The last update time. */
public final long updateTimeMs; public final long updateTimeMs;
/** /**
* If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link
* #FAILURE_REASON_NONE}. * #FAILURE_REASON_NONE}.
*/ */
@FailureReason public final int failureReason; @FailureReason public final int failureReason;
/** Download stop flags. These flags stop downloading any content. */
@StopFlags public final int stopFlags;
private DownloadState( private DownloadState(
DownloadAction action, DownloadAction action,
...@@ -517,7 +536,9 @@ public final class DownloadManager { ...@@ -517,7 +536,9 @@ public final class DownloadManager {
long downloadedBytes, long downloadedBytes,
long totalBytes, long totalBytes,
@FailureReason int failureReason, @FailureReason int failureReason,
@StopFlags int stopFlags,
long startTimeMs) { long startTimeMs) {
this.stopFlags = stopFlags;
Assertions.checkState( Assertions.checkState(
failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED); failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED);
this.id = action.id; this.id = action.id;
...@@ -530,7 +551,6 @@ public final class DownloadManager { ...@@ -530,7 +551,6 @@ public final class DownloadManager {
this.startTimeMs = startTimeMs; this.startTimeMs = startTimeMs;
updateTimeMs = System.currentTimeMillis(); updateTimeMs = System.currentTimeMillis();
} }
} }
private static final class Download { private static final class Download {
...@@ -548,24 +568,28 @@ public final class DownloadManager { ...@@ -548,24 +568,28 @@ public final class DownloadManager {
@MonotonicNonNull private Downloader downloader; @MonotonicNonNull private Downloader downloader;
@MonotonicNonNull private DownloadThread downloadThread; @MonotonicNonNull private DownloadThread downloadThread;
@MonotonicNonNull @DownloadState.FailureReason private int failureReason; @MonotonicNonNull @DownloadState.FailureReason private int failureReason;
@DownloadState.StopFlags private int stopFlags;
private Download( private Download(
DownloadManager downloadManager, DownloadManager downloadManager,
DownloaderFactory downloaderFactory, DownloaderFactory downloaderFactory,
DownloadAction action, DownloadAction action,
int minRetryCount) { int minRetryCount,
int stopFlags) {
this.id = action.id; this.id = action.id;
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.action = action;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.stopFlags = stopFlags;
this.startTimeMs = System.currentTimeMillis(); this.startTimeMs = System.currentTimeMillis();
state = STATE_QUEUED;
actionQueue = new ArrayDeque<>(); actionQueue = new ArrayDeque<>();
actionQueue.add(action); actionQueue.add(action);
if (!downloadManager.maybeStartDownload(this)) {
// If download is started, listeners are already notified about the started state. Otherwise // Don't notify listeners until we make sure the state doesn't change immediately.
// notify them here about the queued state. state = STATE_QUEUED;
setActionAndUpdateState(action);
downloadManager.maybeStartDownload(this);
if (state == STATE_QUEUED) {
downloadManager.onDownloadStateChange(this); downloadManager.onDownloadStateChange(this);
} }
} }
...@@ -580,9 +604,8 @@ public final class DownloadManager { ...@@ -580,9 +604,8 @@ public final class DownloadManager {
if (state == STATE_STARTED) { if (state == STATE_STARTED) {
stopDownloadThread(); stopDownloadThread();
} else { } else {
Assertions.checkState(state == STATE_QUEUED); Assertions.checkState(state == STATE_QUEUED || state == STATE_STOPPED);
action = updatedAction; setActionAndUpdateState(updatedAction);
downloadManager.onDownloadStateChange(this);
} }
} }
...@@ -602,6 +625,7 @@ public final class DownloadManager { ...@@ -602,6 +625,7 @@ public final class DownloadManager {
downloadedBytes, downloadedBytes,
totalBytes, totalBytes,
failureReason, failureReason,
stopFlags,
startTimeMs); startTimeMs);
} }
...@@ -617,34 +641,62 @@ public final class DownloadManager { ...@@ -617,34 +641,62 @@ public final class DownloadManager {
@Override @Override
public String toString() { public String toString() {
return action.type String actionString = action.isRemoveAction ? "remove" : "download";
+ ' ' return id + ' ' + actionString + ' ' + DownloadState.getStateString(state);
+ (action.isRemoveAction ? "remove" : "download")
+ ' '
+ DownloadState.getStateString(state);
} }
public boolean start() { public boolean start() {
if (state != STATE_QUEUED) { if (state != STATE_QUEUED) {
return false; return false;
} }
state = STATE_STARTED;
action = actionQueue.peek();
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.onDownloadStateChange(this); setState(STATE_STARTED);
return true; return true;
} }
public void stop() { public void setStopFlags(int stopFlags) {
updateStopFlags(stopFlags, stopFlags);
}
public void clearStopFlags(int stopFlags) {
updateStopFlags(stopFlags, 0);
}
public void queue() {
if (state == STATE_STARTED) {
stopDownloadThread();
}
}
private void updateStopFlags(int mask, int flags) {
stopFlags = (flags & mask) | (stopFlags & ~mask);
if (stopFlags != 0) {
if (!action.isRemoveAction) {
if (state == STATE_STARTED) { if (state == STATE_STARTED) {
stopDownloadThread(); stopDownloadThread();
} else if (state == STATE_QUEUED) {
setState(STATE_STOPPED);
}
}
} else if (state == STATE_STOPPED) {
setState(STATE_QUEUED);
} }
} }
// Internal methods running on the main thread. private void setActionAndUpdateState(DownloadAction action) {
this.action = action;
setState(!this.action.isRemoveAction && stopFlags != 0 ? STATE_STOPPED : STATE_QUEUED);
}
private void setState(@DownloadState.State int newState) {
if (state != newState) {
state = newState;
downloadManager.onDownloadStateChange(this);
}
}
private void stopDownloadThread() { private void stopDownloadThread() {
Assertions.checkNotNull(downloadThread).cancel(); Assertions.checkNotNull(downloadThread).cancel();
...@@ -654,22 +706,17 @@ public final class DownloadManager { ...@@ -654,22 +706,17 @@ public final class DownloadManager {
failureReason = FAILURE_REASON_NONE; failureReason = FAILURE_REASON_NONE;
if (!downloadThread.isCanceled) { if (!downloadThread.isCanceled) {
if (finalError != null) { if (finalError != null) {
state = STATE_FAILED;
failureReason = FAILURE_REASON_UNKNOWN; failureReason = FAILURE_REASON_UNKNOWN;
} else { setState(STATE_FAILED);
actionQueue.remove(); return;
if (!actionQueue.isEmpty()) {
// Don't continue running. Wait to be restarted by maybeStartDownloads().
state = STATE_QUEUED;
action = actionQueue.peek();
} else {
state = STATE_COMPLETED;
} }
actionQueue.remove();
} }
if (!actionQueue.isEmpty()) {
setActionAndUpdateState(actionQueue.peek());
} else { } else {
state = STATE_QUEUED; setState(STATE_COMPLETED);
} }
downloadManager.onDownloadStateChange(this);
} }
} }
...@@ -748,5 +795,4 @@ public final class DownloadManager { ...@@ -748,5 +795,4 @@ public final class DownloadManager {
return Math.min((errorCount - 1) * 1000, 5000); return Math.min((errorCount - 1) * 1000, 5000);
} }
} }
} }
...@@ -41,6 +41,7 @@ import org.mockito.MockitoAnnotations; ...@@ -41,6 +41,7 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
/** Tests {@link DownloadManager}. */ /** Tests {@link DownloadManager}. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
...@@ -69,6 +70,7 @@ public class DownloadManagerTest { ...@@ -69,6 +70,7 @@ public class DownloadManagerTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
ShadowLog.stream = System.out;
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
uri1 = Uri.parse("http://abc.com/media1"); uri1 = Uri.parse("http://abc.com/media1");
uri2 = Uri.parse("http://abc.com/media2"); uri2 = Uri.parse("http://abc.com/media2");
...@@ -314,6 +316,7 @@ public class DownloadManagerTest { ...@@ -314,6 +316,7 @@ public class DownloadManagerTest {
downloader1.assertStarted(); downloader1.assertStarted();
downloader2.assertDoesNotStart(); downloader2.assertDoesNotStart();
runner2.getTask().assertQueued();
downloader1.unblock(); downloader1.unblock();
downloader2.assertStarted(); downloader2.assertStarted();
downloader2.unblock(); downloader2.unblock();
...@@ -390,11 +393,11 @@ public class DownloadManagerTest { ...@@ -390,11 +393,11 @@ public class DownloadManagerTest {
runOnMainThread(() -> downloadManager.stopDownloads()); runOnMainThread(() -> downloadManager.stopDownloads());
runner1.getTask().assertQueued(); runner1.getTask().assertStopped();
// remove actions aren't stopped. // remove actions aren't stopped.
runner2.getDownloader(0).unblock().assertReleased(); runner2.getDownloader(0).unblock().assertReleased();
runner2.getTask().assertQueued(); runner2.getTask().assertStopped();
// Although remove2 is finished, download2 doesn't start. // Although remove2 is finished, download2 doesn't start.
runner2.getDownloader(1).assertDoesNotStart(); runner2.getDownloader(1).assertDoesNotStart();
...@@ -534,6 +537,10 @@ public class DownloadManagerTest { ...@@ -534,6 +537,10 @@ public class DownloadManagerTest {
return assertState(DownloadState.STATE_QUEUED); return assertState(DownloadState.STATE_QUEUED);
} }
private TaskWrapper assertStopped() {
return assertState(DownloadState.STATE_STOPPED);
}
private TaskWrapper assertState(@State int expectedState) { private TaskWrapper assertState(@State int expectedState) {
while (true) { while (true) {
Integer state = null; Integer state = null;
...@@ -542,6 +549,7 @@ public class DownloadManagerTest { ...@@ -542,6 +549,7 @@ public class DownloadManagerTest {
} catch (InterruptedException e) { } catch (InterruptedException e) {
fail(e.getMessage()); fail(e.getMessage());
} }
assertThat(state).isNotNull();
if (expectedState == state) { if (expectedState == state) {
return this; return this;
} }
......
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