Commit 5e311fc8 by eguven Committed by Oliver Woodman

Refactor DownloadManage to simplify DownloadThread management

Now DownloadManager is responsible for starting and stopping DownloadThreads.

PiperOrigin-RevId: 232278072
parent c61c0bd1
...@@ -34,6 +34,7 @@ import android.os.ConditionVariable; ...@@ -34,6 +34,7 @@ import android.os.ConditionVariable;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Requirements;
...@@ -42,8 +43,11 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -42,8 +43,11 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...@@ -101,16 +105,30 @@ public final class DownloadManager { ...@@ -101,16 +105,30 @@ public final class DownloadManager {
public static final Requirements DEFAULT_REQUIREMENTS = public static final Requirements DEFAULT_REQUIREMENTS =
new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
@Retention(RetentionPolicy.SOURCE)
@IntDef({
START_THREAD_SUCCEEDED,
START_THREAD_WAIT_REMOVAL_TO_FINISH,
START_THREAD_WAIT_DOWNLOAD_CANCELATION,
START_THREAD_TOO_MANY_DOWNLOADS
})
private @interface StartThreadResults {}
private static final int START_THREAD_SUCCEEDED = 0;
private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1;
private static final int START_THREAD_WAIT_DOWNLOAD_CANCELATION = 2;
private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3;
private static final String TAG = "DownloadManager"; private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private final int maxActiveDownloads; private final int maxSimultaneousDownloads;
private final int minRetryCount; private final int minRetryCount;
private final Context context; private final Context context;
private final ActionFile actionFile; private final ActionFile actionFile;
private final DownloaderFactory downloaderFactory; private final DownloaderFactory downloaderFactory;
private final ArrayList<Download> downloads; private final ArrayList<Download> downloads;
private final ArrayList<Download> activeDownloads; private final HashMap<Download, DownloadThread> activeDownloads;
private final Handler handler; private final Handler handler;
private final HandlerThread fileIOThread; private final HandlerThread fileIOThread;
private final Handler fileIOHandler; private final Handler fileIOHandler;
...@@ -122,6 +140,7 @@ public final class DownloadManager { ...@@ -122,6 +140,7 @@ public final class DownloadManager {
@DownloadState.StopFlags private int stickyStopFlags; @DownloadState.StopFlags private int stickyStopFlags;
@Requirements.RequirementFlags private int notMetRequirements; @Requirements.RequirementFlags private int notMetRequirements;
private RequirementsWatcher requirementsWatcher; private RequirementsWatcher requirementsWatcher;
private int simultaneousDownloads;
/** /**
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
...@@ -160,12 +179,12 @@ public final class DownloadManager { ...@@ -160,12 +179,12 @@ public final class DownloadManager {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.actionFile = new ActionFile(actionFile); this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.maxActiveDownloads = maxSimultaneousDownloads; this.maxSimultaneousDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
downloads = new ArrayList<>(); downloads = new ArrayList<>();
activeDownloads = new ArrayList<>(); activeDownloads = new HashMap<>();
Looper looper = Looper.myLooper(); Looper looper = Looper.myLooper();
if (looper == null) { if (looper == null) {
...@@ -310,16 +329,7 @@ public final class DownloadManager { ...@@ -310,16 +329,7 @@ public final class DownloadManager {
/** Returns whether there are no active downloads. */ /** Returns whether there are no active downloads. */
public boolean isIdle() { public boolean isIdle() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (!initialized || !activeDownloads.isEmpty()) { return initialized && activeDownloads.isEmpty();
return false;
}
// Still need to check all downloads as there might be remove tasks going on.
for (int i = 0; i < downloads.size(); i++) {
if (!downloads.get(i).isIdle()) {
return false;
}
}
return true;
} }
/** /**
...@@ -351,29 +361,11 @@ public final class DownloadManager { ...@@ -351,29 +361,11 @@ public final class DownloadManager {
return; return;
} }
} }
Download download = Download download = new Download(this, action, stickyStopFlags, notMetRequirements);
new Download(
this, downloaderFactory, action, minRetryCount, stickyStopFlags, notMetRequirements);
downloads.add(download); downloads.add(download);
logd("Download is added", download); logd("Download is added", download);
} }
private void maybeStartDownload(Download download) {
if (activeDownloads.size() < maxActiveDownloads) {
if (download.start()) {
activeDownloads.add(download);
}
}
}
private void maybeRestartDownload(Download download) {
if (activeDownloads.contains(download)) {
download.start();
} else {
maybeStartDownload(download);
}
}
private void maybeNotifyListenersIdle() { private void maybeNotifyListenersIdle() {
if (!isIdle()) { if (!isIdle()) {
return; return;
...@@ -388,21 +380,11 @@ public final class DownloadManager { ...@@ -388,21 +380,11 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
boolean idle = download.isIdle();
if (idle) {
activeDownloads.remove(download);
}
notifyListenersDownloadStateChange(download); notifyListenersDownloadStateChange(download);
if (download.isFinished()) { if (download.isFinished()) {
downloads.remove(download); downloads.remove(download);
saveActions(); saveActions();
} }
if (idle) {
for (int i = 0; i < downloads.size(); i++) {
maybeStartDownload(downloads.get(i));
}
maybeNotifyListenersIdle();
}
} }
private void notifyListenersDownloadStateChange(Download download) { private void notifyListenersDownloadStateChange(Download download) {
...@@ -513,34 +495,86 @@ public final class DownloadManager { ...@@ -513,34 +495,86 @@ public final class DownloadManager {
return notMetRequirements; return notMetRequirements;
} }
@StartThreadResults
private int startDownloadThread(Download download, DownloadAction action) {
if (activeDownloads.containsKey(download)) {
if (stopDownloadThread(download)) {
return START_THREAD_WAIT_DOWNLOAD_CANCELATION;
}
return START_THREAD_WAIT_REMOVAL_TO_FINISH;
}
if (!action.isRemoveAction) {
if (simultaneousDownloads == maxSimultaneousDownloads) {
return START_THREAD_TOO_MANY_DOWNLOADS;
}
simultaneousDownloads++;
}
Downloader downloader = downloaderFactory.createDownloader(action);
DownloadThread downloadThread = new DownloadThread(download, downloader, action.isRemoveAction);
activeDownloads.put(download, downloadThread);
logd("Download is started", download);
return START_THREAD_SUCCEEDED;
}
private boolean stopDownloadThread(Download download) {
DownloadThread downloadThread = activeDownloads.get(download);
if (downloadThread != null && !downloadThread.isRemoveThread) {
downloadThread.cancel();
logd("Download is cancelled", download);
return true;
}
return false;
}
private void onDownloadThreadStopped(DownloadThread downloadThread, Throwable finalError) {
Download download = downloadThread.download;
logd("Download is stopped", download);
activeDownloads.remove(download);
boolean tryToStartDownloads = false;
if (!downloadThread.isRemoveThread) {
// If maxSimultaneousDownloads was hit, there might be a download waiting for a slot.
tryToStartDownloads = simultaneousDownloads == maxSimultaneousDownloads;
simultaneousDownloads--;
}
download.onDownloadThreadStopped(downloadThread.isCanceled, finalError);
if (tryToStartDownloads) {
for (int i = 0;
simultaneousDownloads < maxSimultaneousDownloads && i < downloads.size();
i++) {
downloads.get(i).start();
}
}
maybeNotifyListenersIdle();
}
@Nullable
private Downloader getDownloader(Download download) {
DownloadThread downloadThread = activeDownloads.get(download);
if (downloadThread != null) {
return downloadThread.downloader;
}
return null;
}
private static final class Download { private static final class Download {
private final String id; private final String id;
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
private final int minRetryCount;
private final long startTimeMs; private final long startTimeMs;
private final ArrayDeque<DownloadAction> actionQueue; private final ArrayDeque<DownloadAction> actionQueue;
/** The current state of the download. */
@DownloadState.State private int state;
@MonotonicNonNull private Downloader downloader; @DownloadState.State private int state;
@MonotonicNonNull private DownloadThread downloadThread;
@MonotonicNonNull @DownloadState.FailureReason private int failureReason; @MonotonicNonNull @DownloadState.FailureReason private int failureReason;
@DownloadState.StopFlags private int stopFlags; @DownloadState.StopFlags private int stopFlags;
@Requirements.RequirementFlags private int notMetRequirements; @Requirements.RequirementFlags private int notMetRequirements;
private Download( private Download(
DownloadManager downloadManager, DownloadManager downloadManager,
DownloaderFactory downloaderFactory,
DownloadAction action, DownloadAction action,
int minRetryCount,
@DownloadState.StopFlags int stopFlags, @DownloadState.StopFlags int stopFlags,
@Requirements.RequirementFlags int notMetRequirements) { @Requirements.RequirementFlags int notMetRequirements) {
this.id = action.id; this.id = action.id;
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory;
this.minRetryCount = minRetryCount;
this.notMetRequirements = notMetRequirements; this.notMetRequirements = notMetRequirements;
if (notMetRequirements != 0) { if (notMetRequirements != 0) {
stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET; stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET;
...@@ -549,7 +583,14 @@ public final class DownloadManager { ...@@ -549,7 +583,14 @@ public final class DownloadManager {
this.startTimeMs = System.currentTimeMillis(); this.startTimeMs = System.currentTimeMillis();
actionQueue = new ArrayDeque<>(); actionQueue = new ArrayDeque<>();
actionQueue.add(action); actionQueue.add(action);
// Set to queued state but don't notify listeners until we make sure we don't switch to
// another state immediately.
state = STATE_QUEUED;
initialize(); initialize();
if (state == STATE_QUEUED) {
downloadManager.onDownloadStateChange(this);
}
} }
public boolean addAction(DownloadAction newAction) { public boolean addAction(DownloadAction newAction) {
...@@ -573,9 +614,6 @@ public final class DownloadManager { ...@@ -573,9 +614,6 @@ public final class DownloadManager {
} else if (!action.equals(updatedAction)) { } else if (!action.equals(updatedAction)) {
Assertions.checkState( Assertions.checkState(
state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED); state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED);
if (state == STATE_DOWNLOADING) {
stopDownloadThread();
}
initialize(); initialize();
} }
return true; return true;
...@@ -585,6 +623,7 @@ public final class DownloadManager { ...@@ -585,6 +623,7 @@ public final class DownloadManager {
float downloadPercentage = C.PERCENTAGE_UNSET; float downloadPercentage = C.PERCENTAGE_UNSET;
long downloadedBytes = 0; long downloadedBytes = 0;
long totalBytes = C.LENGTH_UNSET; long totalBytes = C.LENGTH_UNSET;
Downloader downloader = downloadManager.getDownloader(this);
if (downloader != null) { if (downloader != null) {
downloadPercentage = downloader.getDownloadPercentage(); downloadPercentage = downloader.getDownloadPercentage();
downloadedBytes = downloader.getDownloadedBytes(); downloadedBytes = downloader.getDownloadedBytes();
...@@ -622,13 +661,10 @@ public final class DownloadManager { ...@@ -622,13 +661,10 @@ public final class DownloadManager {
return id + ' ' + DownloadState.getStateString(state); return id + ' ' + DownloadState.getStateString(state);
} }
public boolean start() { public void start() {
if (state != STATE_QUEUED) { if (state == STATE_QUEUED) {
return false; startOrQueue();
} }
startDownloadThread(actionQueue.peek());
setState(STATE_DOWNLOADING);
return true;
} }
public void setStopFlags(int flags) { public void setStopFlags(int flags) {
...@@ -643,9 +679,7 @@ public final class DownloadManager { ...@@ -643,9 +679,7 @@ public final class DownloadManager {
stopFlags = (values & flags) | (stopFlags & ~flags); stopFlags = (values & flags) | (stopFlags & ~flags);
if (stopFlags != 0) { if (stopFlags != 0) {
if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) {
if (state == STATE_DOWNLOADING) { downloadManager.stopDownloadThread(this);
stopDownloadThread();
}
setState(STATE_STOPPED); setState(STATE_STOPPED);
} }
} else if (state == STATE_STOPPED) { } else if (state == STATE_STOPPED) {
...@@ -664,7 +698,9 @@ public final class DownloadManager { ...@@ -664,7 +698,9 @@ public final class DownloadManager {
DownloadAction action = actionQueue.peek(); DownloadAction action = actionQueue.peek();
if (action.isRemoveAction) { if (action.isRemoveAction) {
if (!downloadManager.released) { if (!downloadManager.released) {
startDownloadThread(action); int result = downloadManager.startDownloadThread(this, action);
Assertions.checkState(
result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELATION);
} }
setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING); setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING);
} else if (stopFlags != 0) { } else if (stopFlags != 0) {
...@@ -675,42 +711,29 @@ public final class DownloadManager { ...@@ -675,42 +711,29 @@ public final class DownloadManager {
} }
private void startOrQueue() { private void startOrQueue() {
// Set to queued state but don't notify listeners until we make sure we can't start now. DownloadAction action = Assertions.checkNotNull(actionQueue.peek());
state = STATE_QUEUED; Assertions.checkState(!action.isRemoveAction);
downloadManager.maybeRestartDownload(this); @StartThreadResults int result = downloadManager.startDownloadThread(this, action);
if (state == STATE_QUEUED) { Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH);
downloadManager.onDownloadStateChange(this); if (result == START_THREAD_TOO_MANY_DOWNLOADS) {
setState(STATE_QUEUED);
} else {
setState(STATE_DOWNLOADING);
} }
} }
private void setState(@DownloadState.State int newState) { private void setState(@DownloadState.State int newState) {
state = newState; if (state != newState) {
downloadManager.onDownloadStateChange(this); state = newState;
} downloadManager.onDownloadStateChange(this);
private void startDownloadThread(DownloadAction action) {
if (downloadThread != null) {
return;
}
downloader = downloaderFactory.createDownloader(action);
downloadThread =
new DownloadThread(
this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
}
private void stopDownloadThread() {
if (!downloadThread.isRemoveThread) {
Assertions.checkNotNull(downloadThread).cancel();
} }
} }
private void onDownloadThreadStopped(@Nullable Throwable error) { private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) {
failureReason = FAILURE_REASON_NONE; failureReason = FAILURE_REASON_NONE;
boolean isCanceled = downloadThread.isCanceled;
downloadThread = null;
if (isCanceled) { if (isCanceled) {
if (!isIdle()) { if (!isIdle()) {
startDownloadThread(actionQueue.peek()); downloadManager.startDownloadThread(this, actionQueue.peek());
} }
return; return;
} }
...@@ -733,27 +756,18 @@ public final class DownloadManager { ...@@ -733,27 +756,18 @@ public final class DownloadManager {
} }
} }
private static class DownloadThread implements Runnable { private class DownloadThread implements Runnable {
private final Download download; private final Download download;
private final Downloader downloader; private final Downloader downloader;
private final boolean isRemoveThread; private final boolean isRemoveThread;
private final int minRetryCount;
private final Handler callbackHandler;
private final Thread thread; private final Thread thread;
private volatile boolean isCanceled; private volatile boolean isCanceled;
private DownloadThread( private DownloadThread(Download download, Downloader downloader, boolean isRemoveThread) {
Download download,
Downloader downloader,
boolean isRemoveThread,
int minRetryCount,
Handler callbackHandler) {
this.download = download; this.download = download;
this.downloader = downloader; this.downloader = downloader;
this.isRemoveThread = isRemoveThread; this.isRemoveThread = isRemoveThread;
this.minRetryCount = minRetryCount;
this.callbackHandler = callbackHandler;
thread = new Thread(this); thread = new Thread(this);
thread.start(); thread.start();
} }
...@@ -768,7 +782,7 @@ public final class DownloadManager { ...@@ -768,7 +782,7 @@ public final class DownloadManager {
@Override @Override
public void run() { public void run() {
logd("Download is started", download); logd("Download started", download);
Throwable error = null; Throwable error = null;
try { try {
if (isRemoveThread) { if (isRemoveThread) {
...@@ -801,7 +815,7 @@ public final class DownloadManager { ...@@ -801,7 +815,7 @@ public final class DownloadManager {
error = e; error = e;
} }
final Throwable finalError = error; final Throwable finalError = error;
callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError)); handler.post(() -> onDownloadThreadStopped(this, finalError));
} }
private int getRetryDelayMillis(int errorCount) { private int getRetryDelayMillis(int errorCount) {
......
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