summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java1346
1 files changed, 1346 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java
new file mode 100644
index 0000000000..a6ace12343
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -0,0 +1,1346 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.FAILURE_REASON_NONE;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.FAILURE_REASON_UNKNOWN;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_COMPLETED;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_DOWNLOADING;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_FAILED;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_QUEUED;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_REMOVING;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_RESTARTING;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_STOPPED;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import androidx.annotation.CheckResult;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.Requirements;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.RequirementsWatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheEvictor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Manages downloads.
+ *
+ * <p>Normally a download manager should be accessed via a {@link DownloadService}. When a download
+ * manager is used directly instead, downloads will be initially paused and so must be resumed by
+ * calling {@link #resumeDownloads()}.
+ *
+ * <p>A download manager instance must be accessed only from the thread that created it, unless that
+ * thread does not have a {@link Looper}. In that case, it must be accessed only from the
+ * application's main thread. Registered listeners will be called on the same thread.
+ */
+public final class DownloadManager {
+
+ /** Listener for {@link DownloadManager} events. */
+ public interface Listener {
+
+ /**
+ * Called when all downloads have been restored.
+ *
+ * @param downloadManager The reporting instance.
+ */
+ default void onInitialized(DownloadManager downloadManager) {}
+
+ /**
+ * Called when downloads are ({@link #pauseDownloads() paused} or {@link #resumeDownloads()
+ * resumed}.
+ *
+ * @param downloadManager The reporting instance.
+ * @param downloadsPaused Whether downloads are currently paused.
+ */
+ default void onDownloadsPausedChanged(
+ DownloadManager downloadManager, boolean downloadsPaused) {}
+
+ /**
+ * Called when the state of a download changes.
+ *
+ * @param downloadManager The reporting instance.
+ * @param download The state of the download.
+ */
+ default void onDownloadChanged(DownloadManager downloadManager, Download download) {}
+
+ /**
+ * Called when a download is removed.
+ *
+ * @param downloadManager The reporting instance.
+ * @param download The last state of the download before it was removed.
+ */
+ default void onDownloadRemoved(DownloadManager downloadManager, Download download) {}
+
+ /**
+ * Called when there is no active download left.
+ *
+ * @param downloadManager The reporting instance.
+ */
+ default void onIdle(DownloadManager downloadManager) {}
+
+ /**
+ * Called when the download requirements state changed.
+ *
+ * @param downloadManager The reporting instance.
+ * @param requirements Requirements needed to be met to start downloads.
+ * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
+ * met, or 0.
+ */
+ default void onRequirementsStateChanged(
+ DownloadManager downloadManager,
+ Requirements requirements,
+ @Requirements.RequirementFlags int notMetRequirements) {}
+
+ /**
+ * Called when there is a change in whether this manager has one or more downloads that are not
+ * progressing for the sole reason that the {@link #getRequirements() Requirements} are not met.
+ * See {@link #isWaitingForRequirements()} for more information.
+ *
+ * @param downloadManager The reporting instance.
+ * @param waitingForRequirements Whether this manager has one or more downloads that are not
+ * progressing for the sole reason that the {@link #getRequirements() Requirements} are not
+ * met.
+ */
+ default void onWaitingForRequirementsChanged(
+ DownloadManager downloadManager, boolean waitingForRequirements) {}
+ }
+
+ /** The default maximum number of parallel downloads. */
+ public static final int DEFAULT_MAX_PARALLEL_DOWNLOADS = 3;
+ /** The default minimum number of times a download must be retried before failing. */
+ public static final int DEFAULT_MIN_RETRY_COUNT = 5;
+ /** The default requirement is that the device has network connectivity. */
+ public static final Requirements DEFAULT_REQUIREMENTS = new Requirements(Requirements.NETWORK);
+
+ // Messages posted to the main handler.
+ private static final int MSG_INITIALIZED = 0;
+ private static final int MSG_PROCESSED = 1;
+ private static final int MSG_DOWNLOAD_UPDATE = 2;
+
+ // Messages posted to the background handler.
+ private static final int MSG_INITIALIZE = 0;
+ private static final int MSG_SET_DOWNLOADS_PAUSED = 1;
+ private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2;
+ private static final int MSG_SET_STOP_REASON = 3;
+ private static final int MSG_SET_MAX_PARALLEL_DOWNLOADS = 4;
+ private static final int MSG_SET_MIN_RETRY_COUNT = 5;
+ private static final int MSG_ADD_DOWNLOAD = 6;
+ private static final int MSG_REMOVE_DOWNLOAD = 7;
+ private static final int MSG_REMOVE_ALL_DOWNLOADS = 8;
+ private static final int MSG_TASK_STOPPED = 9;
+ private static final int MSG_CONTENT_LENGTH_CHANGED = 10;
+ private static final int MSG_UPDATE_PROGRESS = 11;
+ private static final int MSG_RELEASE = 12;
+
+ private static final String TAG = "DownloadManager";
+
+ private final Context context;
+ private final WritableDownloadIndex downloadIndex;
+ private final Handler mainHandler;
+ private final InternalHandler internalHandler;
+ private final RequirementsWatcher.Listener requirementsListener;
+ private final CopyOnWriteArraySet<Listener> listeners;
+
+ private int pendingMessages;
+ private int activeTaskCount;
+ private boolean initialized;
+ private boolean downloadsPaused;
+ private int maxParallelDownloads;
+ private int minRetryCount;
+ private int notMetRequirements;
+ private boolean waitingForRequirements;
+ private List<Download> downloads;
+ private RequirementsWatcher requirementsWatcher;
+
+ /**
+ * Constructs a {@link DownloadManager}.
+ *
+ * @param context Any context.
+ * @param databaseProvider Provides the SQLite database in which downloads are persisted.
+ * @param cache A cache to be used to store downloaded data. The cache should be configured with
+ * an {@link CacheEvictor} that will not evict downloaded content, for example {@link
+ * NoOpCacheEvictor}.
+ * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data.
+ */
+ public DownloadManager(
+ Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) {
+ this(
+ context,
+ new DefaultDownloadIndex(databaseProvider),
+ new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory)));
+ }
+
+ /**
+ * Constructs a {@link DownloadManager}.
+ *
+ * @param context Any context.
+ * @param downloadIndex The download index used to hold the download information.
+ * @param downloaderFactory A factory for creating {@link Downloader}s.
+ */
+ public DownloadManager(
+ Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) {
+ this.context = context.getApplicationContext();
+ this.downloadIndex = downloadIndex;
+
+ maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS;
+ minRetryCount = DEFAULT_MIN_RETRY_COUNT;
+ downloadsPaused = true;
+ downloads = Collections.emptyList();
+ listeners = new CopyOnWriteArraySet<>();
+
+ @SuppressWarnings("methodref.receiver.bound.invalid")
+ Handler mainHandler = Util.createHandler(this::handleMainMessage);
+ this.mainHandler = mainHandler;
+ HandlerThread internalThread = new HandlerThread("DownloadManager file i/o");
+ internalThread.start();
+ internalHandler =
+ new InternalHandler(
+ internalThread,
+ downloadIndex,
+ downloaderFactory,
+ mainHandler,
+ maxParallelDownloads,
+ minRetryCount,
+ downloadsPaused);
+
+ @SuppressWarnings("methodref.receiver.bound.invalid")
+ RequirementsWatcher.Listener requirementsListener = this::onRequirementsStateChanged;
+ this.requirementsListener = requirementsListener;
+ requirementsWatcher =
+ new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS);
+ notMetRequirements = requirementsWatcher.start();
+
+ pendingMessages = 1;
+ internalHandler
+ .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0)
+ .sendToTarget();
+ }
+
+ /** Returns whether the manager has completed initialization. */
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ /**
+ * Returns whether the manager is currently idle. The manager is idle if all downloads are in a
+ * terminal state (i.e. completed or failed), or if no progress can be made (e.g. because the
+ * download requirements are not met).
+ */
+ public boolean isIdle() {
+ return activeTaskCount == 0 && pendingMessages == 0;
+ }
+
+ /**
+ * Returns whether this manager has one or more downloads that are not progressing for the sole
+ * reason that the {@link #getRequirements() Requirements} are not met. This is true if:
+ *
+ * <ul>
+ * <li>The {@link #getRequirements() Requirements} are not met.
+ * <li>The downloads are not paused (i.e. {@link #getDownloadsPaused()} is {@code false}).
+ * <li>There are downloads in the {@link Download#STATE_QUEUED queued state}.
+ * </ul>
+ */
+ public boolean isWaitingForRequirements() {
+ return waitingForRequirements;
+ }
+
+ /**
+ * Adds a {@link Listener}.
+ *
+ * @param listener The listener to be added.
+ */
+ public void addListener(Listener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link Listener}.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeListener(Listener listener) {
+ listeners.remove(listener);
+ }
+
+ /** Returns the requirements needed to be met to progress. */
+ public Requirements getRequirements() {
+ return requirementsWatcher.getRequirements();
+ }
+
+ /**
+ * Returns the requirements needed for downloads to progress that are not currently met.
+ *
+ * @return The not met {@link Requirements.RequirementFlags}, or 0 if all requirements are met.
+ */
+ @Requirements.RequirementFlags
+ public int getNotMetRequirements() {
+ return notMetRequirements;
+ }
+
+ /**
+ * Sets the requirements that need to be met for downloads to progress.
+ *
+ * @param requirements A {@link Requirements}.
+ */
+ public void setRequirements(Requirements requirements) {
+ if (requirements.equals(requirementsWatcher.getRequirements())) {
+ return;
+ }
+ requirementsWatcher.stop();
+ requirementsWatcher = new RequirementsWatcher(context, requirementsListener, requirements);
+ int notMetRequirements = requirementsWatcher.start();
+ onRequirementsStateChanged(requirementsWatcher, notMetRequirements);
+ }
+
+ /** Returns the maximum number of parallel downloads. */
+ public int getMaxParallelDownloads() {
+ return maxParallelDownloads;
+ }
+
+ /**
+ * Sets the maximum number of parallel downloads.
+ *
+ * @param maxParallelDownloads The maximum number of parallel downloads. Must be greater than 0.
+ */
+ public void setMaxParallelDownloads(int maxParallelDownloads) {
+ Assertions.checkArgument(maxParallelDownloads > 0);
+ if (this.maxParallelDownloads == maxParallelDownloads) {
+ return;
+ }
+ this.maxParallelDownloads = maxParallelDownloads;
+ pendingMessages++;
+ internalHandler
+ .obtainMessage(MSG_SET_MAX_PARALLEL_DOWNLOADS, maxParallelDownloads, /* unused */ 0)
+ .sendToTarget();
+ }
+
+ /**
+ * Returns the minimum number of times that a download will be retried. A download will fail if
+ * the specified number of retries is exceeded without any progress being made.
+ */
+ public int getMinRetryCount() {
+ return minRetryCount;
+ }
+
+ /**
+ * Sets the minimum number of times that a download will be retried. A download will fail if the
+ * specified number of retries is exceeded without any progress being made.
+ *
+ * @param minRetryCount The minimum number of times that a download will be retried.
+ */
+ public void setMinRetryCount(int minRetryCount) {
+ Assertions.checkArgument(minRetryCount >= 0);
+ if (this.minRetryCount == minRetryCount) {
+ return;
+ }
+ this.minRetryCount = minRetryCount;
+ pendingMessages++;
+ internalHandler
+ .obtainMessage(MSG_SET_MIN_RETRY_COUNT, minRetryCount, /* unused */ 0)
+ .sendToTarget();
+ }
+
+ /** Returns the used {@link DownloadIndex}. */
+ public DownloadIndex getDownloadIndex() {
+ return downloadIndex;
+ }
+
+ /**
+ * Returns current downloads. Downloads that are in terminal states (i.e. completed or failed) are
+ * not included. To query all downloads including those in terminal states, use {@link
+ * #getDownloadIndex()} instead.
+ */
+ public List<Download> getCurrentDownloads() {
+ return downloads;
+ }
+
+ /** Returns whether downloads are currently paused. */
+ public boolean getDownloadsPaused() {
+ return downloadsPaused;
+ }
+
+ /**
+ * Resumes downloads.
+ *
+ * <p>If the {@link #setRequirements(Requirements) Requirements} are met up to {@link
+ * #getMaxParallelDownloads() maxParallelDownloads} will be started, excluding those with non-zero
+ * {@link Download#stopReason stopReasons}.
+ */
+ public void resumeDownloads() {
+ setDownloadsPaused(/* downloadsPaused= */ false);
+ }
+
+ /**
+ * Pauses downloads. Downloads that would otherwise be making progress will transition to {@link
+ * Download#STATE_QUEUED}.
+ */
+ public void pauseDownloads() {
+ setDownloadsPaused(/* downloadsPaused= */ true);
+ }
+
+ /**
+ * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link
+ * Download#STOP_REASON_NONE}.
+ *
+ * @param id The content id of the download to update, or {@code null} to set the stop reason for
+ * all downloads.
+ * @param stopReason The stop reason, or {@link Download#STOP_REASON_NONE}.
+ */
+ public void setStopReason(@Nullable String id, int stopReason) {
+ pendingMessages++;
+ internalHandler
+ .obtainMessage(MSG_SET_STOP_REASON, stopReason, /* unused */ 0, id)
+ .sendToTarget();
+ }
+
+ /**
+ * Adds a download defined by the given request.
+ *
+ * @param request The download request.
+ */
+ public void addDownload(DownloadRequest request) {
+ addDownload(request, STOP_REASON_NONE);
+ }
+
+ /**
+ * Adds a download defined by the given request and with the specified stop reason.
+ *
+ * @param request The download request.
+ * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE}
+ * if the download should be started.
+ */
+ public void addDownload(DownloadRequest request, int stopReason) {
+ pendingMessages++;
+ internalHandler
+ .obtainMessage(MSG_ADD_DOWNLOAD, stopReason, /* unused */ 0, request)
+ .sendToTarget();
+ }
+
+ /**
+ * Cancels the download with the {@code id} and removes all downloaded data.
+ *
+ * @param id The unique content id of the download to be started.
+ */
+ public void removeDownload(String id) {
+ pendingMessages++;
+ internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget();
+ }
+
+ /** Cancels all pending downloads and removes all downloaded data. */
+ public void removeAllDownloads() {
+ pendingMessages++;
+ internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget();
+ }
+
+ /**
+ * Stops the downloads and releases resources. Waits until the downloads are persisted to the
+ * download index. The manager must not be accessed after this method has been called.
+ */
+ public void release() {
+ synchronized (internalHandler) {
+ if (internalHandler.released) {
+ return;
+ }
+ internalHandler.sendEmptyMessage(MSG_RELEASE);
+ boolean wasInterrupted = false;
+ while (!internalHandler.released) {
+ try {
+ internalHandler.wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ mainHandler.removeCallbacksAndMessages(/* token= */ null);
+ // Reset state.
+ downloads = Collections.emptyList();
+ pendingMessages = 0;
+ activeTaskCount = 0;
+ initialized = false;
+ notMetRequirements = 0;
+ waitingForRequirements = false;
+ }
+ }
+
+ private void setDownloadsPaused(boolean downloadsPaused) {
+ if (this.downloadsPaused == downloadsPaused) {
+ return;
+ }
+ this.downloadsPaused = downloadsPaused;
+ pendingMessages++;
+ internalHandler
+ .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, downloadsPaused ? 1 : 0, /* unused */ 0)
+ .sendToTarget();
+ boolean waitingForRequirementsChanged = updateWaitingForRequirements();
+ for (Listener listener : listeners) {
+ listener.onDownloadsPausedChanged(this, downloadsPaused);
+ }
+ if (waitingForRequirementsChanged) {
+ notifyWaitingForRequirementsChanged();
+ }
+ }
+
+ private void onRequirementsStateChanged(
+ RequirementsWatcher requirementsWatcher,
+ @Requirements.RequirementFlags int notMetRequirements) {
+ Requirements requirements = requirementsWatcher.getRequirements();
+ if (this.notMetRequirements != notMetRequirements) {
+ this.notMetRequirements = notMetRequirements;
+ pendingMessages++;
+ internalHandler
+ .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0)
+ .sendToTarget();
+ }
+ boolean waitingForRequirementsChanged = updateWaitingForRequirements();
+ for (Listener listener : listeners) {
+ listener.onRequirementsStateChanged(this, requirements, notMetRequirements);
+ }
+ if (waitingForRequirementsChanged) {
+ notifyWaitingForRequirementsChanged();
+ }
+ }
+
+ private boolean updateWaitingForRequirements() {
+ boolean waitingForRequirements = false;
+ if (!downloadsPaused && notMetRequirements != 0) {
+ for (int i = 0; i < downloads.size(); i++) {
+ if (downloads.get(i).state == STATE_QUEUED) {
+ waitingForRequirements = true;
+ break;
+ }
+ }
+ }
+ boolean waitingForRequirementsChanged = this.waitingForRequirements != waitingForRequirements;
+ this.waitingForRequirements = waitingForRequirements;
+ return waitingForRequirementsChanged;
+ }
+
+ private void notifyWaitingForRequirementsChanged() {
+ for (Listener listener : listeners) {
+ listener.onWaitingForRequirementsChanged(this, waitingForRequirements);
+ }
+ }
+
+ // Main thread message handling.
+
+ @SuppressWarnings("unchecked")
+ private boolean handleMainMessage(Message message) {
+ switch (message.what) {
+ case MSG_INITIALIZED:
+ List<Download> downloads = (List<Download>) message.obj;
+ onInitialized(downloads);
+ break;
+ case MSG_DOWNLOAD_UPDATE:
+ DownloadUpdate update = (DownloadUpdate) message.obj;
+ onDownloadUpdate(update);
+ break;
+ case MSG_PROCESSED:
+ int processedMessageCount = message.arg1;
+ int activeTaskCount = message.arg2;
+ onMessageProcessed(processedMessageCount, activeTaskCount);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ return true;
+ }
+
+ private void onInitialized(List<Download> downloads) {
+ initialized = true;
+ this.downloads = Collections.unmodifiableList(downloads);
+ boolean waitingForRequirementsChanged = updateWaitingForRequirements();
+ for (Listener listener : listeners) {
+ listener.onInitialized(DownloadManager.this);
+ }
+ if (waitingForRequirementsChanged) {
+ notifyWaitingForRequirementsChanged();
+ }
+ }
+
+ private void onDownloadUpdate(DownloadUpdate update) {
+ downloads = Collections.unmodifiableList(update.downloads);
+ Download updatedDownload = update.download;
+ boolean waitingForRequirementsChanged = updateWaitingForRequirements();
+ if (update.isRemove) {
+ for (Listener listener : listeners) {
+ listener.onDownloadRemoved(this, updatedDownload);
+ }
+ } else {
+ for (Listener listener : listeners) {
+ listener.onDownloadChanged(this, updatedDownload);
+ }
+ }
+ if (waitingForRequirementsChanged) {
+ notifyWaitingForRequirementsChanged();
+ }
+ }
+
+ private void onMessageProcessed(int processedMessageCount, int activeTaskCount) {
+ this.pendingMessages -= processedMessageCount;
+ this.activeTaskCount = activeTaskCount;
+ if (isIdle()) {
+ for (Listener listener : listeners) {
+ listener.onIdle(this);
+ }
+ }
+ }
+
+ /* package */ static Download mergeRequest(
+ Download download, DownloadRequest request, int stopReason, long nowMs) {
+ @Download.State int state = download.state;
+ // Treat the merge as creating a new download if we're currently removing the existing one, or
+ // if the existing download is in a terminal state. Else treat the merge as updating the
+ // existing download.
+ long startTimeMs =
+ state == STATE_REMOVING || download.isTerminalState() ? nowMs : download.startTimeMs;
+ if (state == STATE_REMOVING || state == STATE_RESTARTING) {
+ state = STATE_RESTARTING;
+ } else if (stopReason != STOP_REASON_NONE) {
+ state = STATE_STOPPED;
+ } else {
+ state = STATE_QUEUED;
+ }
+ return new Download(
+ download.request.copyWithMergedRequest(request),
+ state,
+ startTimeMs,
+ /* updateTimeMs= */ nowMs,
+ /* contentLength= */ C.LENGTH_UNSET,
+ stopReason,
+ FAILURE_REASON_NONE);
+ }
+
+ private static final class InternalHandler extends Handler {
+
+ private static final int UPDATE_PROGRESS_INTERVAL_MS = 5000;
+
+ public boolean released;
+
+ private final HandlerThread thread;
+ private final WritableDownloadIndex downloadIndex;
+ private final DownloaderFactory downloaderFactory;
+ private final Handler mainHandler;
+ private final ArrayList<Download> downloads;
+ private final HashMap<String, Task> activeTasks;
+
+ @Requirements.RequirementFlags private int notMetRequirements;
+ private boolean downloadsPaused;
+ private int maxParallelDownloads;
+ private int minRetryCount;
+ private int activeDownloadTaskCount;
+
+ public InternalHandler(
+ HandlerThread thread,
+ WritableDownloadIndex downloadIndex,
+ DownloaderFactory downloaderFactory,
+ Handler mainHandler,
+ int maxParallelDownloads,
+ int minRetryCount,
+ boolean downloadsPaused) {
+ super(thread.getLooper());
+ this.thread = thread;
+ this.downloadIndex = downloadIndex;
+ this.downloaderFactory = downloaderFactory;
+ this.mainHandler = mainHandler;
+ this.maxParallelDownloads = maxParallelDownloads;
+ this.minRetryCount = minRetryCount;
+ this.downloadsPaused = downloadsPaused;
+ downloads = new ArrayList<>();
+ activeTasks = new HashMap<>();
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ boolean processedExternalMessage = true;
+ switch (message.what) {
+ case MSG_INITIALIZE:
+ int notMetRequirements = message.arg1;
+ initialize(notMetRequirements);
+ break;
+ case MSG_SET_DOWNLOADS_PAUSED:
+ boolean downloadsPaused = message.arg1 != 0;
+ setDownloadsPaused(downloadsPaused);
+ break;
+ case MSG_SET_NOT_MET_REQUIREMENTS:
+ notMetRequirements = message.arg1;
+ setNotMetRequirements(notMetRequirements);
+ break;
+ case MSG_SET_STOP_REASON:
+ String id = (String) message.obj;
+ int stopReason = message.arg1;
+ setStopReason(id, stopReason);
+ break;
+ case MSG_SET_MAX_PARALLEL_DOWNLOADS:
+ int maxParallelDownloads = message.arg1;
+ setMaxParallelDownloads(maxParallelDownloads);
+ break;
+ case MSG_SET_MIN_RETRY_COUNT:
+ int minRetryCount = message.arg1;
+ setMinRetryCount(minRetryCount);
+ break;
+ case MSG_ADD_DOWNLOAD:
+ DownloadRequest request = (DownloadRequest) message.obj;
+ stopReason = message.arg1;
+ addDownload(request, stopReason);
+ break;
+ case MSG_REMOVE_DOWNLOAD:
+ id = (String) message.obj;
+ removeDownload(id);
+ break;
+ case MSG_REMOVE_ALL_DOWNLOADS:
+ removeAllDownloads();
+ break;
+ case MSG_TASK_STOPPED:
+ Task task = (Task) message.obj;
+ onTaskStopped(task);
+ processedExternalMessage = false; // This message is posted internally.
+ break;
+ case MSG_CONTENT_LENGTH_CHANGED:
+ task = (Task) message.obj;
+ onContentLengthChanged(task);
+ return; // No need to post back to mainHandler.
+ case MSG_UPDATE_PROGRESS:
+ updateProgress();
+ return; // No need to post back to mainHandler.
+ case MSG_RELEASE:
+ release();
+ return; // No need to post back to mainHandler.
+ default:
+ throw new IllegalStateException();
+ }
+ mainHandler
+ .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, activeTasks.size())
+ .sendToTarget();
+ }
+
+ private void initialize(int notMetRequirements) {
+ this.notMetRequirements = notMetRequirements;
+ DownloadCursor cursor = null;
+ try {
+ downloadIndex.setDownloadingStatesToQueued();
+ cursor =
+ downloadIndex.getDownloads(
+ STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING);
+ while (cursor.moveToNext()) {
+ downloads.add(cursor.getDownload());
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to load index.", e);
+ downloads.clear();
+ } finally {
+ Util.closeQuietly(cursor);
+ }
+ // A copy must be used for the message to ensure that subsequent changes to the downloads list
+ // are not visible to the main thread when it processes the message.
+ ArrayList<Download> downloadsForMessage = new ArrayList<>(downloads);
+ mainHandler.obtainMessage(MSG_INITIALIZED, downloadsForMessage).sendToTarget();
+ syncTasks();
+ }
+
+ private void setDownloadsPaused(boolean downloadsPaused) {
+ this.downloadsPaused = downloadsPaused;
+ syncTasks();
+ }
+
+ private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
+ this.notMetRequirements = notMetRequirements;
+ syncTasks();
+ }
+
+ private void setStopReason(@Nullable String id, int stopReason) {
+ if (id == null) {
+ for (int i = 0; i < downloads.size(); i++) {
+ setStopReason(downloads.get(i), stopReason);
+ }
+ try {
+ // Set the stop reason for downloads in terminal states as well.
+ downloadIndex.setStopReason(stopReason);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to set manual stop reason", e);
+ }
+ } else {
+ @Nullable Download download = getDownload(id, /* loadFromIndex= */ false);
+ if (download != null) {
+ setStopReason(download, stopReason);
+ } else {
+ try {
+ // Set the stop reason if the download is in a terminal state.
+ downloadIndex.setStopReason(id, stopReason);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to set manual stop reason: " + id, e);
+ }
+ }
+ }
+ syncTasks();
+ }
+
+ private void setStopReason(Download download, int stopReason) {
+ if (stopReason == STOP_REASON_NONE) {
+ if (download.state == STATE_STOPPED) {
+ putDownloadWithState(download, STATE_QUEUED);
+ }
+ } else if (stopReason != download.stopReason) {
+ @Download.State int state = download.state;
+ if (state == STATE_QUEUED || state == STATE_DOWNLOADING) {
+ state = STATE_STOPPED;
+ }
+ putDownload(
+ new Download(
+ download.request,
+ state,
+ download.startTimeMs,
+ /* updateTimeMs= */ System.currentTimeMillis(),
+ download.contentLength,
+ stopReason,
+ FAILURE_REASON_NONE,
+ download.progress));
+ }
+ }
+
+ private void setMaxParallelDownloads(int maxParallelDownloads) {
+ this.maxParallelDownloads = maxParallelDownloads;
+ syncTasks();
+ }
+
+ private void setMinRetryCount(int minRetryCount) {
+ this.minRetryCount = minRetryCount;
+ }
+
+ private void addDownload(DownloadRequest request, int stopReason) {
+ @Nullable Download download = getDownload(request.id, /* loadFromIndex= */ true);
+ long nowMs = System.currentTimeMillis();
+ if (download != null) {
+ putDownload(mergeRequest(download, request, stopReason, nowMs));
+ } else {
+ putDownload(
+ new Download(
+ request,
+ stopReason != STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED,
+ /* startTimeMs= */ nowMs,
+ /* updateTimeMs= */ nowMs,
+ /* contentLength= */ C.LENGTH_UNSET,
+ stopReason,
+ FAILURE_REASON_NONE));
+ }
+ syncTasks();
+ }
+
+ private void removeDownload(String id) {
+ @Nullable Download download = getDownload(id, /* loadFromIndex= */ true);
+ if (download == null) {
+ Log.e(TAG, "Failed to remove nonexistent download: " + id);
+ return;
+ }
+ putDownloadWithState(download, STATE_REMOVING);
+ syncTasks();
+ }
+
+ private void removeAllDownloads() {
+ List<Download> terminalDownloads = new ArrayList<>();
+ try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) {
+ while (cursor.moveToNext()) {
+ terminalDownloads.add(cursor.getDownload());
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to load downloads.");
+ }
+ for (int i = 0; i < downloads.size(); i++) {
+ downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING));
+ }
+ for (int i = 0; i < terminalDownloads.size(); i++) {
+ downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING));
+ }
+ Collections.sort(downloads, InternalHandler::compareStartTimes);
+ try {
+ downloadIndex.setStatesToRemoving();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to update index.", e);
+ }
+ ArrayList<Download> updateList = new ArrayList<>(downloads);
+ for (int i = 0; i < downloads.size(); i++) {
+ DownloadUpdate update =
+ new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList);
+ mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();
+ }
+ syncTasks();
+ }
+
+ private void release() {
+ for (Task task : activeTasks.values()) {
+ task.cancel(/* released= */ true);
+ }
+ try {
+ downloadIndex.setDownloadingStatesToQueued();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to update index.", e);
+ }
+ downloads.clear();
+ thread.quit();
+ synchronized (this) {
+ released = true;
+ notifyAll();
+ }
+ }
+
+ // Start and cancel tasks based on the current download and manager states.
+
+ private void syncTasks() {
+ int accumulatingDownloadTaskCount = 0;
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ @Nullable Task activeTask = activeTasks.get(download.request.id);
+ switch (download.state) {
+ case STATE_STOPPED:
+ syncStoppedDownload(activeTask);
+ break;
+ case STATE_QUEUED:
+ activeTask = syncQueuedDownload(activeTask, download);
+ break;
+ case STATE_DOWNLOADING:
+ Assertions.checkNotNull(activeTask);
+ syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount);
+ break;
+ case STATE_REMOVING:
+ case STATE_RESTARTING:
+ syncRemovingDownload(activeTask, download);
+ break;
+ case STATE_COMPLETED:
+ case STATE_FAILED:
+ default:
+ throw new IllegalStateException();
+ }
+ if (activeTask != null && !activeTask.isRemove) {
+ accumulatingDownloadTaskCount++;
+ }
+ }
+ }
+
+ private void syncStoppedDownload(@Nullable Task activeTask) {
+ if (activeTask != null) {
+ // We have a task, which must be a download task. Cancel it.
+ Assertions.checkState(!activeTask.isRemove);
+ activeTask.cancel(/* released= */ false);
+ }
+ }
+
+ @Nullable
+ @CheckResult
+ private Task syncQueuedDownload(@Nullable Task activeTask, Download download) {
+ if (activeTask != null) {
+ // We have a task, which must be a download task. If the download state is queued we need to
+ // cancel it and start a new one, since a new request has been merged into the download.
+ Assertions.checkState(!activeTask.isRemove);
+ activeTask.cancel(/* released= */ false);
+ return activeTask;
+ }
+
+ if (!canDownloadsRun() || activeDownloadTaskCount >= maxParallelDownloads) {
+ return null;
+ }
+
+ // We can start a download task.
+ download = putDownloadWithState(download, STATE_DOWNLOADING);
+ Downloader downloader = downloaderFactory.createDownloader(download.request);
+ activeTask =
+ new Task(
+ download.request,
+ downloader,
+ download.progress,
+ /* isRemove= */ false,
+ minRetryCount,
+ /* internalHandler= */ this);
+ activeTasks.put(download.request.id, activeTask);
+ if (activeDownloadTaskCount++ == 0) {
+ sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS);
+ }
+ activeTask.start();
+ return activeTask;
+ }
+
+ private void syncDownloadingDownload(
+ Task activeTask, Download download, int accumulatingDownloadTaskCount) {
+ Assertions.checkState(!activeTask.isRemove);
+ if (!canDownloadsRun() || accumulatingDownloadTaskCount >= maxParallelDownloads) {
+ putDownloadWithState(download, STATE_QUEUED);
+ activeTask.cancel(/* released= */ false);
+ }
+ }
+
+ private void syncRemovingDownload(@Nullable Task activeTask, Download download) {
+ if (activeTask != null) {
+ if (!activeTask.isRemove) {
+ // Cancel the downloading task.
+ activeTask.cancel(/* released= */ false);
+ }
+ // The activeTask is either a remove task, or a downloading task that we just cancelled. In
+ // the latter case we need to wait for the task to stop before we start a remove task.
+ return;
+ }
+
+ // We can start a remove task.
+ Downloader downloader = downloaderFactory.createDownloader(download.request);
+ activeTask =
+ new Task(
+ download.request,
+ downloader,
+ download.progress,
+ /* isRemove= */ true,
+ minRetryCount,
+ /* internalHandler= */ this);
+ activeTasks.put(download.request.id, activeTask);
+ activeTask.start();
+ }
+
+ // Task event processing.
+
+ private void onContentLengthChanged(Task task) {
+ String downloadId = task.request.id;
+ long contentLength = task.contentLength;
+ Download download =
+ Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false));
+ if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) {
+ return;
+ }
+ putDownload(
+ new Download(
+ download.request,
+ download.state,
+ download.startTimeMs,
+ /* updateTimeMs= */ System.currentTimeMillis(),
+ contentLength,
+ download.stopReason,
+ download.failureReason,
+ download.progress));
+ }
+
+ private void onTaskStopped(Task task) {
+ String downloadId = task.request.id;
+ activeTasks.remove(downloadId);
+
+ boolean isRemove = task.isRemove;
+ if (!isRemove && --activeDownloadTaskCount == 0) {
+ removeMessages(MSG_UPDATE_PROGRESS);
+ }
+
+ if (task.isCanceled) {
+ syncTasks();
+ return;
+ }
+
+ @Nullable Throwable finalError = task.finalError;
+ if (finalError != null) {
+ Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError);
+ }
+
+ Download download =
+ Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false));
+ switch (download.state) {
+ case STATE_DOWNLOADING:
+ Assertions.checkState(!isRemove);
+ onDownloadTaskStopped(download, finalError);
+ break;
+ case STATE_REMOVING:
+ case STATE_RESTARTING:
+ Assertions.checkState(isRemove);
+ onRemoveTaskStopped(download);
+ break;
+ case STATE_QUEUED:
+ case STATE_STOPPED:
+ case STATE_COMPLETED:
+ case STATE_FAILED:
+ default:
+ throw new IllegalStateException();
+ }
+
+ syncTasks();
+ }
+
+ private void onDownloadTaskStopped(Download download, @Nullable Throwable finalError) {
+ download =
+ new Download(
+ download.request,
+ finalError == null ? STATE_COMPLETED : STATE_FAILED,
+ download.startTimeMs,
+ /* updateTimeMs= */ System.currentTimeMillis(),
+ download.contentLength,
+ download.stopReason,
+ finalError == null ? FAILURE_REASON_NONE : FAILURE_REASON_UNKNOWN,
+ download.progress);
+ // The download is now in a terminal state, so should not be in the downloads list.
+ downloads.remove(getDownloadIndex(download.request.id));
+ // We still need to update the download index and main thread.
+ try {
+ downloadIndex.putDownload(download);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to update index.", e);
+ }
+ DownloadUpdate update =
+ new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads));
+ mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();
+ }
+
+ private void onRemoveTaskStopped(Download download) {
+ if (download.state == STATE_RESTARTING) {
+ putDownloadWithState(
+ download, download.stopReason == STOP_REASON_NONE ? STATE_QUEUED : STATE_STOPPED);
+ syncTasks();
+ } else {
+ int removeIndex = getDownloadIndex(download.request.id);
+ downloads.remove(removeIndex);
+ try {
+ downloadIndex.removeDownload(download.request.id);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to remove from database");
+ }
+ DownloadUpdate update =
+ new DownloadUpdate(download, /* isRemove= */ true, new ArrayList<>(downloads));
+ mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();
+ }
+ }
+
+ // Progress updates.
+
+ private void updateProgress() {
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.state == STATE_DOWNLOADING) {
+ try {
+ downloadIndex.putDownload(download);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to update index.", e);
+ }
+ }
+ }
+ sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS);
+ }
+
+ // Helper methods.
+
+ private boolean canDownloadsRun() {
+ return !downloadsPaused && notMetRequirements == 0;
+ }
+
+ private Download putDownloadWithState(Download download, @Download.State int state) {
+ // Downloads in terminal states shouldn't be in the downloads list. This method cannot be used
+ // to set STATE_STOPPED either, because it doesn't have a stopReason argument.
+ Assertions.checkState(
+ state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED);
+ return putDownload(copyDownloadWithState(download, state));
+ }
+
+ private Download putDownload(Download download) {
+ // Downloads in terminal states shouldn't be in the downloads list.
+ Assertions.checkState(download.state != STATE_COMPLETED && download.state != STATE_FAILED);
+ int changedIndex = getDownloadIndex(download.request.id);
+ if (changedIndex == C.INDEX_UNSET) {
+ downloads.add(download);
+ Collections.sort(downloads, InternalHandler::compareStartTimes);
+ } else {
+ boolean needsSort = download.startTimeMs != downloads.get(changedIndex).startTimeMs;
+ downloads.set(changedIndex, download);
+ if (needsSort) {
+ Collections.sort(downloads, InternalHandler::compareStartTimes);
+ }
+ }
+ try {
+ downloadIndex.putDownload(download);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to update index.", e);
+ }
+ DownloadUpdate update =
+ new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads));
+ mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();
+ return download;
+ }
+
+ @Nullable
+ private Download getDownload(String id, boolean loadFromIndex) {
+ int index = getDownloadIndex(id);
+ if (index != C.INDEX_UNSET) {
+ return downloads.get(index);
+ }
+ if (loadFromIndex) {
+ try {
+ return downloadIndex.getDownload(id);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to load download: " + id, e);
+ }
+ }
+ return null;
+ }
+
+ private int getDownloadIndex(String id) {
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.request.id.equals(id)) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ private static Download copyDownloadWithState(Download download, @Download.State int state) {
+ return new Download(
+ download.request,
+ state,
+ download.startTimeMs,
+ /* updateTimeMs= */ System.currentTimeMillis(),
+ download.contentLength,
+ /* stopReason= */ 0,
+ FAILURE_REASON_NONE,
+ download.progress);
+ }
+
+ private static int compareStartTimes(Download first, Download second) {
+ return Util.compareLong(first.startTimeMs, second.startTimeMs);
+ }
+ }
+
+ private static class Task extends Thread implements Downloader.ProgressListener {
+
+ private final DownloadRequest request;
+ private final Downloader downloader;
+ private final DownloadProgress downloadProgress;
+ private final boolean isRemove;
+ private final int minRetryCount;
+
+ @Nullable private volatile InternalHandler internalHandler;
+ private volatile boolean isCanceled;
+ @Nullable private Throwable finalError;
+
+ private long contentLength;
+
+ private Task(
+ DownloadRequest request,
+ Downloader downloader,
+ DownloadProgress downloadProgress,
+ boolean isRemove,
+ int minRetryCount,
+ InternalHandler internalHandler) {
+ this.request = request;
+ this.downloader = downloader;
+ this.downloadProgress = downloadProgress;
+ this.isRemove = isRemove;
+ this.minRetryCount = minRetryCount;
+ this.internalHandler = internalHandler;
+ contentLength = C.LENGTH_UNSET;
+ }
+
+ @SuppressWarnings("nullness:assignment.type.incompatible")
+ public void cancel(boolean released) {
+ if (released) {
+ // Download threads are GC roots for as long as they're running. The time taken for
+ // cancellation to complete depends on the implementation of the downloader being used. We
+ // null the handler reference here so that it doesn't prevent garbage collection of the
+ // download manager whilst cancellation is ongoing.
+ internalHandler = null;
+ }
+ if (!isCanceled) {
+ isCanceled = true;
+ downloader.cancel();
+ interrupt();
+ }
+ }
+
+ // Methods running on download thread.
+
+ @Override
+ public void run() {
+ try {
+ if (isRemove) {
+ downloader.remove();
+ } else {
+ int errorCount = 0;
+ long errorPosition = C.LENGTH_UNSET;
+ while (!isCanceled) {
+ try {
+ downloader.download(/* progressListener= */ this);
+ break;
+ } catch (IOException e) {
+ if (!isCanceled) {
+ long bytesDownloaded = downloadProgress.bytesDownloaded;
+ if (bytesDownloaded != errorPosition) {
+ errorPosition = bytesDownloaded;
+ errorCount = 0;
+ }
+ if (++errorCount > minRetryCount) {
+ throw e;
+ }
+ Thread.sleep(getRetryDelayMillis(errorCount));
+ }
+ }
+ }
+ }
+ } catch (Throwable e) {
+ finalError = e;
+ }
+ @Nullable Handler internalHandler = this.internalHandler;
+ if (internalHandler != null) {
+ internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
+ downloadProgress.bytesDownloaded = bytesDownloaded;
+ downloadProgress.percentDownloaded = percentDownloaded;
+ if (contentLength != this.contentLength) {
+ this.contentLength = contentLength;
+ @Nullable Handler internalHandler = this.internalHandler;
+ if (internalHandler != null) {
+ internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget();
+ }
+ }
+ }
+
+ private static int getRetryDelayMillis(int errorCount) {
+ return Math.min((errorCount - 1) * 1000, 5000);
+ }
+ }
+
+ private static final class DownloadUpdate {
+
+ public final Download download;
+ public final boolean isRemove;
+ public final List<Download> downloads;
+
+ public DownloadUpdate(Download download, boolean isRemove, List<Download> downloads) {
+ this.download = download;
+ this.isRemove = isRemove;
+ this.downloads = downloads;
+ }
+ }
+}